:root {
  --bg: #14141a;
  --bg-panel: #1c1c24;
  --bg-panel-raised: #23232e;
  --border: #33333f;
  --text: #e8e6df;
  --text-dim: #9a97a8;
  --silver: #c7c9d3;
  --silver-dim: #6f7180;
  --bronze: #b8834a;
  /* See sheet.css's matching :root for the full rationale; kept in sync since these two
     stylesheets share the same palette. */
  --bronze-dim: #9f7645;
  --accent: #8e7cc3;
  --accent-dim: #8476ad;
  /* #c0564f read at 4.09:1 on --bg - just under the 4.5:1 WCAG
     AA text-contrast minimum - and this color is used as actual text color for real functional
     content (the Delete Campaign button, the over-budget tracker pill), not just decoration.
     #c46059 clears 4.5:1 while staying the same rust-red. Matches sheet.css's --danger, which was
     missing entirely there before this same pass (see that file's comment). */
  --danger: #c46059;
  --ok: #5c9a6e;
  /* WCAG 1.4.3 text-only variants of --accent/--silver-dim for the tier-header bands below;
     those tokens stay untouched since both are also used as non-text elements elsewhere.
     See docs/Mistfall_CharacterBuilder_DevelopmentNotes.docx for the full rationale. Kept in
     sync with sheet.css's matching tokens. */
  --tier1-text: #9b8cca;
  --tier3-text: #8c8e9c;
  --radius: 6px;
  /* Visual polish pass (see docs/visual-design-polish.md items 1/3): every card/panel in
     this app used a flat 1px border with no elevation at all (the nav "More ▾" dropdown was the
     ONLY box-shadow anywhere in either stylesheet) - against a dark theme that reads as "boxes,"
     not "cards." These two tokens give cards a resting shadow and a slightly stronger one on
     hover/interaction, applied selector-by-selector below rather than globally, since not every
     bordered element (form inputs, chips, dividers) should get card-level depth. Kept in sync with
     sheet.css's matching tokens, same convention as --bg-panel-raised etc. */
  --shadow-card: 0 4px 14px rgba(0, 0, 0, 0.35);
  --shadow-card-hover: 0 6px 20px rgba(0, 0, 0, 0.5);
  /* Same pass, item 4 (subtle background texture, a faint noise, kept subtle): a
     procedural film-grain noise generated by an inline SVG feTurbulence filter rather than a
     sourced/commissioned texture image - this app has no asset pipeline for that yet (see the
     polish doc's items 5-7, which DO need real art production), and an inline data: URI needs no
     new file. The feColorMatrix's last row (`0 0 0 0.05 0`) is what keeps this subtle: it rewrites
     the noise's alpha channel to a flat 5%, so every pixel is white at 5% opacity at most rather
     than the harsh full-contrast static feTurbulence produces on its own. Applied only to a few
     larger content cards below (.sheet-section, .bst-card, .choice-card, .landing-campaign-box) -
     not small elements like .stat-box or .sheet-chip, where grain at that size would read as
     noise/dirt rather than parchment texture. This is a first pass tuned by reasoning about
     the math, not by eye (no way to render a live preview of a CSS-only change before deploy) - if
     it reads as too strong/weak/absent once you see it live, the fix is just this one value (either
     the 0.05 alpha, or removing --texture-grain from a selector's background-image entirely). */
  --texture-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch' result='noise'/%3E%3CfeColorMatrix in='noise' type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.05 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  --font: "Segoe UI", system-ui, -apple-system, sans-serif;
  --font-display: "Cinzel", Georgia, "Segoe UI", serif;
}

* { box-sizing: border-box; }

/* Standard "visually hidden but still in the accessibility tree" utility (WCAG 1.3.1/2.4.6) - for
   content a sighted user doesn't need (the app's visible branding is the wordmark image/logo, not
   plain text) but that screen readers and other assistive tech still need in the DOM, e.g. a page's
   one required <h1> landmark. display:none/visibility:hidden would hide it from assistive tech too,
   so this clips it to a 1px box instead - the standard technique (used by WordPress core, Bootstrap
   .sr-only, etc.) rather than something bespoke. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* WCAG 1.4.3 text-contrast fallback for any unstyled anchor - most links in this app already get a
   color from a more specific rule/class (e.g. .app-tool-btn, .nav-drawer-link), but a handful of
   plain <a> elements (e.g. index.html's footer link) inherited the browser default blue/purple,
   which fails contrast against --bg (and doesn't match the app's palette either). :not([class])
   only catches an anchor with no class of its own, so it never overrides a more specific existing
   rule regardless of source order - see sheet.css's matching rule for the full rationale;
   duplicated here because index.html/builder.html load this stylesheet instead of sheet.css. */
a:not([class]) {
  color: var(--bronze);
}

/* WCAG 2.4.7 visible focus indicator - see sheet.css's matching
   rule for the full rationale; duplicated here because index.html/builder.html load this
   stylesheet instead of sheet.css. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

body {
  margin: 0;
  background:
    radial-gradient(ellipse at 15% -10%, rgba(184, 131, 74, 0.07), transparent 45%),
    radial-gradient(ellipse at 100% 0%, rgba(142, 124, 195, 0.06), transparent 40%),
    var(--bg);
  color: var(--text);
  font-family: var(--font);
  line-height: 1.45;
}

/* Thin bronze/silver scrollbars (webkit) to match the metalwork accents used elsewhere */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--bg-panel); }
::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, var(--bronze-dim), var(--silver-dim));
  border-radius: 6px;
  border: 2px solid var(--bg-panel);
}

#app-header {
  position: relative;
  padding: 16px 24px;
  border-bottom: 1px solid var(--border);
  background: linear-gradient(180deg, #1e1e28, #14141a);
  /* NOT overflow:hidden: this would clip .nav-more-menu (the "More ▾"
     overflow dropdown), since the menu is a descendant of
     #app-header positioned at top: calc(100% + 6px) - i.e. deliberately just past the header's own
     bottom edge/divider line. With the header clipping its own overflow, the dropdown's items would get
     cropped away right at that edge instead of floating over the page content below. sheet.css's equivalent
     container (.sheet-actions) never had this overflow:hidden, which is why the same menu already
     worked correctly on every sheet.css-based narrator page (narratorToolkit, encounterBuilder,
     etc.) - only the style.css pages (narratorHome, playerHome, builder, index) were affected.
     Nothing else in this rule's box needs clipping: the background is a plain gradient (nothing to
     bleed past the edge), and #app-header::after's 1px bottom overhang is trivial and already sits
     under .nav-more-menu's z-index:200 regardless. */
}
/* Thin bronze-to-silver-to-bronze rule along the very bottom edge, echoing the
   metal trim on the talent-tree board art without shipping any image weight. */
#app-header::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--bronze) 15%, var(--silver-dim) 50%, var(--bronze) 85%, transparent);
  opacity: 0.8;
}
#app-header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
}
/* Quick links to the standalone Narrator/table tools (Nemesis Group Sheet, Group Resources, Party
   Dashboard, Combat Tracker, Tag Lookup) - previously plain links at the very bottom of the page,
   moved into the header as a proper button row so they're visible without scrolling. An overflow
   "More ▾" menu handles the case where they don't all fit (rejected alternatives: a
   nowrap+horizontal-scroll fix and a smaller-text fix) - renderAppNav() (nav.js) now builds its own internal .nav-row +
   .nav-more <details> structure inside whatever container it's given, adding the shared
   .app-nav-host class to that container so it can shrink correctly as a flex child here. #app-tools
   itself no longer needs its own wrap/scroll handling - it just hosts nav.js's structure. */
#app-tools {
  display: flex;
  align-items: center;
  gap: 8px;
}
.app-tool-btn {
  display: inline-block;
  flex-shrink: 0; /* keeps every pill full-size in .nav-row below, rather than the browser squeezing them - .nav-row handles "doesn't fit" by overflowing pills into the More menu instead of shrinking any of them */
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-panel-raised);
  color: var(--text-dim);
  font-size: 0.8rem;
  text-decoration: none;
  white-space: nowrap;
  transition: border-color 0.15s, color 0.15s, background 0.15s, transform 0.15s, box-shadow 0.15s;
}
/* Visual polish: a much smaller lift than the primary button's (-1px, no
   dedicated shadow token beyond a soft ambient one) - these are small pill links in a dense nav
   row, not standalone CTAs, so a full card-shadow lift would read as jumpy rather than tactile. */
.app-tool-btn:hover {
  border-color: var(--accent-dim);
  color: var(--text);
  background: var(--accent-dim);
  transform: translateY(-1px);
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
}

/* Overflow "More ▾" menu - see sheet.css's identical block for the full
   rationale/structure comment (kept in sync by hand across the two stylesheets, same convention as
   --bg-panel-raised etc. - this app has no build step and no shared/base CSS file). */
.app-nav-host {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  flex: 1 1 auto;
}
.nav-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  flex: 1 1 auto;
  overflow: hidden;
}
.nav-more {
  position: relative;
  flex-shrink: 0;
}
.nav-more-trigger {
  display: inline-block;
  list-style: none;
  cursor: pointer;
}
.nav-more-trigger::marker,
.nav-more-trigger::-webkit-details-marker {
  display: none;
}
.nav-more-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 180px;
  padding: 6px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-panel-raised);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}
.nav-more-menu .app-tool-btn,
.nav-more-menu .sheet-link {
  display: block;
  width: 100%;
  text-align: left;
  white-space: nowrap;
}
/* Left-side collapsible nav drawer - see nav.js's
   renderNavDrawer() header comment for the full design. `.nav-drawer-host` is a plain wrapper div
   (no visible box of its own - overlay + panel are both `position:fixed`, so the host's own size
   never matters) that gets `.nav-drawer-open` toggled on it by JS; every visible transition lives in
   these two child selectors reacting to that one class, nothing animated from script. */
.nav-drawer-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 10, 14, 0.55);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
  z-index: 300;
}
.nav-drawer-panel {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 280px;
  max-width: 82vw;
  background: var(--bg-panel-raised);
  border-right: 1px solid var(--border);
  box-shadow: 6px 0 28px rgba(0, 0, 0, 0.45);
  transform: translateX(-100%);
  transition: transform 0.25s ease;
  z-index: 301;
  padding: 18px 16px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.nav-drawer-host.nav-drawer-open .nav-drawer-overlay {
  opacity: 1;
  pointer-events: auto;
}
.nav-drawer-host.nav-drawer-open .nav-drawer-panel {
  transform: translateX(0);
}
.nav-drawer-close {
  align-self: flex-end;
  background: transparent;
  border: none;
  color: var(--text-dim);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  padding: 4px;
  margin-bottom: 10px;
}
.nav-drawer-close:hover { color: var(--text); }
.nav-drawer-home {
  font-family: var(--font-display);
  font-weight: 600;
  margin-bottom: 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}
/* Accent-tinted (not plain --text-dim) and set off by a hairline divider - now that both roles'
   drawers have several groups (see nav.js's own NAV_CATEGORIES comment), a plain gray label read too
   close to the links themselves. The first group keeps the old tight spacing/no divider, right under
   the "← Home" link's own divider - two rules back to back there would double up. */
.nav-drawer-category {
  text-transform: uppercase;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--accent);
  /* 2026-09-09: tightened from 18px/6px margin + 12px padding-top - with 4 categories and 16
     links in the Narrator drawer, the old spacing pushed the panel past viewport height on
     typical screens, forcing a scrollbar. This (plus .nav-drawer-link's own trim below) removes
     it without visibly cramming the list. */
  margin: 14px 0 5px;
  padding-top: 9px;
  border-top: 1px solid var(--border);
}
.nav-drawer-category:first-of-type {
  margin-top: 2px;
  padding-top: 0;
  border-top: none;
}
.nav-drawer-link {
  display: block;
  padding: 6px 10px;
  border-radius: 6px;
  color: var(--text);
  text-decoration: none;
  font-size: 0.9rem;
}
.nav-drawer-link:hover { background: var(--bg-panel); }
/* The hamburger trigger itself sits in the compact hero header (see .app-header-hero-compact above)
   - a plain three-bar icon (no pseudo-element tricks) matching this app's otherwise unadorned button
   language (e.g. the "More ▾" trigger next to it elsewhere in the app). */
.nav-hamburger-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 40px;
  height: 36px;
  background: rgba(20, 20, 26, 0.55);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}
.nav-hamburger-btn:hover { border-color: var(--silver-dim); }
.nav-hamburger-bar {
  display: block;
  width: 18px;
  height: 2px;
  background: var(--text);
  border-radius: 1px;
}
#app-header h1 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.55rem;
  letter-spacing: 0.03em;
  color: var(--silver);
  text-shadow: 0 0 18px rgba(184, 131, 74, 0.25);
}
#app-header .subtitle {
  margin: 3px 0 0;
  color: var(--text-dim);
  font-size: 0.85rem;
  letter-spacing: 0.01em;
}
/* Plain (unscoped) fallback for the same look outside a header - narratorHome.html's
   "Narrator — Your Campaigns" subtitle used to sit inside
   #app-header next to the old plain-text <h1>; now that the header only holds the hamburger trigger
   + wordmark (see .app-header-hero-compact above), the subtitle moved down into <main> instead, where
   the #app-header-scoped rule above no longer reaches it. builder.html/playerHome.html still use
   .subtitle inside their own #app-header exactly as before - unaffected, since that rule's higher
   specificity (an ID + a class) still wins there regardless of this plain-class rule existing too. */
.subtitle {
  margin: 3px 0 0;
  color: var(--text-dim);
  font-size: 0.85rem;
  letter-spacing: 0.01em;
}
/* Landing page hero header: img/mistfall-header-image.jpg is a wide illustration with its character
   lineup clustered on the right ~55-60% and an open, foggy road on the left ~40-45%. background-
   size: cover + background-position: center keeps that right-heavy framing intact at any header
   width (a short, very wide header crops mostly top/bottom off this image, not left/right, so the
   characters stay in frame); #app-wordmark (below) is pinned to the LEFT of the row specifically so
   it always sits over the empty road rather than the artwork, at any viewport width down to the
   `max-width: 700px` breakpoint where the image is dropped entirely (a small header on a phone
   doesn't have enough width for logo + art to coexist without one crowding the other - simplest fix
   is to not show the art there, same tradeoff as any responsive hero banner). The soft left-to-
   transparent gradient over the image is pure legibility insurance for the logo, not a stylistic
   choice - without it, a lighter patch of sky/fog directly behind the chrome lettering at some
   viewport width would fight with it for contrast.
   `.app-header-hero` is its own class, not just `#app-header` - builder.html shares that same ID
   for its compact title+nav header and must never pick up any of this. */
#app-header.app-header-hero {
  background-image: linear-gradient(90deg, rgba(20, 20, 26, 0.6) 0%, rgba(20, 20, 26, 0.25) 42%, rgba(20, 20, 26, 0) 62%),
    url("../img/mistfall-header-image.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  /* Checked precisely against the source art (2000x1342): the skull lying in the road sits
     around y=1050-1200, the wings' tops around y=100 - showing both needs roughly 1100-1150px of
     the image's own vertical span in frame at once. At a typical 1600-1920px-wide viewport,
     background-size:cover's scale factor is ~0.8-0.96 (width-driven, image is wider relative to its
     height than the header is), so the header needs to be roughly that span * scale ≈ 900px tall
     for a centered background-position to reveal wingtips through the skull without cropping either
     one - confirmed by rendering the actual crop math against the real file before committing to
     this number, not eyeballed. */
  min-height: 900px;
  display: flex;
  /* flex-start (rather than align-items: center, which would vertically center it in the 900px-tall
     banner) plus this rule's own padding pins the wordmark to the top-left corner of the padded box. */
  align-items: flex-start;
  padding: 32px 32px;
}
@media (max-width: 700px) {
  #app-header.app-header-hero {
    background-image: linear-gradient(180deg, #1e1e28, #14141a);
    min-height: 0;
    padding: 16px 24px;
  }
}
/* `margin: 0` (not `0 auto`) keeps this pinned to the start of #app-header-inner's flex row rather
   than centered - with only one child, the row's own `justify-content: space-between` already puts
   a lone item at the start on its own, so no change to #app-header-inner itself was needed either.
   This rule's selector is just `#app-wordmark`, not scoped to `.app-header-hero`, since that ID only
   ever appears on index.html regardless.
   Width is a `clamp()` tied to viewport width rather than one fixed `max-width` - the hero's own
   background art scales with viewport width too (that's what background-size:cover does), so a
   static logo size looked increasingly small/lost on wide screens and oversized on narrow ones
   instead of growing and shrinking in step with it. The subtitle line ("An Urban Fantasy Narrative
   Roleplaying Game") is a thin, dim stroke in the source art, occupying only the bottom ~10% of the
   whole graphic, so it's the first thing to become illegible as the logo shrinks; 40vw (~640px at
   1600px wide, ~750px at 1920px, both checked against the real art) reads clearly at both while
   still leaving well over 150px of clearance before the character art starts on the right at any
   width tested. `filter: drop-shadow(...)` adds a soft dark halo hugging every opaque pixel
   (respects the PNG's real alpha channel, confirmed transparent) as extra insurance against a
   lighter patch of sky/fog landing behind the dimmer subtitle stroke at some viewport width. */
#app-wordmark {
  display: block;
  margin: 0;
  width: clamp(340px, 40vw, 750px);
  height: auto;
  position: relative;
  z-index: 1;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.75));
}
@media (max-width: 700px) {
  #app-wordmark { margin: 0 auto; }
  #app-header.app-header-hero #app-header-inner { justify-content: center; }
}
/* Compact hero header variant, used on narratorHome.html.
   Reuses .app-header-hero's own background-image/gradient/size/position declarations above
   unchanged (same art, same treatment) but overrides the height way down from index.html's 900px.
   That 900px only works on index.html because the entire hero card fits in a single 100vh page
   below it (see body:has(#landing-page .landing-hero) further down) - narratorHome.html has no such
   treatment (it's a real content page: campaign lists, a create-campaign form, this drawer's own
   trigger), so a 900px banner here would just reproduce the exact "lots of scrolling past artwork
   before reaching anything useful" complaint that drove index.html's whole hero redesign in the
   first place. Both selectors are fully qualified with #app-header.app-header-hero (matching the
   specificity of the rules they override above) so this doesn't need to rely on source order to win. */
#app-header.app-header-hero.app-header-hero-compact {
  min-height: 150px;
  align-items: center;
  padding: 20px 32px;
}
/* justify-content:flex-start (not the base rule's space-between) keeps the hamburger trigger and
   the wordmark clustered together on the left, since the nav drawer opens from the left side and
   would read oddly if the wordmark drifted to the opposite edge of a two-child row. */
#app-header.app-header-hero.app-header-hero-compact #app-header-inner {
  justify-content: flex-start;
  gap: 16px;
}
/* A smaller width than the base #app-wordmark rule's clamp(340px, 40vw, 750px) - that range assumes
   a ~900px-tall banner with room for a big logo; scaled down to fit comfortably inside this
   variant's much shorter 150px. */
#app-header.app-header-hero.app-header-hero-compact #app-wordmark {
  width: clamp(190px, 18vw, 260px);
}
@media (max-width: 700px) {
  #app-header.app-header-hero.app-header-hero-compact {
    min-height: 0;
    padding: 16px 24px;
  }
  #app-header.app-header-hero.app-header-hero-compact #app-header-inner {
    justify-content: flex-start;
  }
}
#build-tag {
  color: var(--bronze);
  border: 1px solid var(--bronze-dim);
  border-radius: 3px;
  padding: 0 5px;
  font-size: 0.75rem;
}

/* Scoped via :has() to builder.html specifically - the only page with a #app-body, same "same
   shared #app-header/#app-footer ID, this one page needs different container behavior" situation
   the existing body:has(#landing-page .landing-hero) rule further down handles for the landing
   page's hero variant. #app itself has no rule anywhere else, so this can't affect
   narratorHome/playerHome/index/profile, which all still rely on natural whole-page scrolling.
   Turning #app into a fixed-viewport-height flex column (header/footer at their natural,
   real-rendered height - including the footer note text wrapping to two lines on narrower widths -
   #app-body given exactly what's left) is what lets #app-body's own height rule below give its
   step-nav/step-content/char-sheet columns a real, stable height to scroll within, without
   guessing at the header/footer's combined pixel height the way an earlier version of this rule
   did (see the CHANGELOG entry this shipped under for that gap). */
body:has(#app-body) #app {
  display: flex;
  flex-direction: column;
  height: 100vh;
}
body:has(#app-body) #app-header,
body:has(#app-body) #app-footer {
  flex: none;
}
/* #step-container and #char-sheet already each carry their own overflow-y: auto (see those rules
   below), but that only ever does anything once #app-body itself stops growing to fit its tallest
   child - with only a min-height, the grid row always stretched to whichever column was tallest
   (usually #step-content, the middle "selection" column, on a heritage/class step with a lot of
   cards), so overflow-y: auto on the OTHER column never had anything to overflow within, and the
   whole page scrolled together instead of each column scrolling on its own. flex: 1 1 auto (this is
   now also a flex item of #app, per the rule above) plus min-height: 0 (grid items default to
   min-height: auto, which would otherwise still force this to stretch to its tallest child despite
   the flex sizing) turns each column into its own independently-scrolling pane - the middle
   "selection" column and the right "summary" (#char-sheet) column scroll separately, matching the
   account nav's own panel styling, and #step-nav itself (11 rows) also gets its own scrollbar rather
   than being clipped on a short window. (#step-content itself is a further nested flex column, not
   the scrolling element directly - see its own comment further down for why: its #step-footer
   child needs to stay out of the scrolled area entirely, not just sit at the bottom of it.) */
#app-body {
  display: grid;
  grid-template-columns: 220px 1fr 340px;
  gap: 0;
  flex: 1 1 auto;
  min-height: 0;
}
/* The sign-in gate (docs/builder-signin-gate.md, main.js's boot()) starts #app-body with a plain
   `hidden` attribute and only clears it once a signed-in user is confirmed. Without this rule, the
   ID selector above (specificity 1-0-0) beats the browser's own low-specificity `[hidden] {
   display: none }` default, so `hidden` alone does nothing here and the whole wizard - card grid,
   footer buttons, character sheet preview - stays visible underneath the "Loading…"/sign-in-prompt
   gate host the whole time. */
#app-body[hidden] {
  display: none;
}

/* Step nav */
#step-nav {
  border-right: 1px solid var(--border);
  padding: 16px 10px;
  background: var(--bg-panel);
  /* min-height: 0 overrides the grid item default of min-height: auto (matching flexbox's own
     min-size:auto default), which would otherwise force this column to stretch to fit its full
     11-row content regardless of #app-body's own fixed height above, defeating overflow-y below. */
  min-height: 0;
  overflow-y: auto;
}
.nav-step {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 10px;
  border-radius: var(--radius);
  cursor: pointer;
  color: var(--text-dim);
  font-size: 0.88rem;
  margin-bottom: 2px;
  border: 1px solid transparent;
}
.nav-step:hover { background: var(--bg-panel-raised); }
.nav-step.active {
  background: var(--bg-panel-raised);
  color: var(--text);
  border-color: var(--accent-dim);
  box-shadow: inset 3px 0 0 var(--accent);
}
.nav-step.complete .nav-dot { background: var(--ok); border-color: var(--ok); box-shadow: 0 0 6px rgba(92, 154, 110, 0.6); }
.nav-dot {
  width: 9px; height: 9px; border-radius: 50%;
  border: 1px solid var(--silver-dim);
  flex: none;
  transition: background .15s, box-shadow .15s;
}

/* Mobile step drawer (builder.html only - a phone-width problem the 1100px tablet breakpoint below
   never addressed). Above 700px #step-nav is the plain 180px/220px sidebar it's always been - these
   rules do nothing until the @media (max-width: 700px) block further down actually repositions
   #step-nav itself as a fixed off-canvas panel and gives this button/overlay something to control.
   Below that width, #step-nav's own 11-row list (one per wizard step) was eating close to half the
   screen permanently, squeezing #step-content's card grids (each card needs a 260px minimum) into a
   sliver too narrow to lay out at all - this trigger swaps that permanent sidebar for the same
   "hidden until asked for" pattern the account nav's own hamburger drawer already uses elsewhere in
   this app (nav.js's renderNavDrawer()), just built directly here rather than through that function -
   renderNavDrawer() is wired specifically for categorizedNavItemsForRole()'s href-based page links,
   not this wizard's own onclick-driven, active/complete-tracking step list, so main.js's
   openStepNav()/closeStepNav() are a small purpose-built equivalent instead of a forced reuse. */
#step-nav-toggle {
  display: none;
  align-items: center;
  gap: 6px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-size: 0.9rem;
  padding: 10px 14px;
  cursor: pointer;
  width: 100%;
  text-align: left;
}
#step-nav-toggle:hover { border-color: var(--silver-dim); }
#step-nav-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(10, 10, 14, 0.55);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
  z-index: 300;
}
/* The actual @media (max-width: 700px) block for this drawer lives further down, right after the
   @media (max-width: 1100px) tablet breakpoint's own #app-body { grid-template-columns: ... } rule
   (search "Mobile step drawer" again) - NOT here next to its base styles above. Below 700px, both
   media queries match at once, and since the 1100px block's #app-body rule comes later in the
   cascade than this comment's original position, it would otherwise win over this drawer's own
   grid-template-columns override (same specificity, later source order wins) and leave #app-body
   stuck at the tablet's 180px/1fr two-column grid instead of collapsing to one. */
@media (max-width: 700px) {
  #step-nav-toggle { display: flex; }
  #step-nav-overlay { display: block; }
  #step-nav-overlay.step-nav-open { opacity: 1; pointer-events: auto; }
}

/* Main content
   Bryan (2026-09-11), on a screenshot of the Heritage step: "another floating bar situation going
   on" - the Back/Start Over/Next bar was sitting mid-screen on top of card text instead of staying
   out of the way. Root cause: #step-footer (position: sticky; bottom: 0 - see its own rule further
   down) used to live INSIDE this scrolling element, as a sibling of #step-container. Sticky sticks
   to the bottom of its nearest scrolling ancestor's visible viewport, not the true bottom of the
   page, so while scrolling through a tall card grid (Heritage/Class especially, comparison cards
   easily taller than the column) the button bar would park itself over whatever row of cards
   currently filled the bottom of the column, hovering over live content rather than sitting below
   it - exactly what the screenshot shows: card text visible both above and below the bar.
   The fix moves the scrolling itself down a level: #step-content is now the flex column, only
   #step-container (the actual per-step card content) scrolls, and #step-footer is a normal
   (non-sticky, non-overlapping) flex item below it - always visible without needing to scroll to
   it, but never drawn on top of anything. See CHANGELOG.md for this entry. */
#step-content {
  display: flex;
  flex-direction: column;
  padding: 0;
  /* See #app-body's own comment above - grid items need min-height: 0 explicitly, or this stretches
     to its content's full height (the grid item default) and overflow-y never triggers. */
  min-height: 0;
}
#step-container {
  flex: 1 1 auto;
  overflow-y: auto;
  min-height: 0;
  padding: 22px 28px;
}
#step-container h2 {
  margin-top: 0;
  color: var(--silver);
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: 0.02em;
  font-size: 1.3rem;
  border-bottom: 1px solid var(--border);
  padding-bottom: 8px;
  position: relative;
}
#step-container h2::after {
  content: "";
  position: absolute;
  left: 0; bottom: -1px;
  width: 64px;
  height: 1px;
  background: var(--bronze);
}
.step-intro {
  color: var(--text-dim);
  font-size: 0.92rem;
  margin-bottom: 16px;
}
.rule-block {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-left: 3px solid var(--bronze-dim);
  border-radius: var(--radius);
  padding: 10px 14px;
  margin-bottom: 12px;
  font-size: 0.88rem;
  color: var(--text-dim);
}
.rule-block strong { color: var(--text); }

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 12px;
  margin-bottom: 10px;
}
.choice-card {
  background-color: var(--bg-panel);
  /* Visual polish: --texture-grain, see :root's comment. */
  background-image: var(--texture-grain);
  border: 1px solid var(--border);
  /* Visual polish: bronze top accent + resting shadow, same "card has a spine"
     treatment as .sheet-section/.stat-box/.pd-card in sheet.css - .choice-card is this stylesheet's
     equivalent recurring content card (the wizard's Heritage/Class/Background option grid), so it
     was the one glaring gap in style.css for this treatment. Border-color changes on :hover/.selected
     below already override border-top's color too (shorthand `border-color` touches all four sides),
     which reads as an intentional accent shift on interaction rather than a conflict. */
  border-top: 3px solid var(--bronze-dim);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding: 12px 14px;
  cursor: pointer;
  transition: border-color .12s, background .12s, transform .12s, box-shadow .12s;
}
.choice-card:hover {
  border-color: var(--silver-dim);
  background-color: var(--bg-panel-raised);
  transform: translateY(-1px);
  box-shadow: var(--shadow-card-hover);
}
.choice-card.selected {
  border-color: var(--accent);
  background: #262238;
  box-shadow: 0 0 0 1px var(--accent-dim), 0 4px 14px rgba(142, 124, 195, 0.18);
}
.choice-card.disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.choice-card.disabled:hover { transform: none; }
.choice-card h3 {
  margin: 0 0 4px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: 0.01em;
  color: var(--text);
}
.choice-card .tag {
  display: inline-block;
  font-size: 0.7rem;
  color: var(--bronze);
  border: 1px solid var(--bronze-dim);
  border-radius: 3px;
  padding: 1px 6px;
  margin-bottom: 6px;
}
.choice-card p {
  margin: 4px 0;
  font-size: 0.83rem;
  color: var(--text-dim);
}
.choice-card .benefit {
  color: var(--silver);
  font-size: 0.83rem;
  margin-top: 6px;
}

/* Visual polish - see sheet.css's matching rule/comment for why this class
   exists (narratorToolkit.js's/homebrew.js's bare per-generator <h2>s had no styling at all). No
   style.css page uses it yet, but this app has no shared/base CSS file, so a primitive like this is
   kept in sync across both stylesheets by hand regardless - same convention as sheet.css's own
   .ghost copy of this file's button.ghost. */
.section-heading {
  margin: 20px 0 8px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--bronze);
  border-bottom: 1px solid var(--bronze-dim);
  padding-bottom: 6px;
}
.section-heading:first-child { margin-top: 0; }

.subgroup-title {
  /* Visual polish: extends the Cinzel display font to this eyebrow-style
     label too, matching .sheet-section h2/.stat-strip-title's already-Cinzel treatment in
     sheet.css - color/size/border are left untouched, since silver-dim (not bronze) here is a
     deliberate, distinct look from those, marking this as a collapsible sub-group rather than a
     full section header. */
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--silver-dim);
  text-transform: uppercase;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  margin: 18px 0 8px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--border);
}

/* Collapsible Step 11 review sections (Skills/Manifestations/Talents) - native <details>, same
   .subgroup-title look on the <summary>, plus a rotating disclosure arrow. */
summary.subgroup-title {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
}
summary.subgroup-title::-webkit-details-marker { display: none; }
summary.subgroup-title::after {
  content: "\25BE";
  margin-left: auto;
  font-size: 0.75rem;
  color: var(--text-dim);
  transition: transform 0.15s ease;
}
.subgroup-collapsible:not([open]) summary.subgroup-title::after { transform: rotate(-90deg); }
.subgroup-collapsible:not([open]) summary.subgroup-title { border-bottom: none; }

/* Step 6 Skills - 3-column grid so the 6 characteristics form two rows of three instead of one
   long vertical stack. Falls back to a single column on narrow viewports. */
.skills-columns {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px 24px;
}
@media (max-width: 900px) {
  .skills-columns { grid-template-columns: 1fr; }
}

/* Step 6 Mundane Talents - each tree collapses into a <details> (native disclosure arrow, same
   visual language as the other collapsible sections) so a full characteristic's talent list isn't
   one long scroll of text. */
.talent-tree-collapsible {
  margin-bottom: 6px;
  padding-left: 8px;
}
.talent-tree-collapsible summary {
  font-size: 0.85rem;
  color: var(--text);
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 0;
}
.talent-tree-collapsible summary::-webkit-details-marker { display: none; }
.talent-tree-collapsible summary::after {
  content: "\25BE";
  margin-left: auto;
  font-size: 0.7rem;
  color: var(--text-dim);
  transition: transform 0.15s ease;
}
.talent-tree-collapsible:not([open]) summary::after { transform: rotate(-90deg); }
.talent-tree-collapsible .talent-tree-body { padding-top: 2px; }

.checkbox-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 4px;
}
.checkbox-row:hover { background: var(--bg-panel-raised); }
.checkbox-row label { font-size: 0.86rem; }
.checkbox-row .desc { color: var(--text-dim); font-size: 0.78rem; }

.budget-tracker {
  display: inline-block;
  font-size: 0.8rem;
  padding: 3px 10px;
  border-radius: 12px;
  border: 1px solid var(--border);
  margin-bottom: 10px;
  color: var(--text-dim);
}
.budget-tracker.ok { border-color: var(--ok); color: var(--ok); }
.budget-tracker.over { border-color: var(--danger); color: var(--danger); }

select, input[type=text], input[type=number] {
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 4px;
  padding: 6px 8px;
  font-family: var(--font);
  font-size: 0.88rem;
}

#step-footer {
  /* No longer position: sticky - see #step-content's comment above. This is now a normal flex
     item sitting below the scrolling #step-container, so it never overlaps card content; flex:
     none keeps it at its natural height instead of stretching to share #step-content's leftover
     space with #step-container. */
  flex: none;
  display: flex;
  gap: 10px;
  padding: 14px 28px;
  border-top: 1px solid var(--border);
  background: var(--bg);
}
button {
  background: linear-gradient(180deg, #9a87d1, var(--accent));
  color: #fff;
  border: none;
  border-radius: var(--radius);
  padding: 9px 18px;
  font-size: 0.88rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: filter .12s, transform .12s, box-shadow .12s;
}
/* Visual polish: this already lifted on hover - just adding the new shadow
   token so the lift reads as the button rising off the page, not just moving up 1px with nothing
   to sell the depth. Secondary/tertiary variants below (.ghost, #btn-back) deliberately don't get
   this - keeping their hover subtle (border/color only) preserves the existing visual hierarchy
   between this primary action button and everything styled to look secondary next to it. */
button:hover:not(:disabled) { filter: brightness(1.08); transform: translateY(-1px); box-shadow: var(--shadow-card-hover); }
button:disabled { opacity: 0.35; cursor: not-allowed; }
button.ghost {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-dim);
  margin-left: auto;
}
button.ghost:hover:not(:disabled) { border-color: var(--silver-dim); color: var(--text); filter: none; }

/* Accessibility Review v11, A21: modeSwitchLink/forgotLink (landing.js) moved from <a href="#">
   to real <button> elements so they announce and behave as buttons (Space works, no dead href
   target) - this keeps their old plain-link look. Form controls don't inherit font-family/size
   from an ancestor the way <a> does, so font: inherit is required here or these would render in
   the browser's default UI font instead of matching the surrounding text. The generic button hover
   (brightness/lift/shadow, above) reads wrong for a plain-text link, so it's turned off here the
   same way .ghost does above - just a color change on hover, like the <a> it replaced. */
button.link-button {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: var(--bronze);
  text-decoration: underline;
  cursor: pointer;
}
button.link-button:hover:not(:disabled) { color: var(--text); filter: none; transform: none; box-shadow: none; }
/* WCAG 4.1.2: styles a real <a> (not a button nested in one) to look like the app's primary
   button - see docs/Mistfall_CharacterBuilder_DevelopmentNotes.docx for the full rationale. */
a.btn-link-primary {
  display: inline-block;
  background: linear-gradient(180deg, #9a87d1, var(--accent));
  color: #fff;
  border-radius: var(--radius);
  padding: 9px 18px;
  font-size: 0.88rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  text-decoration: none;
  cursor: pointer;
  transition: filter .12s, transform .12s, box-shadow .12s;
}
a.btn-link-primary:hover { filter: brightness(1.08); transform: translateY(-1px); box-shadow: var(--shadow-card-hover); }
button#btn-back {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
}
button#btn-back:hover:not(:disabled) { border-color: var(--silver-dim); filter: none; }

/* Character sheet sidebar */
#char-sheet {
  position: relative;
  border-left: 1px solid var(--border);
  background: var(--bg-panel);
  padding: 18px 16px;
  overflow-y: auto;
  /* See #app-body's own comment above - grid items need min-height: 0 explicitly, or this stretches
     to its content's full height (the grid item default) and overflow-y never triggers. */
  min-height: 0;
}
#char-sheet::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--bronze), transparent);
  opacity: 0.7;
}
#char-sheet h2 {
  margin: 0 0 12px;
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: 0.02em;
  font-size: 1.05rem;
  color: var(--silver);
}
/* Character name - compact identity field above the tiers (not a titled .cs-section), matching
   step11_summary.js's page <h2> / sheet.js's page <h1> treatment of the name as a heading rather
   than a field bundled into "Campaign" below it. */
.cs-name-header {
  margin-bottom: 14px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
.cs-name-header input {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--silver);
}
.cs-section {
  margin-bottom: 14px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
/* h3, not h4: see charSheet.js's section() comment -
   this was skipping a level under the h2 that always precedes it (builder.html's sidebar or
   step11_summary.js's review step). */
.cs-section h3,
.cs-section-collapsible summary {
  /* Visual polish: the character sheet sidebar's own section titles were the
     one clear gap in this file's Cinzel coverage - #char-sheet's own h2 (above), .cs-name-header,
     .landing-section-title, and #step-container h2 all already used the display font; these
     per-section titles (Campaign, Characteristics, Skills, etc. - repeated ~15-20x down the sidebar)
     didn't. Shared with .cs-section-collapsible summary below (the collapsible sidebar sections -
     Skills/Manifestations/Talents) - same title styling, the collapsible variant just adds the
     extra layout/interaction declarations in its own rule further down. */
  font-family: var(--font-display);
  font-weight: 600;
  margin: 0 0 6px;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--bronze);
}
.cs-section h3::before {
  content: "\2756  ";
  font-size: 0.7rem;
  color: var(--bronze-dim);
}
.cs-row {
  display: flex;
  justify-content: space-between;
  font-size: 0.84rem;
  padding: 2px 0;
}
.cs-row .val { color: var(--silver); }
.cs-empty { color: var(--text-dim); font-size: 0.82rem; font-style: italic; }

/* Collapsible sidebar sections (Skills/Manifestations/Talents) - native <details>, same diamond-
   bullet title styling as .cs-section h3 above (shared rule), plus a rotating disclosure arrow. */
.cs-section-collapsible summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 4px;
}
.cs-section-collapsible summary::-webkit-details-marker { display: none; }
.cs-section-collapsible summary::before {
  content: "\2756";
  font-size: 0.7rem;
  color: var(--bronze-dim);
}
.cs-section-collapsible summary::after {
  content: "\25BE";
  margin-left: auto;
  font-size: 0.7rem;
  color: var(--text-dim);
  transition: transform 0.15s ease;
}
.cs-section-collapsible:not([open]) summary::after { transform: rotate(-90deg); }
.cs-section-collapsible .cs-section-body { padding-top: 2px; }

/* Tier banners group sidebar sections by how often they're actually referenced at the table -
   Tier 1 "checked on every roll" (Characteristics/Skills/Status/Defense/Conditions), Tier 2
   "checked often" (Gear, Companions, Talents, Injuries), Tier 3 "background & downtime" (Origin,
   Class Features, Resources, Foundation, Nemesis arc). Purely a visual grouping aid - it doesn't
   collapse or hide anything, sections within a tier still render normally above/below it. */
.cs-tier-header {
  margin: 20px 0 10px;
  padding: 5px 10px;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  border-radius: var(--radius);
}
.cs-tier-header:first-child { margin-top: 0; }

/* Same tier banners, sized for the main content column (Step 11 Review & Export) rather than the
   narrow sidebar - reuses .subgroup-title's position/spacing but with the tier accent color. */
.tier-header {
  text-transform: uppercase;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  margin: 22px 0 10px;
  padding: 6px 12px;
  border-radius: var(--radius);
}
.tier-header:first-child { margin-top: 0; }
/* Shared with .cs-tier-header above (same tier accent color, deliberately) - declared once here
   rather than twice. */
.cs-tier-header.tier-1, .tier-header.tier-1 { background: rgba(142, 124, 195, 0.15); color: var(--tier1-text); border: 1px solid var(--accent-dim); }
.cs-tier-header.tier-2, .tier-header.tier-2 { background: rgba(184, 131, 74, 0.14); color: var(--bronze); border: 1px solid var(--bronze-dim); }
.cs-tier-header.tier-3, .tier-header.tier-3 { background: rgba(199, 201, 211, 0.08); color: var(--tier3-text); border: 1px solid var(--border); }

.char-grid {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 4px 8px;
  align-items: center;
}
.char-grid input[type=number] {
  width: 54px;
  text-align: center;
}

/* Rank input + die-per-rank badge (Skills/Manifestations - Ch.2 Table 2-1) shown side by side
   inside a .char-grid's second column. */
.rank-with-die {
  display: flex;
  align-items: center;
  gap: 6px;
}
.die-badge {
  font-size: 0.72rem;
  color: var(--bronze);
  border: 1px solid var(--bronze-dim);
  border-radius: 3px;
  padding: 1px 6px;
  white-space: nowrap;
}

.list-tag {
  display: inline-block;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  border-radius: 3px;
  font-size: 0.75rem;
  padding: 2px 7px;
  margin: 2px 3px 2px 0;
}

#app-footer {
  border-top: 1px solid var(--border);
  padding: 10px 24px;
  background: var(--bg-panel);
}
#app-footer p {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.76rem;
  letter-spacing: 0.01em;
}

/* F2 (Campaign Simulation v1): in a small window the Builder's pinned footer (a 3-4 line note once it
   wraps) plus the Back/Next bar left the step's own content only ~200px. There, the informational
   note is dropped, the legal links stay on one short line, and the Back/Next bar is tightened, so
   the step content keeps most of the screen. */
@media (max-width: 700px), (max-height: 760px) {
  body:has(#app-body) #app-footer { padding: 4px 16px; }
  body:has(#app-body) #app-footer .app-footer-note { display: none; }
  #step-footer { padding: 8px 16px; }
}

/* F20: compact name fields for when the Character Sheet sidebar is hidden (main.js #builder-identity). */
#builder-identity { display: none; }
@media (max-width: 1100px) {
  #app-body { grid-template-columns: 180px 1fr; }
  #char-sheet { display: none; }
  #builder-identity {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 6px 10px;
    align-items: center;
    padding: 14px 28px 0;
    max-width: 560px;
  }
  #builder-identity label { font-size: 0.8rem; color: var(--text-dim); }
  #builder-identity input { width: 100%; }
}
@media (max-width: 480px) {
  #builder-identity { grid-template-columns: 1fr; padding: 12px 16px 0; }
}

/* Mobile step drawer, continued (base #step-nav-toggle/#step-nav-overlay styles and the
   display:flex/block half of this media query are further up - search "Mobile step drawer" for that
   block's own comment on why the rest of this lives down here instead of next to it). This has to
   come after the @media (max-width: 1100px) block directly above so its #app-body rule wins the
   cascade at phone widths, where both media queries match at once. */
@media (max-width: 700px) {
  /* F2: the Steps button is its own grid row - without explicit rows the two rows split the height
     evenly, so the button stretched to half the screen and the step content got the other half. */
  #app-body { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); }
  #step-nav-toggle { margin: 6px 8px 0; width: auto; }
  /* Same slide-in-from-the-left mechanics as .nav-drawer-panel (see that rule's own comment) -
     not literally sharing the class, since #step-nav is already a real, permanently-present element
     with its own id-based sidebar styling above, not a host+overlay+panel triple built fresh by JS -
     but the visual language (280px/82vw-capped panel, translateX slide, same shadow/z-index) matches
     on purpose so this reads as the same interaction pattern as the account nav's drawer. */
  #step-nav {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: 280px;
    max-width: 82vw;
    box-shadow: 6px 0 28px rgba(0, 0, 0, 0.45);
    transform: translateX(-100%);
    transition: transform 0.25s ease;
    z-index: 301;
  }
  #step-nav.step-nav-open { transform: translateX(0); }
}

/* Landing page (index.html/landing.js) - role picker + local-only campaign create/continue. Reuses
   .card-grid/.choice-card (above) for the Narrator/Player cards rather than inventing new card
   styling, and the global select/input/button rules above for the campaign form. */
#landing-page {
  max-width: 760px;
  margin: 0 auto;
  padding: 32px 24px 90px;
}
.landing-section-title {
  margin: 0 0 14px;
  color: var(--silver);
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: 0.02em;
  font-size: 1.15rem;
  /* Visual polish: was plain var(--border) gray - the doc's "extend the
     gradient-rule treatment... as the standard divider under every card/section title" call,
     applied here as the simpler solid-bronze-line version it explicitly allows for, rather than the
     full multi-stop gradient #app-header::after uses. This title isn't inside a card that already
     gets its own bronze top accent (unlike .sheet-section/.choice-card - see item 1 above), so
     there's no risk of doubling up on bronze right next to itself. */
  border-bottom: 1px solid var(--bronze-dim);
  padding-bottom: 8px;
}
.landing-account-line {
  margin: 0 0 20px;
  color: var(--text-dim);
  font-size: 0.82rem;
}
.landing-account-line a {
  color: var(--accent);
  text-decoration: none;
}
.landing-account-line a:hover { text-decoration: underline; }

/* Signed-out hero: <body> itself is the single 100vh hero, not the header - the background image
   + legibility gradient move from #app-header down to body, so they span the header AND
   #landing-page as one continuous picture instead of two separately-cropped layers. body becomes a
   flex column (header auto-height for the wordmark, #landing-page flex:1 for the sign-in card,
   footer hidden - see each rule below) that exactly fills the viewport, so header + card + footer
   never together exceed one screen. Scoped via :has(#landing-page .landing-hero) - that element
   only exists in landing.js's signed-out render branch, so the signed-in dashboard keeps its
   original scrollable header+main+footer layout untouched. */
body:has(#landing-page .landing-hero) {
  display: flex;
  flex-direction: column;
  height: 100vh;
  /* Belt-and-suspenders against a page-level scrollbar - the flex sizing below is what actually
     keeps content within one viewport; this just guarantees no stray pixel (a border, a rounding
     error) can ever produce one. #landing-page below keeps its own overflow-y: auto as an escape
     hatch so content can never become truly unreachable (e.g. the email/password form toggled open
     on a short mobile viewport) - only the outer page is locked, not the sign-in card itself. */
  overflow: hidden;
  background-image: linear-gradient(90deg, rgba(20, 20, 26, 0.6) 0%, rgba(20, 20, 26, 0.25) 42%, rgba(20, 20, 26, 0) 62%),
    url("../img/mistfall-header-image.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
/* The header no longer paints its own copy of the art (body does, underneath it) and no longer
   needs the 900px min-height that was tuned to reveal the skull/wingtips within a header-sized
   slice - now the same crop math plays out against the FULL viewport height instead, which lands in
   a similar range on typical desktop screens anyway. It shrinks to exactly the wordmark's own
   height + padding, leaving the rest of the viewport to #landing-page below. */
body:has(#landing-page .landing-hero) #app-header.app-header-hero {
  background-image: none;
  min-height: 0;
  flex: 0 0 auto;
  /* The base #app-header rule (up top) draws a 1px border-bottom on every page - here it would cut
     a hard line across the hero art (through the character lineup), so it's hidden too (separate
     from the ::after trim line just below, hidden for the same reason). */
  border-bottom: none;
}
/* The header's usual bottom trim line (#app-header::after, just below) is meant to cap off a
   self-contained header box against whatever plain background follows it - here that "following"
   background is the SAME continuous hero image the header itself sits on, so the line would read as
   a stray seam cutting across the artwork between the wordmark and the sign-in card. Hidden only in
   this state; every other page/state still gets the trim line as normal. */
body:has(#landing-page .landing-hero) #app-header.app-header-hero::after {
  display: none;
}
/* #landing-page is normally a centered 760px column (see its rule above) for the signed-in role/
   campaign UI - that would strand a left-aligned card in the middle of the viewport, nowhere near
   the wordmark's actual left edge (the header's own 32px padding). Here it instead becomes the
   flexible remainder of the 100vh column: flex:1 so it fills whatever space the header didn't use,
   min-height:0 so it's allowed to actually shrink to that space instead of forcing body taller than
   the viewport (flex children default to min-height:auto, sized to their content, which is the
   classic cause of "100vh layout scrolls anyway" bugs), and overflow-y:auto as the safety valve
   described above. */
body:has(#landing-page .landing-hero) #landing-page {
  max-width: none;
  margin: 0;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 24px 32px;
}
/* The footer's attribution line used to have nowhere left to go in a single fixed viewport without
   either crowding the sign-in card or reintroducing the scrollbar this whole change exists to
   remove, so it was dropped entirely for this state. Restored since the footer's attribution
   (©2026 7th Reality Games, LLC and a link to the Privacy Statement) needs to stay visible at the
   bottom of the page - no CSS override needed here at all; body's own flex-column layout already
   handles this safely. #app-footer keeps its normal opaque #app-footer rule above (solid `--bg-panel`
   background + top border, so it reads cleanly over the hero art rather than needing its own
   transparency handling) and simply becomes the flex column's third auto-height item, alongside the
   header above and #landing-page's flex:1/min-height:0/overflow-y:auto in between - the footer takes
   its natural height off the bottom of the fixed 100vh, #landing-page shrinks to fill whatever's
   left, and its own overflow-y:auto stays the escape hatch on short viewports. No page-level
   scrollbar reappears; the visible sign-in card just sits a little higher, not because of new CSS,
   but because nothing here is hiding the footer anymore. */
.landing-hero {
  display: flex;
  justify-content: flex-start;
}
.landing-hero .landing-section-title { text-align: left; }
.landing-signin-card {
  width: 100%;
  max-width: 320px;
}
/* Wider sibling of .landing-signin-card, used by narratorHome.html's and playerHome.html's full
   landing-page hero treatment - that class's 320px
   ceiling was sized for landing.js's sign-in/role buttons, which never hold more than a line or two
   of text. Both role-home pages' cards hold an account line, campaign controls, and (narratorHome
   only) a campaign-switch row with a <select>, a member list, and an invite panel - all of which
   would wrap awkwardly or need horizontal scrolling at 320px. Named role-neutrally (not
   ".landing-narrator-card", its original name when only narratorHome.html used it) since
   playerHome.html now shares it too. Still `width:100%` (inherited from .landing-signin-card above,
   both classes are applied together) so it keeps shrinking to fit on narrow viewports; this just
   raises how wide it's allowed to get on anything larger. Declared after .landing-signin-card so its
   max-width wins on source order for the equal-specificity property they share. */
.landing-role-home-card {
  max-width: 560px;
}
/* A player's per-campaign character summary row ("Your Characters" section, showing all of a
   player's characters, the campaigns they're a part of, and the narrator for those campaigns) - a
   lightweight list, not form controls, so it borrows the same
   bordered-stack look .landing-campaign-note uses rather than .landing-campaign-row's flex/gap
   sizing (which is tuned for inputs+buttons sitting on one line). */
.landing-character-row {
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.landing-character-row:first-child {
  padding-top: 0;
  border-top: none;
}
.landing-character-row .landing-character-name {
  font-weight: 600;
  color: var(--text);
}
.landing-character-row .landing-character-meta {
  margin: 2px 0 0;
  color: var(--text-dim);
  font-size: 0.82rem;
}
/* 2026-09-09: a live character's name+meta are now wrapped in a real <a> to sheet.html (see
   profile.js's own renderRow() comment) - reset the browser's default link
   styling (no blue text/underline) so it still reads as plain text at rest, and add a hover/focus
   affordance so it's discoverable as clickable without looking like every other link on the page. */
.landing-character-link {
  display: block;
  margin: -4px -6px;
  padding: 4px 6px;
  border-radius: 6px;
  text-decoration: none;
  color: inherit;
}
.landing-character-link:hover,
.landing-character-link:focus-visible {
  background: var(--bg-panel-raised);
}
.landing-character-link:hover .landing-character-name {
  color: var(--accent);
}
/* Below 700px the header drops the hero art in favor of a flat gradient (see the max-width:700px
   block up in .app-header-hero/#app-wordmark) - mirror that at the body level now that body is what
   actually paints the art, and re-center the wordmark/card pairing same as before. */
@media (max-width: 700px) {
  body:has(#landing-page .landing-hero) {
    background-image: linear-gradient(180deg, #1e1e28, #14141a);
  }
  .landing-hero { justify-content: center; }
  .landing-hero .landing-section-title { text-align: center; }
}
/* Sign in with Google / Sign in with Email - two full-width stacked options (replacing the old
   "Google button + small text link to reveal email/password" pattern) so both read as equally
   valid top-level choices rather than one being a buried alternative. Google keeps the app's
   normal filled-button look (the one-click, already-verified default); Email is the same size but
   outlined/secondary, since clicking it only reveals the actual sign-in form below rather than
   completing sign-in itself. */
.landing-signin-btn {
  display: block;
  width: 100%;
  margin: 0 0 10px;
  text-align: center;
}
.landing-signin-btn.secondary {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
}
.landing-signin-btn.secondary:hover:not(:disabled) { border-color: var(--silver-dim); filter: none; }
/* Public Launch Readiness v1, item 1: age/ToS gate above the sign-in options. */
.landing-age-confirm {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0 0 14px;
  font-size: 0.82rem;
  line-height: 1.4;
  color: var(--text-dim);
  cursor: pointer;
}
.landing-age-confirm input[type="checkbox"] {
  flex-shrink: 0;
  margin-top: 2px;
  width: 16px;
  height: 16px;
}
.landing-age-confirm a { color: var(--bronze); }

/* The Google button's "G" mark + label - see landing.js's GOOGLE_G_ICON_SVG
   comment for why it's inline SVG instead of an <img>. inline-flex (not the button's own block
   layout) so the icon and "Sign in with" text sit side by side and stay centered together within
   the button's existing text-align: center, instead of each trying to center on its own line. */
.landing-google-btn-inner {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.landing-google-icon {
  display: inline-flex;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}
.landing-google-icon svg { display: block; width: 100%; height: 100%; }
/* Email/password form, inside the "Sign in with Email" toggle. The email and
   password inputs and the submit button are plain, unstyled elements - unlike the two buttons above
   (.landing-signin-btn), which are explicitly block+full-width. Bare <input>/<button> render
   inline-block by default, so with no width/display rules of their own they wrapped like words in a
   sentence: depending on how long the button's label happened to be ("Sign in" vs the longer
   "Create account"), it might land flush against the password field on the same line, or wrap to
   its own line with no gap above it - explaining why the two modes looked different even though
   they're the exact same markup. This forces every field and the button onto its own full-width
   line with consistent spacing, regardless of which mode is showing. */
.landing-email-form {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.landing-email-form input[type="email"],
.landing-email-form input[type="password"] {
  display: block;
  width: 100%;
  margin: 0 0 10px;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 4px;
  padding: 8px 10px;
  font-family: var(--font);
  font-size: 0.88rem;
}
.landing-email-form button {
  display: block;
  width: 100%;
  margin: 0 0 10px;
}
#landing-campaign { margin-top: 32px; }
/* "Your Characters" section (playerHome.html only) - this div had no margin-top at
   all before, so it sat flush against whatever the Campaign section above it ended with. Same 32px as #landing-campaign's own
   margin-top above, for a consistent rhythm between all three stacked sections (account line ->
   Campaign -> Your Characters). */
#landing-characters { margin-top: 32px; }
/* Caps the character list's own height and scrolls internally past that rather than letting a
   player in many campaigns push
   the whole card - and, since #landing-page itself already scrolls under the hero-card layout (see
   the `body:has(#landing-page .landing-hero) #landing-page` rule above), the WHOLE PAGE - taller.
   The themed scrollbar (::-webkit-scrollbar* at the top of this file) applies automatically, no
   extra styling needed here. A second class alongside .landing-campaign-box (not merged into it) so
   this doesn't also cap the Campaign section's own box just above it, which shouldn't scroll
   internally the same way. */
.landing-character-list {
  max-height: 320px;
  overflow-y: auto;
}
.landing-campaign-box {
  background-color: var(--bg-panel);
  /* Visual polish: --texture-grain, see :root's comment. */
  background-image: var(--texture-grain);
  border: 1px solid var(--border);
  /* Visual polish: same "card has a spine" top accent + resting shadow as
     .sheet-section/.choice-card - narratorHome.html/playerHome.html's campaign list box is this
     stylesheet's other recurring content card, alongside .choice-card. */
  border-top: 3px solid var(--bronze-dim);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding: 16px 18px;
}
.landing-campaign-subtitle {
  color: var(--text-dim);
  text-transform: uppercase;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  margin-bottom: 8px;
}
/* Brand-new-account welcome state (Nav Reorg Phase 2, profile.js's renderFirstTimeWelcome()) - two
   big, equal-weight prompts (Create a Character / Create a New Campaign) rather than the normal
   returning-user layout's Continue hero + two list sections, since neither has anything to show yet.
   Same auto-fill card-grid pattern as .pregen-grid (sheet.css)/the Bestiary's own card grid -
   2-up on a wide viewport, stacks to 1 column once a card would go below ~260px. Cards reuse
   .landing-campaign-box for the same spine-accent look as every other content card on this page. */
.landing-welcome-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 16px;
  margin: 18px 0;
}
.landing-welcome-card {
  display: flex;
  flex-direction: column;
  text-align: center;
}
.landing-welcome-card h3 {
  margin: 0 0 8px;
  color: var(--silver);
  font-family: var(--font-display);
  font-size: 1.05rem;
}
.landing-welcome-card p {
  flex: 1;
}
.landing-welcome-card .landing-signin-btn { margin-top: auto; }
.landing-campaign-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
.landing-campaign-row select { flex: 1; min-width: 200px; }
/* Delete Campaign button - style.css has no danger-button variant yet (the
   sheet-page tools' .gr-remove-btn lives in sheet.css, which the landing page doesn't load), so a
   small one is defined here using the existing --danger variable rather than pulling in sheet.css. */
.landing-danger-btn {
  border-color: var(--danger);
  color: var(--danger);
}
.landing-danger-btn:hover:not(:disabled) { filter: none; border-color: var(--danger); background: var(--danger); color: #fff; }
.landing-campaign-empty {
  color: var(--text-dim);
  font-size: 0.86rem;
  font-style: italic;
  margin: 0 0 4px;
}
.landing-campaign-form {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
.landing-campaign-form input { flex: 1; min-width: 160px; }
.landing-campaign-note {
  margin: 16px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  color: var(--text-dim);
  font-size: 0.78rem;
  line-height: 1.5;
}
/* profile.html - the badges, Continue hero, and campaign sections stack on one page, so each needs
   the same 32px rhythm between themselves and the bio/badges header above. #landing-continue-hero/
   #landing-campaigns are the Nav Reorg Phase 1 IDs (renamed from #landing-narrator-campaigns/
   #landing-player-campaigns - see docs/profile-navigation.md) - this rule's selector list was never
   updated at the time, so it silently stopped matching anything and the Continue hero/Your Campaigns
   sections lost their top margin entirely (visible as the Continue button crowding the badge grid
   right above it - jsdom has no layout engine, so nothing in this project's test suite could have
   caught a pure spacing regression like this one). */
#profile-header { margin-top: 20px; }
#profile-badges,
#landing-continue-hero,
#landing-campaigns { margin-top: 32px; }
/* Avatar + display name + bio, side by side on anything wide enough, stacked (avatar centered above
   the text) once it isn't - mirrors how .landing-campaign-row's inputs wrap on narrow viewports. */
.profile-header {
  display: flex;
  gap: 16px;
  align-items: flex-start;
}
@media (max-width: 480px) {
  .profile-header { flex-direction: column; align-items: center; text-align: center; }
}
.profile-avatar-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex-shrink: 0;
}
.profile-avatar,
.profile-avatar-placeholder {
  width: 72px;
  height: 72px;
  border-radius: 50%;
  border: 2px solid var(--bronze-dim);
  object-fit: cover;
}
/* Letter-avatar fallback for an account with no Google photo and no uploaded image yet - same
   circle size/border as a real <img> avatar (.profile-avatar above) so nothing shifts once one is
   added, just centered text instead of a picture. */
.profile-avatar-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-panel-raised);
  color: var(--silver-dim);
  font-size: 1.6rem;
  font-weight: 600;
}
.profile-info-col { flex: 1; min-width: 0; }
.profile-bio-input {
  display: block;
  width: 100%;
  margin-top: 6px;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 4px;
  padding: 8px 10px;
  font-family: var(--font);
  font-size: 0.86rem;
  resize: vertical;
}
/* Badge grid - earned badges read at full color/opacity, locked ones gray out (grayscale filter +
   reduced opacity, same "not yet available" treatment used elsewhere in the app) rather than being
   hidden outright, so a player can see what's still ahead of them. */
.profile-badge-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 12px;
}
.profile-badge {
  background-color: var(--bg-panel);
  background-image: var(--texture-grain);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 10px;
  text-align: center;
}
.profile-badge-locked {
  filter: grayscale(1);
  opacity: 0.55;
}
.profile-badge-icon {
  font-size: 1.6rem;
  line-height: 1;
  margin-bottom: 6px;
}
.profile-badge-name {
  font-weight: 600;
  font-size: 0.82rem;
  color: var(--text);
}
.profile-badge-desc {
  margin-top: 2px;
  color: var(--text-dim);
  font-size: 0.74rem;
  line-height: 1.4;
}
.profile-badge-progress {
  margin-top: 4px;
  color: var(--silver-dim);
  font-size: 0.72rem;
  font-weight: 600;
}

/* Trust & Safety (Public Launch Readiness v1, item 2) - Report/Block button pair next to a
   member's name (campaignInfo.js) or on a public profile view (profile.js). Kept in sync by hand
   across style.css/sheet.css like .ghost above. */
.trust-safety-actions { display: inline-flex; gap: 6px; margin-left: 8px; vertical-align: middle; }

/* Builder "Start Over" in-page confirm (main.js). */
.builder-reset-confirm { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 10px 28px 0; padding: 10px 14px; border: 1px solid var(--bronze); border-radius: var(--radius); }
.builder-reset-confirm[hidden] { display: none; }
.builder-reset-confirm span { flex: 1 1 240px; }

/* Bring a character into a campaign (bringCharacter.js, docs/bring-character.md). */
.bring-section > summary { cursor: pointer; font-family: var(--font-display); color: var(--silver); padding: 10px 14px; }
.bring-list { display: flex; flex-direction: column; gap: 6px; }
.bring-row { display: grid; grid-template-columns: auto 1fr; column-gap: 10px; align-items: center; padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; }
.bring-row input { grid-row: span 3; }
.bring-name { font-weight: 700; }
.bring-meta, .bring-where { font-size: 0.78rem; color: var(--text-dim); }
.bring-row:has(input:checked) { border-color: var(--accent); }
.bring-confirm { margin-top: 10px; padding: 10px 14px; border: 1px solid var(--bronze); border-radius: 6px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.bring-confirm[hidden] { display: none; }
.bring-confirm p { flex: 1 1 100%; margin: 0; }
.bring-warn { color: var(--bronze); font-size: 0.85rem; }
.bring-status { font-size: 0.8rem; color: var(--bronze); margin: 6px 0 0; }
.lineage-line { font-size: 0.75rem; color: var(--text-dim); margin: 4px 0 0; }
