/* Text RPG — Parchment & Ink aesthetic */

@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=JetBrains+Mono:wght@400;500;600&family=Cinzel:wght@400;500;600;700&display=swap');

:root {
  --paper: #f1e8d4;
  --paper-2: #e8dcc0;
  --paper-deep: #d9c9a3;
  --ink: #1c1410;
  --ink-2: #3a2a1f;
  --ink-faded: #6b5238;
  --rule: #8a6a4a;
  --accent: #7a1e1e;        /* burgundy wax seal */
  --accent-2: #3b5a3b;      /* aged green */
  --gold: #a07820;
  /* P14 (D-06): five trigger-badge colors — parchment palette. Used by
     .trigger-badge[data-trigger-type="..."] in chip render. Each value
     verified ≥4.5:1 AA contrast vs --paper-2 #e8dcc0 (some adjusted
     darker from CONTEXT-locked draft hexes to meet the AA threshold —
     see 14-02-SUMMARY.md). */
  --trigger-item:         #8e4f24;  /* copper (adjusted from #b06a3a → 4.68:1) */
  --trigger-skill:        #3d4f95;  /* indigo (adjusted from #4a5fad → 5.61:1) */
  --trigger-knowledge:    #246060;  /* teal   (adjusted from #2f7c7c → 5.29:1) */
  --trigger-stat:         #6e3a6e;  /* plum   (locked, 6.19:1) */
  --trigger-relationship: #705218;  /* ochre  (adjusted from #b58a3a → 5.30:1) */
  /* P18 (D-01): xp-preview color — sage green, distinct from existing 5 triggers.
     Verified ≥4.5:1 AA contrast vs --paper-2 #e8dcc0. */
  --trigger-xp:           #3a6b3a;  /* sage green (5.45:1) */
  /* P21 (UI-SPEC): four overflow-attribution color tokens.
     Today only --overflow-state is wired; gear/feat tokens forward-compat for
     OVERFLOW-V15-01. All AA-clear (≥4.5:1) on --paper #f1e8d4. */
  --overflow-base:  #654321;  /* deep ink (9.21:1) — base skill */
  --overflow-gear:  #8B4513;  /* saddle brown (6.04:1) — gear bonus */
  --overflow-feat:  #4B6043;  /* moss green (5.83:1) — feat bonus */
  --overflow-state: #B8860B;  /* dark goldenrod (4.62:1) — temporary states */
  --shadow: 0 1px 0 rgba(28,20,16,0.05), 0 10px 30px -14px rgba(28,20,16,0.35);
  /* P34 (THEME-02): --danger moved up from the stray single-line `:root` rule
     that used to sit near the end of this file. Hex byte-identical. */
  --danger: #a83232;

  /* ═══ P34 (THEME-02) — the rest of the 71-token contract ═══════════════════
     Enumerated in test/v17-theme-contract.test.js::TOKEN_CONTRACT. Two classes
     of value live below, and they follow different rules:

       CONSOLIDATION — the value is COPIED BYTE-FOR-BYTE out of the declaration
         the token will later replace (plan 34-05 rewrites those base rules).
         Never designed, never rounded, never harmonised. Each group names its
         source. If a value here differs by one character from what styles.css
         renders today, parchment or observatory has visibly changed and the
         consolidation has failed.
       GREENFIELD — a new name that no shipped rule consumes yet. The parchment
         and observatory values are chosen to be no-ops, or to reproduce an
         existing surface exactly, so they cannot move a pixel either.

     Plan 34-03 is purely additive: nothing reads these tokens yet, so the two
     shipped palettes must render identically to the pre-change baseline at
     proxy/test-runner/mobile-shots/palettes-baseline/. Plan 34-05 wires the
     base rules; plan 34-07 adds the dark-glass and arcane blocks. ══════════ */

  /* P34 (THEME-02): ground and texture — CONSOLIDATION.
     --ground is the paintable page background; it is applied to `html` only
     (see the html/body split below). --paper MUST stay a flat colour: the 13
     color-mix() sites that blend against var(--paper) silently void if it ever
     becomes a gradient. --vignette copies the base `body::after` background; --grain-*
     copy the base `body::before` rule; --scrim copies the base `.modal-bg`.
     ⚠ --grain-image and --grain-opacity are a MATCHED PAIR. Each SVG bakes a
     different alpha into its own feColorMatrix, so pairing one palette's image
     with another palette's opacity produces a grain that is invisible or
     overpowering. Change them together or not at all. */
  --ground: var(--paper);
  --vignette: radial-gradient(ellipse at center, transparent 50%, rgba(40,25,15,0.25) 100%);
  --grain-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='400' height='400'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.35  0 0 0 0 0.25  0 0 0 0 0.15  0 0 0 0.08 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  --grain-opacity: 0.35;
  --scrim: rgba(28,20,16,0.55);

  /* P34 (THEME-02): surface, border and backdrop tiers — GREENFIELD.
     The glass palettes need a named surface ramp; parchment reuses hexes that
     already exist in this block (--surface == --paper-2) so nothing new is
     invented. --panel-backdrop is `none` here, which makes
     `backdrop-filter: var(--panel-backdrop)` a no-op on the legacy palettes. */
  --surface: #e8dcc0;
  --surface-2: #ede3cc;
  --surface-raised: #f7f0dd;
  --border-soft: rgba(28,20,16,.18);
  --border-strong: rgba(28,20,16,.42);
  --panel-backdrop: none;

  /* P34 (THEME-02): elevation and glow — GREENFIELD.
     New names; the existing composite --shadow above is untouched and stays in
     the contract for backward compatibility. */
  --shadow-sm: 0 1px 0 rgba(28,20,16,.06);
  --shadow-md: 0 2px 16px rgba(28,20,16,.18);
  --shadow-lg: 0 10px 30px -14px rgba(28,20,16,.35);
  --glow-gilt: 0 0 10px rgba(160,120,32,.28);

  /* P34 (THEME-02): downstream tints — GREENFIELD, consumed by Phase 38 for
     risk-edge tinting. Nothing in Phase 34 reads them. They are deliberately
     NOT the consolidation vehicle: the --wash- and --tint- family below carries
     the byte-exact values instead. Parchment has no directional wash today, so
     `transparent` here is the correct no-op. */
  --tint-wash: transparent;
  --tint-wash-strong: transparent;
  --tint-sunken: rgba(28,20,16,.06);

  /* P34 (THEME-02): verdict colours — CONSOLIDATION.
     These five hexes are hardcoded and palette-BLIND today (styles.css:789-793,
     822-827, 949, 958), so both legacy palettes take them verbatim. Tokenizing
     them is a refactor, not a restyle. Plan 34-05 folds the literals at the
     .roll-card.*, .roll-card-tag.*, .mod-chip.negative and .roll.fail sites
     into these names; plans 34-07 lifts them for the dark palettes. */
  --verdict-success: #4a7c4f;
  --verdict-partial: #b58a3a;
  --verdict-fail: #8a3030;
  --verdict-crit: var(--gold);
  --verdict-crit-fail: #5a1a1a;
  /* REVIEW WR-01 — the crit-success CARD FILL, split off --verdict-crit.
     The pre-P34 rule mixed a LITERAL #c9a341 into the fill while its border
     took var(--gold), so the two were never the same colour in either shipped
     palette (parchment --gold is #a07820, observatory's is #e6c97a). Routing
     the fill through --verdict-crit silently changed both — a pixel move in
     palettes CONTEXT locks as "must be unaffected". This token restores the
     literal for the legacy palettes; the dark palettes point it back at their
     own --verdict-crit, which is what they want. */
  --verdict-crit-fill: #c9a341;

  /* P34 (THEME-02): consolidation-derived — every value below is copied
     BYTE-FOR-BYTE from the base declaration named in its trailing comment. The
     matching per-selector observatory fork supplies the observatory column.
     Plan 34-05 replaces both with a single tokenised base rule and deletes the
     fork.
     The wash tokens carry the FULL gradient rather than just a colour on
     purpose: .player-action stops at `transparent 50%` in parchment and 40% in
     observatory, and a stop percentage is a length that may not live in a
     palette block as a standalone token. Keeping the whole gradient inside a
     gradient-typed token (allow-listed by NAME in GRADIENT_TOKENS) is what
     makes zero residual forks reachable instead of the two research allowed. */
  --panel-bg: var(--paper);                                                        /* .modal:1206 + .tweaks-panel:1225 */
  --pane-bg: linear-gradient(180deg, transparent, rgba(0,0,0,0.02));               /* .side-pane:1273 */
  --hover-wash: rgba(28,20,16,0.08);                                               /* .btn-ghost:hover:191 */
  --wash-action: linear-gradient(90deg, rgba(0,0,0,0.04), transparent 50%);        /* .player-action:479 */
  --wash-suggest: linear-gradient(90deg, rgba(0,0,0,0.02), transparent 65%);       /* .suggested-action:568 */
  --wash-suggest-hover: linear-gradient(90deg, rgba(0,0,0,0.05), transparent 60%); /* .suggested-action:hover:628 */
  --wash-row: linear-gradient(90deg, rgba(0,0,0,0.025), transparent 70%);          /* .item-row/.lore-entry:1691 + .ledger-card:1885 */
  --tint-flat-row: rgba(0,0,0,0.025);                                              /* .side-pane .stat-row:1742 */
  --tint-flat-dc: rgba(0,0,0,0.02);                                                /* .suggested-action-dc:612 */
  --tint-raised: rgba(255,255,255,0.4);                                            /* .roll-card-tag:820 */
  --tint-raised-soft: linear-gradient(180deg, rgba(255,255,255,0.3), transparent 60%); /* .d20-face:849 */
  --tint-raised-chip: rgba(255,255,255,0.3);                                       /* .mod-chip:942 */
  --kbd-face: rgba(0,0,0,0.03);                                                    /* kbd:529 gradient stop 1 */
  --kbd-face-2: rgba(0,0,0,0.08);                                                  /* kbd:529 gradient stop 2 */
  --kbd-shadow: 0 1px 0 rgba(0,0,0,0.12);                                          /* kbd:534 */
  --kbd-ink: var(--ink-2);                                                         /* kbd:533 — the fork overrides `color`, which the UI-SPEC map omitted */
  --roll-bg: var(--paper);                                                         /* .roll:739 */
  --shadow-card: 0 2px 12px rgba(0,0,0,0.05);                                      /* .roll-card:782 */
  --shadow-hover: 0 2px 10px rgba(0,0,0,0.08);                                     /* .suggested-action:hover:630 */

  /* P34 (THEME-02): tokens that were REFERENCED but never DEFINED anywhere.
     All six are consumed today through `var(--x, fallback)` — except --bg at
     styles.css:2692, which has no fallback at all, so
     `.banner.storage-banner { background: var(--bg) }` is invalid at
     computed-value time and the DATA-04 storage-failure banner renders fully
     transparent. Defining --bg is what makes that banner visible again.
     For parchment, --bg-card and --accent-rgb reproduce today's inline fallback
     values byte-for-byte, so nothing changes THERE.

     REVIEW WR-10 — "nothing changes here" was too broad, and the omission would
     have mis-triaged a legitimate baseline diff. --ink-muted and --ink-shadow
     were previously UNDECLARED, so the call-site fallbacks always won. The same
     review round then landed REVIEW WR-02, whose call-site fix gave
     .banner.claude-banner a visible background for the first time — a third
     genuine parchment pixel move, and the first this token has ever produced.
     REVIEW WR-03 (iteration 2) folds it into the count, because the COUNT is
     what a reader triaging a baseline diff trusts.

     So parchment has THREE expected pixel differences in this plan, not zero:
       --ink-muted   rgba(120,120,120,0.85) -> var(--ink-faded) #6b5238   (.thinking-preview-label + -text colour)
       --ink-shadow  rgba(120,120,120,0.35) -> var(--rule) #8a6a4a        (.thinking-preview border-left)
       .banner.claude-banner  transparent -> rgba(122,30,30,0.06)         (REVIEW WR-02 call-site fix)
     (The iteration-1 note had those two hexes transposed — parchment's
     --ink-faded is #6b5238 and its --rule is #8a6a4a, not the other way round.
     Browser-checked: .thinking-preview border-left-color computes to
     rgb(138,106,74) = #8a6a4a.)

     All three are intentional. For the first two, the greys were never a palette
     choice — they were the fallback of a name nobody had defined. For the third,
     --accent-rgb's only consumer had an invalid rgba() call site and rendered NO
     background in ANY palette until that was fixed. */
  --ink-accent: var(--gold);        /* play.jsx:1672,1685 inline ledger borders (was hardcoded #d4a017) */
  --ink-muted: var(--ink-faded);    /* .thinking-preview label + text */
  --ink-shadow: var(--rule);        /* .thinking-preview border-left */
  --bg: var(--paper);               /* .banner.storage-banner background (NO fallback at the call site) */
  --bg-card: rgba(0,0,0,0.04);      /* .login-badge:2363 — equals today's fallback */
  --accent-rgb: 122 30 30;          /* channels of --accent #7a1e1e; .banner.claude-banner:2379 */

  /* v1.7 P39 (CHROME-01): THE CTA TRIAD — the one primary/selected treatment
     shared by .btn-primary and .chip.active on every screen.
     WHY A TOKEN AND NOT A color-mix(): the label needs to branch light-vs-dark
     and no single mix does that. Parchment keeps its ink-on-paper CTA (correct
     on a light ground) and gains only the gilt EDGE; the three dark palettes
     take a gold-tinted glass fill with a gold label. That branch is exactly
     what a palette token is for — see the .play-shell gilt note at :2809 for
     the converse case (a value one mix DOES cover, so it stays untokenised).
     --cta-ink must resolve to a HEX or a var() chain ending in one:
     test/v17-theme-contrast.test.js::resolveToken cannot read color-mix(), and
     "every gated lit-text token resolves to a colour" would fail on it. Only
     --cta-fill / --cta-edge may carry a mix — neither is a `color:` value, so
     neither is scanned. */
  --cta-fill: var(--ink);           /* unchanged from the pre-P39 .btn-primary fill */
  --cta-ink: var(--paper);          /* unchanged — knockout label on the ink fill */
  --cta-edge: var(--gold);          /* the ONLY parchment change: gilt edge, was --ink */
}

/* Observatory palette override */
:root[data-palette="observatory"] {
  --paper: #0b1020;
  --paper-2: #111836;
  --paper-deep: #1a2347;
  --ink: #e7e3d6;
  --ink-2: #cfc8b4;
  --ink-faded: #8f8a7a;
  --rule: #39406a;
  --accent: #d9b45a;
  --accent-2: #7aa2c8;
  --gold: #e6c97a;
  /* P14 (D-06): five trigger-badge colors — observatory palette. Lifted
     from parchment-base hues so they read on --paper-2 #111836 (deep-night
     surface). All five verified ≥4.5:1 AA contrast vs --paper-2. */
  --trigger-item:         #e89968;  /* lifted copper (7.61:1) */
  --trigger-skill:        #8b9ce0;  /* lifted indigo (6.57:1) */
  --trigger-knowledge:    #6bbcbc;  /* lifted teal   (7.90:1) */
  --trigger-stat:         #b07ab0;  /* lifted plum   (5.18:1) */
  --trigger-relationship: #e8c878;  /* lifted ochre  (10.73:1) */
  /* P18 (D-01): xp-preview color — lifted sage on deep-night surface. */
  --trigger-xp:           #7fc97f;  /* lifted sage (6.42:1 vs --paper-2 #111836) */
  /* P21 (UI-SPEC): observatory overflow tokens — lifted to read on deep-night.
     All AA-clear (≥4.5:1) on --paper #0b1020. */
  --overflow-base:  #D4D4D4;  /* near-white (11.42:1) */
  --overflow-gear:  #C9A66B;  /* warm gold (7.31:1) */
  --overflow-feat:  #88C9A1;  /* soft teal (7.84:1) */
  --overflow-state: #F4D35E;  /* lemon (11.18:1) */
  /* P34 (THEME-02): --shadow was never declared here, so observatory has always
     inherited the parchment composite from the bare `:root` block above. The
     contract requires the name in every palette, so it is restated here at that
     exact inherited value — a no-op by construction. */
  --shadow: 0 1px 0 rgba(28,20,16,0.05), 0 10px 30px -14px rgba(28,20,16,0.35);
  /* P34 (THEME-02): --danger moved up from the stray single-line observatory
     `:root` rule that used to sit near the end of this file. Hex unchanged. */
  --danger: #f08080;

  /* ═══ P34 (THEME-02) — the rest of the 71-token contract ═══════════════════
     Same groups, same order as the parchment block above, so a side-by-side
     diff of the two blocks lines up. See that block's header for the
     CONSOLIDATION vs GREENFIELD rule. Every consolidation value below is copied
     byte-for-byte out of the matching per-selector observatory fork, whose line
     number is named in the trailing comment. ═══════════════════════════════ */

  /* P34 (THEME-02): ground and texture — CONSOLIDATION.
     --vignette from the observatory `body::after` fork; --grain-* from the
     observatory `body::before` fork; --scrim from the observatory `.modal-bg`
     fork. ⚠ --grain-image and --grain-opacity are a MATCHED PAIR: this SVG
     bakes 0.05 alpha into its feColorMatrix against parchment's 0.08, so the
     0.5 opacity is only correct paired with THIS image. Never cross them. */
  --ground: var(--paper);
  --vignette: radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.55) 100%);
  --grain-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='400' height='400'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.4' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.85  0 0 0 0 0.80  0 0 0 0 0.65  0 0 0 0.05 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  --grain-opacity: 0.5;
  --scrim: rgba(0,0,0,0.75);

  /* P34 (THEME-02): surface, border and backdrop tiers — GREENFIELD.
     --surface / --surface-2 / --surface-raised reuse hexes that already exist
     in this palette (--surface == --paper-2, --surface-raised == --paper-deep),
     so no new colour is introduced. --panel-backdrop stays `none`. */
  --surface: #111836;
  --surface-2: #0f1530;
  --surface-raised: #1a2347;
  --border-soft: rgba(231,227,214,.16);
  --border-strong: rgba(231,227,214,.38);
  --panel-backdrop: none;

  /* P34 (THEME-02): elevation and glow — GREENFIELD. */
  --shadow-sm: 0 1px 0 rgba(0,0,0,.35);
  --shadow-md: 0 2px 16px rgba(0,0,0,.35);
  --shadow-lg: 0 16px 40px rgba(0,0,0,.55);
  --glow-gilt: 0 0 12px rgba(230,201,122,.35);

  /* P34 (THEME-02): downstream tints — GREENFIELD, Phase 38 consumes them. */
  --tint-wash: rgba(255,255,255,.03);
  --tint-wash-strong: rgba(255,255,255,.06);
  --tint-sunken: rgba(0,0,0,.28);

  /* P34 (THEME-02): verdict colours — CONSOLIDATION. Identical to parchment
     because the five hexes are palette-BLIND at their call sites today.
     Tokenizing is a refactor, not a restyle. */
  --verdict-success: #4a7c4f;
  --verdict-partial: #b58a3a;
  --verdict-fail: #8a3030;
  --verdict-crit: var(--gold);
  --verdict-crit-fail: #5a1a1a;
  /* REVIEW WR-01 — same literal as parchment; see that block's note. The
     .roll-card.crit-success fill was #c9a341-based before P34 in BOTH shipped
     palettes and stays so. */
  --verdict-crit-fill: #c9a341;

  /* P34 (THEME-02): consolidation-derived — every value copied BYTE-FOR-BYTE
     from the observatory fork named in its trailing comment. */
  --panel-bg: var(--paper);                                                          /* .modal + .tweaks-panel are var(--paper) in both palettes; the token exists so the glass palettes can override it */
  --pane-bg: linear-gradient(180deg, transparent, rgba(255,255,255,0.02));           /* fork:1283 */
  --hover-wash: rgba(231,227,214,0.08);                                              /* fork:197 */
  --wash-action: linear-gradient(90deg, rgba(255,255,255,0.03), transparent 40%);    /* fork:482 — note the 40% stop vs parchment's 50% */
  --wash-suggest: linear-gradient(90deg, rgba(255,255,255,0.03), transparent 65%);   /* fork:622 */
  --wash-suggest-hover: linear-gradient(90deg, rgba(255,255,255,0.06), transparent 60%); /* fork:633 */
  --wash-row: linear-gradient(90deg, rgba(255,255,255,0.03), transparent 70%);       /* forks:1697 and 1900 — identical, one token serves both */
  --tint-flat-row: rgba(255,255,255,0.04);                                           /* fork:1750 */
  --tint-flat-dc: rgba(255,255,255,0.04);                                            /* fork:615 */
  --tint-raised: rgba(0,0,0,0.3);                                                    /* fork:823 */
  --tint-raised-soft: linear-gradient(180deg, rgba(0,0,0,0.3), transparent 60%);     /* fork:853 */
  --tint-raised-chip: rgba(0,0,0,0.25);                                              /* fork:946 */
  --kbd-face: rgba(255,255,255,0.04);                                                /* fork:538 gradient stop 1 */
  --kbd-face-2: rgba(255,255,255,0.1);                                               /* fork:538 gradient stop 2 */
  --kbd-shadow: 0 1px 0 rgba(0,0,0,0.35);                                            /* fork:540 */
  --kbd-ink: var(--ink);                                                             /* fork:539 — the `color` override the UI-SPEC map omitted */
  --roll-bg: var(--paper-deep);                                                      /* fork:745 */
  --shadow-card: 0 2px 16px rgba(0,0,0,0.35);                                        /* fork:787 */
  --shadow-hover: 0 2px 14px rgba(0,0,0,0.25);                                       /* fork:634 */

  /* P34 (THEME-02): tokens that were REFERENCED but never DEFINED.
     Unlike parchment, two of these are a real (and intended) FIX for
     observatory, because the call-site fallbacks were palette-blind:
       --bg-card   BEFORE rgba(0,0,0,0.04) (4% black on a deep-night ground,
                   effectively invisible)  AFTER rgba(255,255,255,0.06).
                   Affects .login-badge:2363.
       --accent-rgb BEFORE 180 96 52 (fixed warm copper)  AFTER 217 180 90, the
                   channels of observatory's own --accent #d9b45a. Affects the
                   .banner.claude-banner tint at 2379.
       --ink-muted  BEFORE rgba(120,120,120,0.85) (the call-site fallback, which
                   always won because the name was undeclared)  AFTER
                   var(--ink-faded) #8f8a7a. Affects .thinking-preview's text.
       --ink-shadow BEFORE rgba(120,120,120,0.35)  AFTER var(--rule) #39406a.
                   Affects .thinking-preview's border-left.
     REVIEW WR-10 — the last two were missing from this list, which previously
     called --bg-card and --accent-rgb "the ONLY expected observatory pixel
     differences". They are FOUR, and only when those elements are on screen.
     --bg additionally un-breaks the storage banner.
     REVIEW WR-02 — the --accent-rgb entry could not have been "confirmed
     against the Task 3 baseline diff": its only consumer, .banner.claude-banner,
     had a syntactically invalid rgba() call site and rendered no background in
     any palette. That is fixed at the call site now, so this entry becomes a
     real (and still expected) observatory difference for the first time.
     REVIEW WR-01 — the .roll-card.crit-success FILL is deliberately NOT on this
     list: --verdict-crit-fill keeps the pre-P34 literal for both legacy
     palettes. */
  --ink-accent: var(--gold);
  --ink-muted: var(--ink-faded);
  --ink-shadow: var(--rule);
  --bg: var(--paper);
  --bg-card: rgba(255,255,255,0.06);
  --accent-rgb: 217 180 90;

  /* v1.7 P39 (CHROME-01): CTA triad — see the parchment block for the contract.
     Observatory is a DARK palette, so it takes the gilt treatment rather than
     parchment's ink fill: the pre-P39 `background: var(--ink)` painted a cream
     #e7e3d6 slab on deep-night, which is the same foreign white-block this phase
     removes from dark-glass. Label is --gold #e6c97a, 9.4:1 on the fill. */
  --cta-fill: color-mix(in srgb, var(--gold) 14%, var(--paper-deep));
  --cta-ink: var(--gold);
  --cta-edge: var(--gold);
}

/* ═══════════════════════════════════════════════════════════════════════════
   P34 (THEME-02): dark-glass palette — ported from
   .claude/skills/sketch-findings-TRPG/sources/themes/dark-glass.css
   ("deep charcoal-blue ground with cool light blooms, translucent frosted
   panels, warm gold accent"). This is the app's DEFAULT palette (34-04).

   Same 71 tokens, same declaration order, as the parchment block above, so a
   side-by-side diff of any two palette blocks lines up.

   AA discipline: every colour token below records its measured WCAG ratio
   against this palette's NORMATIVE background — --surface-raised
   rgba(255,255,255,.10) flattened over the flat --paper #0d1118 = #25292f,
   the brightest opaque panel tier a badge can sit on. Light-on-dark inverts
   the usual worst case: a light foreground GAINS contrast as its ground
   darkens, so the lightest panel is the true floor, not the darkest ground.
   Ratios recomputed this session; test/v17-theme-contrast.test.js re-derives
   every one of them from these declarations rather than from a fixture.
   REVIEW CR-02: --surface-raised is the brightest DECLARED tier, but it has no
   consumer yet — the panels that exist resolve --panel-bg. The whole TEXT
   family is therefore ALSO gated against the LIT composite (--panel-bg over
   this palette's brightest bloom stop = #29394e), which is the
   .side-pane-under-the-bloom case. Ratios below record both grounds.
   REVIEW CR-02 (iteration 2): the first pass gated only the ink ramp here, and
   this note then claimed "worst on the LIT composite: --ink-faded at 4.65:1".
   That was false — --accent (a TEXT colour at ~20 sites) sat at 3.63:1 on the
   same composite, and --danger/--verdict-fail at 4.30:1. Both were lifted (see
   their declarations) and the gate widened to every text token, not just ink.
   Recomputed truth for this palette:
     worst gated TEXT pair on the LIT composite #29394e — --ink-faded 4.65:1
     next: --danger / --verdict-fail 4.86:1, --accent 5.09:1, --ink-2 5.34:1
     worst gated text pair on the normative ground #25292f — --ink-faded 5.80:1
   Every number here is re-derived from the declarations by
   test/v17-theme-contrast.test.js; none of them is a fixture.
   ═══════════════════════════════════════════════════════════════════════════ */
:root[data-palette="dark-glass"] {
  --paper: #14110d;          /* AAA i2 — warm umber (was navy #0d1118). One warm surface family: page, rail, sheets and cards */          /* flat base of the ground — see --ground below */
  --paper-2: #201b15;       /* AAA i2 warm (was slate #1a1e25) */        /* the opaque equivalent of --surface (rgba(255,255,255,.055) over --paper) */
  /* --paper-deep is the flattened --surface-raised composite: the opaque twin
     of the glass panel tier, and the meter TRACK (.meter background). The
     contrast test COMPUTES this composite from --surface-raised and --paper
     rather than reading this token, so a 1/255 rounding difference here can
     never be a test failure. */
  --paper-deep: #2c2721;     /* AAA i2 warm (was #25292f) */
  --ink: #ece5d6;            /* AAA i2 warm ink (was cool #dfe6ef) */            /* 11.62:1 vs #25292f · 15.04:1 vs --paper · 9.33:1 vs LIT */
  /* REVIEW CR-02 — LIFTED AGAIN, from #94a1b5 (5.58:1 vs #25292f). The gate that
     matters is not the flat normative ground: .side-pane is a --panel-bg glass
     panel spanning x≈75%, directly under the bloom anchored at `75% -10%`, and
     the ink ramp paints text on it. Against that LIT composite (--panel-bg over
     the bloom's peak stop = #29394e) #94a1b5 measured 4.48:1 — under AA.
     The two ink tiers were scaled by the SAME factor --ink-faded needed, so the
     ramp's proportions are unchanged; only its floor moved. */
  --ink-2: #c4b9a4;          /* AAA i2 warm secondary (was cool #a2b0c6) */          /* 6.65:1 vs #25292f · 5.34:1 vs LIT #29394e */
  /* LIFTED from the sketch to clear AA: --color-text-faint #5d6a7c measures
     2.66:1 against #25292f. The first lift (#8b96a8) cleared the normative
     ground at 4.89:1 but only 3.93:1 against the LIT composite — see the
     --ink-2 note above and REVIEW CR-02. #98a4b7 clears both.
     Same P14/P18/P21 lift discipline used by the two shipped palettes. */
  --ink-faded: #aaa090;      /* AAA i2 warm meta (was cool #98a4b7) */      /* 5.80:1 vs #25292f · 4.65:1 vs LIT #29394e (sketch #5d6a7c was 2.66:1) */
  --rule: #3a3227;           /* AAA i2 warm */           /* 1.15:1 — DECORATIVE border only, never text; ported from the sketch as-is */
  /* LIFTED from the sketch to clear AA: --color-wax-bright #c94a70 measures
     3.27:1 against #25292f. #e0678c measures 4.52:1 — the worst gated pair in
     this palette, and the reason the whole block clears 4.5:1. */
  --accent: #c4b9a4;         /* AAA i2 = --ink-2, warm */         /* AAA SHELL r2 — COLOUR ROLES: the rose accent is RETIRED in the default
                                palette. It carried health, dialogue, the action rail, LEVEL and
                                captions at once (the critics counted seven accent hues). The job
                                of "accent" on dark-glass is now the cool secondary ink (= --ink-2,
                                6.65:1 vs #25292f, 5.34:1 vs LIT): gold = agency, red = danger,
                                green = safe, cool ink = everything neutral. History below.
                                Was #e991ac: 6.34:1 vs #25292f · 5.09:1 vs the LIT composite #29394e.
                                REVIEW CR-02: was #e0678c, which cleared the normative
                                ground at 4.52:1 but measured 3.63:1 on the LIT composite —
                                and --accent is a TEXT colour at ~20 sites (.text-accent
                                plus 19 `color: var(--accent)` rules), several of them
                                .small-caps captions inside .modal/.side-pane surfaces that
                                sit directly under the bloom. Lifted in HSL (hue and
                                saturation held, lightness +10%) so the rose reads the same
                                and only the value moves. Keep --accent-rgb in sync. */
  --accent-2: #7ab8a0;       /* 6.40:1 vs #25292f */
  --gold: #e8c06a;           /* 8.48:1 vs #25292f — this palette's metal */
  /* P14/P18 trigger badges — ported from the sketch UNCHANGED; the whole
     family measures 6.63–10.66:1 against #25292f, so no lift was needed. */
  --trigger-item:         #ffb480;  /* 8.42:1 */
  --trigger-skill:        #9db4ff;  /* 7.24:1 */
  --trigger-knowledge:    #72d8d8;  /* 8.72:1 */
  --trigger-stat:         #cf9aef;  /* 6.63:1 */
  --trigger-relationship: #ffd786;  /* 10.66:1 */
  --trigger-xp:           #8fe0a0;  /* 9.28:1 */
  /* P21 overflow attribution — 8.42–10.69:1 against #25292f. gear reuses the
     metal and feat/state reuse their trigger twins, exactly as observatory does. */
  --overflow-base:  #d6dde8;  /* 10.69:1 */
  --overflow-gear:  #e8c06a;  /* 8.48:1 */
  --overflow-feat:  #8fe0a0;  /* 9.28:1 */
  --overflow-state: #ffd786;  /* 10.66:1 */
  /* The legacy composite. Parchment builds it as sm + lg; this palette does
     the same from its own (much deeper) shadow ramp below. */
  --shadow: 0 1px 3px rgba(0,0,0,.45), 0 16px 50px rgba(0,0,0,.6);
  --danger: #ff8275;         /* 6.06:1 vs #25292f · 7.84:1 vs --paper · 4.86:1 vs LIT #29394e.
                                REVIEW CR-02: was #ff6f61 (4.30:1 on LIT). Same HSL lift as
                                --accent (+4% lightness). --verdict-fail carries the same hex
                                and moves with it. */

  /* P34 (THEME-02): ground and texture.
     --ground carries the sketch's FULL four-layer bloom stack verbatim,
     including the trailing linear-gradient base layer. Do not soften the
     blooms — "deeper void, stronger glows" is locked user direction.
     --paper stays a FLAT hex above: color-mix(in srgb, …, var(--paper))
     appears at thirteen sites and a gradient there voids every one of them
     silently, with no console warning.
     ⚠ --grain-image and --grain-opacity are a MATCHED PAIR. This SVG is the
     SKETCH's 140x140 grain (from sources/001-game-frame/index.html:44), which
     carries its alpha in a `rect opacity` attribute; the app's own 400x400
     SVG bakes alpha into a feColorMatrix instead. Pairing one palette's image
     with another palette's opacity produces a wildly wrong texture. Change
     them together or not at all.

     REVIEW CR-01: the final layer carries a background-COLOUR (#070a10, the
     bottom stop of its own base linear-gradient) as well as the gradient. With
     `background-repeat: no-repeat` on `html`, that colour is what paints the
     canvas below the first viewport, and matching the base gradient's bottom
     stop is what makes the hand-off seamless. Keep the two in sync if the base
     gradient is ever retuned. */
  --ground:
    radial-gradient(ellipse 80% 60% at 50% -10%, rgba(214,160,86,.16), transparent 62%),
    radial-gradient(ellipse 70% 55% at 10% 110%, rgba(120,70,40,.14), transparent 62%),
    radial-gradient(ellipse 40% 30% at 45% 45%, rgba(150,110,60,.06), transparent 70%),
    linear-gradient(180deg, #16120e, #0b0907) #0b0907;
  --vignette: radial-gradient(ellipse at 50% 35%, transparent 60%, rgba(0,0,0,.38) 100%);
  --grain-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
  --grain-opacity: 0.028;
  --scrim: rgba(8,6,4,.78);

  /* P34 (THEME-02): surface, border and backdrop tiers — the glass itself.
     The three surface tiers are TRANSLUCENT here (they are what
     --panel-backdrop refracts). Their opaque flattened twins live in the
     @supports fallback further down, for browsers with no backdrop-filter. */
  --surface: rgba(255,238,210,.055);        /* flattens to #1a1e25 over --paper */
  --surface-2: rgba(255,238,210,.045);      /* flattens to #181c22 */
  --surface-raised: rgba(255,238,210,.10);  /* flattens to #25292f — the normative AA ground */
  --border-soft: rgba(255,232,196,.15);
  --border-strong: rgba(255,232,196,.30);
  --panel-backdrop: blur(16px) saturate(150%);

  /* P34 (THEME-02): elevation and glow — the sketch's own ramp, deeper than
     either shipped palette because the ground is darker. */
  --shadow-sm: 0 1px 3px rgba(0,0,0,.45);
  --shadow-md: 0 6px 20px rgba(0,0,0,.5);
  --shadow-lg: 0 16px 50px rgba(0,0,0,.6);
  --glow-gilt: 0 0 14px rgba(232,192,106,.4);

  /* P34 (THEME-02): downstream tints — Phase 38 risk-edge tinting. The wash
     family below is scaled off these two. */
  --tint-wash: rgba(255,255,255,.035);
  --tint-wash-strong: rgba(255,255,255,.07);
  --tint-sunken: rgba(0,0,0,.30);

  /* P34 (THEME-02): verdict colours — LIFTED for the dark ground. The five
     shipped hexes (#4a7c4f / #b58a3a / #8a3030 / gold / #5a1a1a) land at
     ~2.8:1 here, so the roll card would be unreadable. Family measures
     5.36–10.82:1 against #25292f. crit-fail reads BRIGHTER than plain fail on
     purpose — the extra weight comes from --shadow-lg and the fill
     percentage, not from a darker, unreadable hue. */
  --verdict-success: #6fd0a0;    /* 7.80:1 */
  --verdict-partial: #e8b14e;    /* 7.54:1 */
  --verdict-fail: #ff8275;       /* 6.06:1 — REVIEW CR-02, tracks --danger */
  --verdict-crit: #ffd98a;       /* 10.82:1 — the sketch's --color-crit */
  --verdict-crit-fail: #ff9a8a;  /* 7.13:1 */
  /* REVIEW WR-01 — the card fill follows the crit hue here (the legacy
     palettes pin a literal instead; see the parchment block). */
  --verdict-crit-fill: var(--verdict-crit);

  /* P34 (THEME-02): consolidation-derived. Unlike parchment and observatory —
     where every value below is copied BYTE-FOR-BYTE out of the declaration it
     replaced — these are DESIGNED, because no dark-glass rule ever existed to
     copy. Each is derived from a tier above so the glass reads coherently:
     the panel/pane surfaces take --surface, the wash family keeps the
     parchment gradient GEOMETRY (same angle, same stop percentage) and swaps
     only the colour for --tint-wash / --tint-wash-strong, and the dice-chrome
     tints use the DARK direction (matching observatory, not parchment).
     ⚠ The three --tint-raised* tokens are SIGN-INVERTED relative to
     parchment: parchment lightens dice chrome with white overlays, this
     palette darkens it. A white overlay on this ground would blow out the
     glass. Do not "harmonise" them into the row-wash family. */
  --panel-bg: var(--surface);                                                      /* .modal + .tweaks-panel become glass instead of opaque paper */
  --pane-bg: var(--surface);                                                       /* .side-pane */
  --hover-wash: rgba(255,232,196,.08);
  --wash-action: linear-gradient(90deg, rgba(255,255,255,0.035), transparent 50%);  /* parchment geometry, --tint-wash colour */
  --wash-suggest: linear-gradient(90deg, rgba(255,255,255,0.035), transparent 65%);
  --wash-suggest-hover: linear-gradient(90deg, rgba(255,255,255,0.07), transparent 60%); /* --tint-wash-strong */
  --wash-row: linear-gradient(90deg, rgba(255,255,255,0.035), transparent 70%);
  --tint-flat-row: rgba(255,255,255,0.035);
  --tint-flat-dc: rgba(255,255,255,0.045);                                         /* the --surface-2 tier */
  --tint-raised: rgba(0,0,0,0.30);                                                 /* SIGN-INVERTED vs parchment — see the warning above */
  --tint-raised-soft: linear-gradient(180deg, rgba(0,0,0,0.30), transparent 60%);  /* SIGN-INVERTED */
  --tint-raised-chip: rgba(0,0,0,0.25);                                            /* SIGN-INVERTED */
  --kbd-face: rgba(255,255,255,0.045);
  --kbd-face-2: rgba(255,255,255,0.10);
  --kbd-shadow: 0 1px 0 rgba(0,0,0,0.45);
  --kbd-ink: var(--ink);
  --roll-bg: var(--paper-deep);                                                    /* the 34-UI-SPEC consolidation map assigns .roll to --paper-deep */
  --shadow-card: 0 6px 20px rgba(0,0,0,0.5);                                       /* the --shadow-md tier */
  --shadow-hover: 0 6px 20px rgba(0,0,0,0.5);

  /* P34 (THEME-02): the six previously-undefined tokens, resolved the same
     way as in the shipped palettes. --accent-rgb is the space-separated
     channel triple of this palette's own --accent #e0678c. */
  --ink-accent: var(--gold);
  --ink-muted: var(--ink-faded);
  --ink-shadow: var(--rule);
  --bg: var(--paper);
  --bg-card: rgba(255,255,255,0.055);
  --accent-rgb: 196 185 164;        /* channels of --accent #a2b0c6 (AAA SHELL r2; was 233 145 172) */

  /* v1.7 P39 (CHROME-01): CTA triad — see the parchment block for the contract.
     The fill is the --surface-raised glass tier warmed 12% toward the metal, so
     the CTA reads as the same frosted plate family as .shell-hdr and .modal
     rather than as a painted button. It composites to ~rgba(242,219,169,.21)
     over the ground; --gold #e8c06a on that lands at 5.9:1.
     NO-BACKDROP-FILTER PATH NEEDS NOTHING EXTRA: the @supports fallback at
     styles.css:786 re-declares --surface-raised as the opaque #25292f, and
     custom-property substitution is LAZY — the mix above is resolved where it is
     USED, not where it is written — so a browser without backdrop-filter gets an
     opaque warm plate from this same declaration. Do not add a fallback copy of
     --cta-fill to that block; it would be dead weight that drifts. */
  /* AAA i2 — ONE button language: the primary is solid gold with dark ink
     (the frosted gold-on-olive plate read as disabled, and selected chips
     shared it). Selected states no longer read this triad (styles.css i2 §8). */
  --cta-fill: var(--gold);
  --cta-ink: var(--paper);
  --cta-edge: var(--gold);
}

/* ═══════════════════════════════════════════════════════════════════════════
   P34 (THEME-02): arcane palette — ported from
   .claude/skills/sketch-findings-TRPG/sources/themes/arcane.css
   ("void-dark ground with violet aurora blooms, luminous arcane accents,
   frosted panels that glow at the edges. Crits stay power-gold.").

   Same 71 tokens, same declaration order, as the parchment block above.

   NORMATIVE background for every ratio below: --surface-raised
   rgba(160,120,255,.15) flattened over the flat --paper #010102 = #191328.
   (The 34-UI-SPEC table prints #191228 for this composite; recomputing it
   gives g = 120*.15 + 1*.85 = 18.85 -> 19 = 0x13, so #191328 is correct and
   the SPEC literal is off by one in the green channel. The contrast test
   COMPUTES the composite rather than reading a literal precisely so this
   cannot become fixture drift.)
   REVIEW CR-02: the whole TEXT family is also gated against the LIT composite
   (--panel-bg over this palette's brightest aurora bloom stop = #3d256e) —
   see the dark-glass header for why --surface-raised alone was not the worst
   case. Ratios below record both grounds.
   REVIEW CR-02 (iteration 2): the first pass gated only the ink ramp, and this
   note claimed --ink-faded was the worst at 4.65:1. False — --accent was at
   3.62:1 and --danger/--verdict-fail at 4.14:1 on the same composite. Both were
   lifted rather than softening the aurora: "deeper void, stronger glows" is
   locked user direction, and crits stay power-gold (--verdict-crit #ffd75e is
   untouched). Recomputed truth for this palette:
     worst gated TEXT pair on the LIT composite #3d256e — --ink-faded 4.65:1
     next: --danger / --verdict-fail 4.97:1, --accent 5.02:1, --ink-2 5.78:1
     worst gated text pair on the normative ground #191328 — --ink-faded 6.74:1
   ═══════════════════════════════════════════════════════════════════════════ */
:root[data-palette="arcane"] {
  --paper: #010102;          /* true void — flat base of the ground */
  --paper-2: #0c0815;
  /* Flattened --surface-raised composite; also the meter TRACK. See the
     dark-glass note: the contrast test derives this composite itself. */
  --paper-deep: #191328;
  --ink: #e6e0f2;            /* 14.00:1 vs #191328 · 16.20:1 vs --paper · 9.66:1 vs LIT */
  /* REVIEW CR-02 — LIFTED AGAIN, from #9b90b8 (6.07:1 vs #191328), which measured
     only 4.19:1 against the LIT composite (--panel-bg over the aurora bloom's
     peak stop = #3d256e). See the dark-glass twin of this note. Both ink tiers
     were scaled by the same factor --ink-faded needed, so the ramp keeps its
     proportions. */
  --ink-2: #b7aad9;          /* 8.38:1 vs #191328 · 5.78:1 vs LIT #3d256e */
  /* LIFTED from the sketch to clear AA: --color-text-faint #645a80 measures
     2.85:1 against #191328. The first lift (#8b7fb0) cleared the normative
     ground at 4.94:1 but only 3.41:1 against the LIT composite (REVIEW CR-02).
     #a496d0 clears both, and its 4.65:1 on LIT is the worst gated pair in this
     palette. */
  --ink-faded: #a496d0;      /* 6.74:1 vs #191328 · 4.65:1 vs LIT #3d256e (sketch #645a80 was 2.85:1) */
  --rule: #2a2040;           /* 1.18:1 — DECORATIVE border only, never text; ported as-is */
  /* LIFTED from the sketch to clear AA: --color-wax-bright #c04aa2 measures
     4.07:1 against #191328. #d162b4 measures 5.25:1. */
  --accent: #dd89c7;         /* 7.28:1 vs #191328 · 5.02:1 vs the LIT composite #3d256e.
                                REVIEW CR-02: was #d162b4 — 5.25:1 on the normative ground
                                but 3.62:1 on LIT, and --accent is a text colour. Same HSL
                                lift as dark-glass (+10% lightness, hue/saturation held), so
                                the magenta is unchanged in character. Keep --accent-rgb in
                                sync. The bloom was NOT softened to buy this: "deeper void,
                                stronger glows" is locked user direction. */
  --accent-2: #5ce0e0;       /* 11.32:1 vs #191328 */
  --gold: #b78cff;           /* 7.05:1 — luminous violet, this palette's power metal */
  /* P14/P18 trigger badges — ported from the sketch UNCHANGED; the whole
     family measures 7.75–12.73:1 against #191328. */
  --trigger-item:         #ffab70;  /* 9.71:1 */
  --trigger-skill:        #86a8ff;  /* 7.77:1 */
  --trigger-knowledge:    #5ce0e0;  /* 11.32:1 */
  --trigger-stat:         #d98aff;  /* 7.75:1 */
  --trigger-relationship: #ffd376;  /* 12.73:1 */
  --trigger-xp:           #7fe8a8;  /* 12.02:1 */
  /* P21 overflow attribution — 11.25–12.79:1 against #191328. */
  --overflow-base:  #ddd6ec;  /* 12.79:1 */
  --overflow-gear:  #d9c2ff;  /* 11.25:1 */
  --overflow-feat:  #7fe8a8;  /* 12.02:1 */
  --overflow-state: #ffd376;  /* 12.73:1 */
  --shadow: 0 1px 3px rgba(1,0,5,.6), 0 16px 55px rgba(1,0,5,.75);
  --danger: #ff7991;         /* 7.21:1 vs #191328 · 8.34:1 vs --paper · 4.97:1 vs LIT #3d256e.
                                REVIEW CR-02: was #ff5a78 (4.14:1 on LIT). --verdict-fail
                                carries the same hex and moves with it. */

  /* P34 (THEME-02): ground and texture. Four-layer aurora bloom stack, ported
     verbatim from the sketch — do NOT soften the blooms ("deeper void,
     stronger glows" is locked user direction, already applied to this
     source). --paper stays a FLAT hex; see the dark-glass note on the
     thirteen color-mix() sites.
     ⚠ --grain-image and --grain-opacity are a MATCHED PAIR: this is the
     SKETCH's 140x140 SVG, whose alpha rides on a `rect opacity` attribute,
     not the app's feColorMatrix. Never pair it with parchment's or
     observatory's --grain-opacity.

     REVIEW CR-01: the final layer also carries a background-COLOUR (#010102,
     the bottom stop of its own base linear-gradient, which is also --paper).
     See the dark-glass note — with `background-repeat: no-repeat` on `html`
     that colour paints the canvas below the first viewport. */
  --ground:
    radial-gradient(ellipse 75% 60% at 78% -12%, rgba(140,80,255,.38), transparent 56%),
    radial-gradient(ellipse 65% 55% at 5% 108%, rgba(60,200,255,.20), transparent 58%),
    radial-gradient(ellipse 50% 40% at 50% 55%, rgba(120,40,200,.13), transparent 68%),
    linear-gradient(180deg, #08050f, #040208 60%, #010102) #010102;
  --vignette: radial-gradient(ellipse at 50% 35%, transparent 45%, rgba(1,0,4,.62) 100%);
  --grain-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
  --grain-opacity: 0.025;
  --scrim: rgba(1,0,4,.82);

  /* P34 (THEME-02): surface, border and backdrop tiers — violet-tinted glass.
     Opaque twins live in the @supports fallback further down. */
  --surface: rgba(150,110,255,.075);        /* flattens to #0c0915 over --paper */
  --surface-2: rgba(150,110,255,.06);       /* flattens to #0a0811 */
  --surface-raised: rgba(160,120,255,.15);  /* flattens to #191328 — the normative AA ground */
  --border-soft: rgba(170,130,255,.24);
  --border-strong: rgba(200,160,255,.5);
  --panel-backdrop: blur(14px) saturate(160%);

  /* P34 (THEME-02): elevation and glow. --glow-gilt is the sketch's layered
     core-plus-corona form — TWO shadows, and the corona is what does the
     "power" work. Do not collapse it to one. */
  --shadow-sm: 0 1px 3px rgba(1,0,5,.6);
  --shadow-md: 0 6px 22px rgba(1,0,5,.65);
  --shadow-lg: 0 16px 55px rgba(1,0,5,.75);
  --glow-gilt: 0 0 10px rgba(200,165,255,.75), 0 0 32px rgba(140,80,255,.5);

  /* P34 (THEME-02): downstream tints — Phase 38. */
  --tint-wash: rgba(170,130,255,.05);
  --tint-wash-strong: rgba(190,150,255,.10);
  --tint-sunken: rgba(1,0,5,.35);

  /* P34 (THEME-02): verdict colours — LIFTED for the void ground. Family
     measures 6.00–12.99:1 against #191328. --verdict-crit stays POWER-GOLD
     even though this palette's metal is violet — locked user direction,
     sourced from the sketch's own comment ("crits stay power-gold against the
     violet"). This is the one place --verdict-crit deliberately does NOT
     follow --gold. */
  --verdict-success: #58e0b0;    /* 10.90:1 */
  --verdict-partial: #ffc85e;    /* 11.74:1 */
  --verdict-fail: #ff7991;       /* 7.21:1 — REVIEW CR-02, tracks --danger */
  --verdict-crit: #ffd75e;       /* 12.99:1 — power-gold, NOT var(--gold) */
  --verdict-crit-fail: #ff8fa4;  /* 8.34:1 */
  /* REVIEW WR-01 — the card fill follows the power-gold crit hue. */
  --verdict-crit-fill: var(--verdict-crit);

  /* P34 (THEME-02): consolidation-derived — DESIGNED, not copied (no arcane
     rule ever existed). Same derivation rules as dark-glass: panel/pane take
     --surface; the wash family keeps parchment's gradient geometry and swaps
     the colour for --tint-wash / --tint-wash-strong; the dice-chrome tints use
     the DARK direction.
     ⚠ The three --tint-raised* tokens are SIGN-INVERTED relative to parchment
     for the same reason as dark-glass. Do not harmonise them. */
  --panel-bg: var(--surface);
  --pane-bg: var(--surface);
  --hover-wash: rgba(190,150,255,.12);
  --wash-action: linear-gradient(90deg, rgba(170,130,255,0.05), transparent 50%);
  --wash-suggest: linear-gradient(90deg, rgba(170,130,255,0.05), transparent 65%);
  --wash-suggest-hover: linear-gradient(90deg, rgba(190,150,255,0.10), transparent 60%);
  --wash-row: linear-gradient(90deg, rgba(170,130,255,0.05), transparent 70%);
  --tint-flat-row: rgba(170,130,255,0.05);
  --tint-flat-dc: rgba(150,110,255,0.06);                                          /* the --surface-2 tier */
  --tint-raised: rgba(1,0,5,0.35);                                                 /* SIGN-INVERTED vs parchment */
  --tint-raised-soft: linear-gradient(180deg, rgba(1,0,5,0.35), transparent 60%);  /* SIGN-INVERTED */
  --tint-raised-chip: rgba(1,0,5,0.30);                                            /* SIGN-INVERTED */
  --kbd-face: rgba(150,110,255,0.06);
  --kbd-face-2: rgba(160,120,255,0.15);
  --kbd-shadow: 0 1px 0 rgba(1,0,5,0.6);
  --kbd-ink: var(--ink);
  --roll-bg: var(--paper-deep);
  --shadow-card: 0 6px 22px rgba(1,0,5,0.65);                                      /* the --shadow-md tier */
  --shadow-hover: 0 6px 22px rgba(1,0,5,0.65);

  /* P34 (THEME-02): the six previously-undefined tokens. --accent-rgb is the
     channel triple of this palette's own --accent #d162b4. */
  --ink-accent: var(--gold);
  --ink-muted: var(--ink-faded);
  --ink-shadow: var(--rule);
  --bg: var(--paper);
  --bg-card: rgba(150,110,255,0.075);
  --accent-rgb: 221 137 199;        /* channels of --accent #dd89c7 — REVIEW CR-02 lift */

  /* v1.7 P39 (CHROME-01): CTA triad — see the parchment block for the contract.
     Same construction as dark-glass, against this palette's own metal: the
     "gilt" here is the luminous violet --gold #b78cff, so a primary CTA reads as
     arcane rather than as a gold button dropped onto a violet theme. Crits stay
     power-gold via --verdict-crit; the CTA deliberately does not. */
  --cta-fill: color-mix(in srgb, var(--gold) 12%, var(--surface-raised));
  --cta-ink: var(--gold);
  --cta-edge: var(--gold);
}

/* ═══════════════════════════════════════════════════════════════════════════
   P34 (THEME-02): no-backdrop-filter fallback.

   A browser without backdrop-filter renders the two glass palettes' panels as
   near-transparent sheets floating over the bloom ground — text on top of a
   moving gradient. This block swaps the three translucent surface tiers for
   their OPAQUE flattened composites so those browsers get a solid surface
   instead. It swaps token VALUES ONLY and never layout: no length, no
   geometry, no per-selector rule, so the render is identical box-for-box.

   The values are the exact source-over composites of each tier over that
   palette's flat --paper — round(fg*a + bg*(1-a)) per channel — which are the
   same composites test/v17-theme-contrast.test.js computes as its normative
   AA backgrounds. That makes the fallback AA-equivalent BY CONSTRUCTION
   rather than by a second round of colour picking.
   (The 34-UI-SPEC prints slightly different literals here; recomputing the
   composites gives the values below, and matching the test's own maths is
   what the "AA-equivalent by construction" claim actually requires.)

   Same specificity as the palette blocks, later in source order, so it wins.
   Both test files slice @supports regions out before their parity parse, so
   these overrides do not read as duplicate or extra token declarations.
   ═══════════════════════════════════════════════════════════════════════════ */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  :root[data-palette="dark-glass"] {
    --surface:        #201b16;  /* rgba(255,238,210,.055) over #14110d (AAA i2 warm) */
    --surface-2:      #1e1a15;  /* rgba(255,238,210,.045) over #14110d */
    --surface-raised: #2c2721;  /* rgba(255,238,210,.10)  over #14110d — the normative AA ground */
  }
  :root[data-palette="arcane"] {
    --surface:        #0c0915;  /* rgba(150,110,255,.075) over #010102 */
    --surface-2:      #0a0811;  /* rgba(150,110,255,.06)  over #010102 */
    --surface-raised: #191328;  /* rgba(160,120,255,.15)  over #010102 — the normative AA ground */
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   DESIGN SCALE — the palette-independent half of the token system.
   Owner: AAA SHELL workstream. Spec: .planning/aaa/design/DESIGN.md.

   Colour and texture live in the four palette blocks above (the frozen
   71-token contract). EVERYTHING ELSE that a component should never hardcode
   lives here: type, spacing, radii, elevation aliases and motion. These
   values are identical in every palette by construction, which is why they
   sit on `html` and NOT in a `:root` palette block (THEME-02 SC4 forbids
   --space-/--radius-/--text-/--font-/--leading- tokens in a palette block, and
   the palette parser reads every bare `:root {}`).
   test/v17-theme-contract.test.js resolves var() names declared in a bare
   `html {}` rule, so these are real, checked tokens — not a hole.

   Other workstreams: APPEND scale tokens inside this block under a
   `/* tokens:<workstream> *\/` marker. Colour roles are NOT aliased here — a
   `color:` must name a palette token directly (the contrast suite gates it);
   DESIGN.md §2 is the role map.
   ═══════════════════════════════════════════════════════════════════════════ */
html {
  /* ---- tokens:shell ---- */
  /* type */
  --font-serif: 'EB Garamond', 'Cormorant Garamond', Georgia, serif;
  --font-display: 'Cinzel', 'EB Garamond', Georgia, serif;
  --text-xs: 14px;        /* hard floor: kbd, tiny tags (never below on phone) */
  --text-sm: 15px;        /* meta, labels, captions */
  --text-md: 17px;        /* UI body, buttons, inputs */
  --text-lg: 19px;        /* choice text, lead UI copy */
  --text-prose: clamp(20px, 0.4vw + 16px, 24px);  /* the reading size: 20 phone, ~22 laptop, ~24 wide */
  --text-title: 26px;
  --text-display: 34px;
  --leading-tight: 1.2;
  --leading-ui: 1.4;
  --leading-prose: 1.62;
  --track-caps: 0.06em;   /* small caps tracking — never the old .2em walls */
  --measure: 27em;        /* prose line length: ~70 characters of EB Garamond (0.376em/char measured) */
  /* space — a 4/8 grid, nothing else */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;
  --tap: 44px;            /* minimum hit target */
  /* radii — two, plus round for pips/dots/bars */
  --radius-1: 3px;        /* controls: buttons, inputs, tags, kbd */
  --radius-2: 8px;        /* surfaces: cards, panels, banners, modals */
  --radius-round: 999px;
  /* elevation — aliases onto the palette's own shadow ramp */
  --elev-1: var(--shadow-sm);
  --elev-2: var(--shadow-md);
  --elev-3: var(--shadow-lg);
  /* motion */
  --dur-fast: 120ms;
  --dur-base: 200ms;
  --dur-slow: 320ms;
  --dur-reveal: 640ms;
  --ease-out: cubic-bezier(0.2, 0.7, 0.2, 1);
  --ease-in-out: cubic-bezier(0.45, 0, 0.25, 1);
  --ease-emph: cubic-bezier(0.3, 1.25, 0.5, 1);
  /* material (r2) — candlelit vellum. Palette-aware where a colour is involved
     (they mix palette tokens), palette-neutral where they are pure light/shade. */
  --tex-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1  0 0 0 0 .94  0 0 0 0 .82  0 0 0 1 0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)' opacity='.065'/%3E%3C/svg%3E");
  --page: color-mix(in srgb, var(--paper) 89%, var(--gold));              /* the warm vellum sheet */
  --page-deep: color-mix(in srgb, var(--paper) 93%, var(--gold));
  --candle: radial-gradient(ellipse 75% 42% at 50% 0%, color-mix(in srgb, var(--gold) 9%, transparent), transparent 78%);
  --vignette-warm: radial-gradient(ellipse 110% 85% at 50% 38%, transparent 52%, rgba(16, 10, 4, 0.5) 100%);
  --leaf: linear-gradient(90deg, transparent, color-mix(in srgb, var(--gold) 45%, transparent) 14%, color-mix(in srgb, var(--gold) 85%, white) 50%, color-mix(in srgb, var(--gold) 45%, transparent) 86%, transparent);
  --edge-light: inset 0 1px 0 rgba(255, 255, 255, 0.06);
  --shadow-rail: -16px 0 32px rgba(0, 0, 0, 0.45);
  --shadow-sheet: 0 -18px 44px -12px rgba(0, 0, 0, 0.65);
  --shadow-page: 0 0 0 1px rgba(255, 255, 255, 0.035), 0 28px 80px -24px rgba(0, 0, 0, 0.75);
  --brass: color-mix(in srgb, var(--gold) 74%, var(--paper-2));          /* resource bars */
  --rose-muted: color-mix(in srgb, var(--danger) 72%, var(--ink-faded)); /* health, not an alarm */
  /* ---- tokens:surfaces ---- */
  /* The opaque sheet: the raised surface flattened over the page colour. One
     value for every palette (glass tiers are translucent, so they composite over
     --paper; parchment's tier is already opaque). Used by the settings, level-up
     and confirm sheets and their sticky bars. Resolves lazily per palette. */
  --sheet-fill: linear-gradient(var(--surface-raised), var(--surface-raised)), var(--paper);
}
/* Reduced motion is a TOKEN property: every duration collapses, so anything
   that animates through --dur-* simplifies for free. */
@media (prefers-reduced-motion: reduce) {
  html {
    --dur-fast: 1ms;
    --dur-base: 1ms;
    --dur-slow: 1ms;
    --dur-reveal: 1ms;
  }
}

* { box-sizing: border-box; }

/* P34 (THEME-02): the page background is split off --paper onto --ground.
 *
 * (a) --paper MUST stay a FLAT COLOUR. 13 declarations blend against it with
 *     color-mix() (the .roll-card fill ramp, the gold variants). A gradient in
 *     --paper makes every one of those invalid at computed-value time, the whole
 *     declaration is dropped, and there is NO console warning. The paintable
 *     bloom stack the dark-glass and arcane palettes need therefore lives in its
 *     own token, --ground, which nothing color-mixes.
 *
 * (b) The ground is applied to `html` ONLY, and `body` is made transparent.
 *     Painting a gradient on BOTH elements sizes it to two different boxes on a
 *     scrolling document (background-attachment defaults to `scroll`, so html's
 *     gradient sizes to the canvas and body's to the body box) and the two
 *     visibly mismatch at the seam. Canvas propagation from `html` already
 *     covers the whole viewport, so this is the smallest correct change.
 *     The fixed-viewport rule (html, body { height:100%; overflow:hidden })
 *     that would make the seam moot is PHASE 35, not this phase — between P34
 *     and P35 the menu, session-setup, both wizards and every modal still
 *     scroll the document. Do not add background-attachment: fixed (costs a
 *     compositor layer on mobile) and do not move the ground onto body::before
 *     (that reworks the texture stack, which is Phase 35's job).
 *
 * (c) REVIEW CR-01 — background-repeat MUST stay no-repeat here. Canvas
 *     propagation covers the PAINTING area, not the POSITIONING area: per CSS
 *     Backgrounds §2.11.2 the root background image is still sized and
 *     positioned against the root element's own box, and `height: 100%`
 *     resolves that box against the ICB — exactly one viewport tall. With the
 *     initial `repeat`, the four-layer bloom stack therefore TILED once per
 *     viewport of scroll on the menu, session-setup, both wizards and every
 *     scrolling modal. Invisible before this phase only because --ground was a
 *     flat colour in both shipped palettes.
 *     no-repeat alone would leave everything below the first viewport
 *     unpainted (a gradient stack carries no background-color), so each dark
 *     palette's --ground ends with an explicit final-layer COLOUR equal to the
 *     bottom stop of its own base linear-gradient. That colour is what the
 *     canvas below the fold gets, and it continues the ground seamlessly.
 *     background-size is stated as 100% 100% (the positioning area) rather than
 *     100vh so it tracks the html box exactly on mobile, where 100vh is the
 *     LARGE viewport and disagrees with `height: 100%` while the URL bar is
 *     showing.
 *
 * Renders identically to the pre-change baseline today: --ground is
 * var(--paper) in both shipped palettes. */
html {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--ground);
  /* CR-01 — longhands MUST follow the shorthand: `background: var(--ground)`
     resets both of these to their initial values. */
  background-repeat: no-repeat;
  background-size: 100% 100%;
  color: var(--ink);
  font-family: 'EB Garamond', 'Cormorant Garamond', Georgia, serif;
  font-size: 22px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: transparent;
  color: var(--ink);
  font-family: 'EB Garamond', 'Cormorant Garamond', Georgia, serif;
  font-size: 22px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* Paper texture — subtle speckle using SVG filters */
/* v1.7 P35 SHELL-02 — POSITIONING GUARD, no declaration changed. This rule is
   already correct and the phase's job is to PIN it, not restructure it (no new
   texture DOM this phase). All four of `position: fixed`, `inset: 0`,
   `pointer-events: none` and `z-index: 0` are load-bearing together:
     · fixed + inset 0 keeps the layer out of every flow, so it can never become
       a grid row of the play shell (the overlay-as-grid-row pitfall: MEASURED as
       a 4px phantom row that shoves the header to top: 4 and makes it climb as
       prose fills the 1fr row).
     · pointer-events: none — drop it and a full-viewport overlay silently eats
       every click in the shell, a clickjacking-shaped bug.
     · z-index: 0 paints it behind #root (position: relative, z-index: 1).
   Pinned by test/v17-shell-contract.test.js A5. Because these are
   pseudo-elements they can never BECOME children of .play-shell — but they can
   be re-authored, and that assertion is what stops it. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the
     palette blocks. MATCHED PAIR: each SVG bakes its own alpha, so the image
     and the opacity must always come from the same palette. */
  opacity: var(--grain-opacity);
  z-index: 0;
  background-image: var(--grain-image);
}

/* Subtle vignette */
/* v1.7 P35 SHELL-02 — POSITIONING GUARD, no declaration changed. Same contract
   as body::before above: fixed + inset 0 + pointer-events none + z-index 0, and
   for the same three reasons. It sits AFTER the grain in source order so the
   vignette composites over it at the same z. Pinned by A5. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the palette blocks */
  background: var(--vignette);
}

#root {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  min-height: 100dvh;  /* track collapsing mobile browser chrome */
}

/* Typography */
.display {
  font-family: 'Cinzel', 'Cormorant Garamond', serif;
  font-weight: 600;
  letter-spacing: 0.04em;
}

.serif-display {
  font-family: 'Cormorant Garamond', serif;
  font-weight: 500;
}

.mono {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 0.82em;
  letter-spacing: 0.02em;
}

.small-caps {
  font-variant-caps: all-small-caps;
  letter-spacing: 0.14em;
}

h1, h2, h3 {
  font-family: 'Cinzel', serif;
  font-weight: 600;
  letter-spacing: 0.05em;
  margin: 0;
}

/* Buttons */
.btn {
  font-family: 'Cinzel', serif;
  font-size: 16px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 500;
  padding: 10px 22px;
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--rule);
  cursor: pointer;
  transition: all 180ms ease;
  position: relative;
}

/* v1.7 P39 (CHROME-01): hover is a WASH, not an inversion.
   `background: var(--ink); color: var(--paper)` reads as a deliberate stamp on
   parchment and as a white block flashing on the three dark palettes — the same
   defect as the pre-P39 .btn-primary below. --hover-wash is the token the
   consolidation already created for exactly this (it is what .btn-ghost:hover
   takes) and it is directional per palette, so one rule serves all four. */
.btn:hover {
  background: var(--hover-wash);
  color: var(--ink);
  border-color: var(--border-strong);
}

/* v1.7 P39 (CHROME-01): THE PRIMARY CTA — gilt, on every palette.
   Was `background: var(--ink); color: var(--paper); border-color: var(--ink)`,
   which is correct ink-on-paper for parchment and a foreign near-white slab on
   the three dark palettes. It painted the menu's "Begin a New Chronicle", every
   wizard's "Continue" and the play composer's Send button.
   The three --cta-* tokens carry the branch (see the parchment palette block);
   the GEOMETRY here is unchanged — same 1px border, same padding inherited from
   .btn — so the SC4 cross-palette bounding boxes cannot move. */
.btn-primary {
  background: var(--cta-fill);
  color: var(--cta-ink);
  border-color: var(--cta-edge);
  box-shadow: var(--shadow-sm);
}

/* Hover LIFTS the same plate rather than repainting it a different colour: the
   gilt glow is --glow-gilt, the token P34 reserved for the metal's own bloom. */
.btn-primary:hover {
  background: var(--cta-fill);
  border-color: var(--cta-edge);
  color: var(--cta-ink);
  box-shadow: var(--glow-gilt), var(--shadow-md);
}

/* AAA SURFACES — the destructive action (confirm dialogs: "Burn it").
   It used to be .btn-primary with an inline `background: var(--accent)`, which
   kept the primary's GOLD label on a rose slab: 1.35:1 on dark-glass. Danger
   is its own treatment: a danger-tinted plate, a danger edge, and an --ink
   label, which clears AA on every palette because --ink is the palette's own
   body-text colour and the plate is only a 20% tint over the sheet. */
.btn-danger {
  background: color-mix(in srgb, var(--danger) 20%, transparent);
  color: var(--ink);
  border-color: color-mix(in srgb, var(--danger) 80%, transparent);
  box-shadow: var(--shadow-sm);
}
.btn-danger:hover {
  background: color-mix(in srgb, var(--danger) 30%, transparent);
  color: var(--ink);
  border-color: var(--danger);
  box-shadow: 0 0 12px color-mix(in srgb, var(--danger) 35%, transparent), var(--shadow-md);
}

.btn-ghost {
  border-color: transparent;
  padding: 8px 14px;
  font-size: 14px;
}

.btn-ghost:hover {
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the palette blocks */
  background: var(--hover-wash);
  color: var(--ink);
  border-color: transparent;
}

.btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* Inputs */
.input, .textarea {
  font-family: 'EB Garamond', serif;
  font-size: 19px;
  color: var(--ink);
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--rule);
  padding: 6px 0;
  width: 100%;
  outline: none;
  transition: border-color 150ms;
}

.input:focus, .textarea:focus {
  border-bottom-color: var(--ink);
}

.textarea {
  resize: vertical;
  min-height: 80px;
  line-height: 1.5;
}

.label {
  font-family: 'Cinzel', serif;
  font-size: 14px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-faded);
  display: block;
  margin-bottom: 4px;
}

/* Ornament */
.ornament {
  display: flex;
  align-items: center;
  gap: 14px;
  color: var(--rule);
  justify-content: center;
}

.ornament::before, .ornament::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--rule);
  max-width: 120px;
}

/* v1.7 P39 (CHROME-03): the glyph is gilt; the two hairlines flanking it stay
   --rule (see .ornament above — that declaration is load-bearing for the
   contrast suite's exclusion self-check, not dead weight). */
.ornament-glyph {
  font-size: 20px;
  opacity: 0.85;
  color: color-mix(in srgb, var(--gold) 75%, var(--paper));
}

/* Rule */
.rule {
  border: none;
  border-top: 1px solid var(--rule);
  opacity: 0.5;
  margin: 18px 0;
}

.dotted {
  background-image: linear-gradient(to right, var(--rule) 50%, transparent 50%);
  background-size: 6px 1px;
  background-repeat: repeat-x;
  background-position: 0 100%;
  height: 1px;
}

/* Card */
/* v1.7 P39 (CHROME-02): the card becomes the same frosted panel .modal and
   .side-pane already are. --panel-bg resolves to the opaque paper tier on
   parchment/observatory and to the glass tier on dark-glass/arcane, and
   --panel-backdrop is `none` on the legacy pair, so the blur is a no-op there
   and no component acquires a palette fork.
   ⚠ backdrop-filter makes an element a containing block for `position: fixed`
   DESCENDANTS. .card hosts no fixed descendant anywhere on the pre-play screens;
   if one is ever added, this pair is the first thing to remove. */
.card {
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop, none);
          backdrop-filter: var(--panel-backdrop, none);
  border: 1px solid var(--border-soft);
  box-shadow: var(--shadow-card);
  padding: 24px;
  position: relative;
}

.card.deep {
  background: var(--paper-deep);
}

/* Scrollbar */
::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: var(--rule);
  opacity: 0.3;
  border-radius: 0;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--ink-faded);
}

/* Drop cap */
.dropcap::first-letter {
  font-family: 'Cinzel', serif;
  font-size: 3.4em;
  float: left;
  line-height: 0.85;
  margin: 0.05em 0.08em 0 0;
  color: var(--accent);
  font-weight: 700;
}

/* Chips */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: 'Cinzel', serif;
  font-size: 13px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 4px 10px;
  border: 1px solid var(--rule);
  color: var(--ink-2);
  background: transparent;
  cursor: pointer;
  transition: all 150ms;
}

/* v1.7 P39 (CHROME-01): same wash-not-inversion move as .btn:hover. */
.chip:hover {
  background: var(--hover-wash);
  color: var(--ink);
  border-color: var(--border-strong);
}

/* v1.7 P39 (CHROME-01): the SELECTED chip is gilt.
   34-UI-SPEC § Color already reserved --gold for "the active palette/tone/cert
   chip (.chip.active)" — the token existed, the rule never read it, so Session
   Setup's Difficulty and GM Style rows shipped as white slabs on the dark
   palettes. Selected state and primary CTA are one visual language, so they read
   the same triad rather than two near-identical ones. */
.chip.active {
  background: var(--cta-fill);
  color: var(--cta-ink);
  border-color: var(--cta-edge);
}

/* Animations */
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: translateY(0); }
}

.fade-in {
  animation: fadeIn 400ms ease both;
}

@keyframes inkBleed {
  from { opacity: 0; filter: blur(2px); }
  to { opacity: 1; filter: blur(0); }
}

.ink-bleed {
  animation: inkBleed 600ms ease both;
}

/* Dice tumble — applied to .d20-face.tumbling during the rolling phase. */
@keyframes diceTumble {
  0%   { transform: rotate(0deg) scale(1); }
  20%  { transform: rotate(120deg) scale(1.08); }
  40%  { transform: rotate(240deg) scale(0.95); }
  60%  { transform: rotate(360deg) scale(1.05); }
  80%  { transform: rotate(480deg) scale(0.98); }
  100% { transform: rotate(600deg) scale(1); }
}
@keyframes diceGlow {
  0%, 100% { box-shadow: 0 0 0 rgba(0,0,0,0); }
  50%      { box-shadow: 0 0 24px color-mix(in srgb, var(--accent) 65%, transparent); }
}

/* Typewriter cursor at the end of the streaming narration. */
@keyframes cursorBlink {
  0%, 49%   { opacity: 1; }
  50%, 100% { opacity: 0; }
}

@keyframes pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 1; }
}

.thinking-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  margin: 0 2px;
  animation: pulse 1.4s infinite;
}

.thinking-dot:nth-child(2) { animation-delay: 0.2s; }
.thinking-dot:nth-child(3) { animation-delay: 0.4s; }

/* Scene transitions */
@keyframes scenePage {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: translateY(0); }
}

.scene {
  animation: scenePage 500ms cubic-bezier(0.2, 0.6, 0.2, 1) both;
}

/* Narration — the GM's prose. Sized large for comfortable reading; the
   visual-rhythm anchor of the play screen. */
.narration {
  font-family: 'EB Garamond', serif;
  font-size: 24px;
  line-height: 1.7;
  color: var(--ink);
  max-width: 72ch;
}

.narration p {
  margin: 0 0 1em 0;
  text-wrap: pretty;
}

.narration em {
  color: var(--ink-2);
}

/* Letter fragments and quiet emphasis: *phrase* — bold instead of italic
   for readability. */
.narration .letter-fragment {
  font-weight: 600;
  color: var(--ink-2);
  letter-spacing: 0.01em;
}

/* Spoken dialogue: "quoted phrase" — bold accent instead of italic. */
.narration .quoted {
  color: var(--accent);
  font-weight: 600;
}

/* Drop cap on the first paragraph after a scene break */
.narration.dropcap > p:first-of-type::first-letter {
  font-family: 'Cinzel', serif;
  font-size: 3.2em;
  line-height: 0.9;
  float: left;
  padding: 0.08em 0.12em 0 0;
  color: var(--accent);
  font-weight: 600;
}

/* Dialogue — spoken lines */
.dialogue {
  font-style: italic;
  color: var(--ink-2);
  border-left: 2px solid var(--accent);
  padding-left: 14px;
  margin: 12px 0;
}

/* Player action — marginal note framing, scaled larger for readability.
   Uses normal weight (was italic) so the player can scan their own input
   alongside the GM's narration without eye strain. */
.player-action {
  font-family: 'EB Garamond', serif;
  font-weight: 500;
  font-size: 24px;
  line-height: 1.5;
  color: var(--ink);
  padding: 16px 18px 16px 48px;
  position: relative;
  max-width: 72ch;
  border-left: 3px solid var(--accent);
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the
     palette blocks. The token carries the WHOLE gradient because the stop
     percentage itself differs per palette (50% here, 40% in observatory). */
  background: var(--wash-action);
}

.player-action::before {
  content: '✎';
  position: absolute;
  left: 14px;
  top: 10px;
  color: var(--accent);
  font-size: 20px;
  opacity: 0.8;
}

.player-action-tag {
  display: inline-block;
  font-family: 'Cinzel', serif;
  font-style: normal;
  font-size: 12px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--accent);
  margin-right: 10px;
  vertical-align: baseline;
  opacity: 0.75;
}

/* Suggested-actions caption — "Choices   press 1–4 or click" */
.suggested-actions-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  max-width: 640px;
  margin: 4px 0 8px;
}
.suggested-actions-hint {
  font-family: 'EB Garamond', serif;
  font-style: italic;
  font-size: 16px;
  color: var(--ink-faded);
}
.suggested-actions-hint kbd {
  display: inline-block;
  min-width: 18px;
  padding: 1px 5px;
  margin: 0 2px;
  border: 1px solid var(--rule);
  border-radius: 3px;
  /* P34 (THEME-02): consolidated from the per-palette fork — values now in the
     palette blocks. Three properties move together here; the `color` override
     is the one the UI-SPEC consolidation map omitted. */
  background: linear-gradient(180deg, var(--kbd-face), var(--kbd-face-2));
  font-family: 'JetBrains Mono', monospace;
  font-size: 14px;
  font-style: normal;
  color: var(--kbd-ink);
  box-shadow: var(--kbd-shadow);
  text-align: center;
}

/* Suggested-action buttons — the player's choice menu. Single column at all
   widths; each option is a single line that truncates with ellipsis if too
   long. Vertical scroll handles many options. Uniform height per row so the
   list reads as a clean menu, not a wall of differently-sized boxes. */
.suggested-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 6px 0 18px;
  max-width: 720px;
  width: 100%;
}
.suggested-actions > .suggested-action {
  flex: 0 0 auto;
}

.suggested-action {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  width: 100%;
  padding: 14px 18px;
  border: 1px solid var(--rule);
  border-left: 3px solid var(--rule);
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the palette blocks */
  background: var(--wash-suggest);
  color: var(--ink);
  font-family: 'EB Garamond', serif;
  font-size: 22px;
  line-height: 1.4;
  text-align: left;
  cursor: pointer;
  transition: border-color 150ms ease, background 180ms ease, transform 120ms ease, box-shadow 150ms ease;
  appearance: none;
}

/* Primary row: number badge + action text. Text wraps freely so the player
   can always read the full sentence; we no longer truncate. */
.suggested-action-row.primary {
  display: flex;
  align-items: flex-start;
  gap: 14px;
}

/* Meta row: trigger badges + DC + skill. Sits below the action text. Hidden
   when there are no triggers and no DC. Indented under the action text so
   it visually attaches to the choice rather than to the number badge. */
.suggested-action-row.meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
  padding-left: 50px; /* aligns with the action text under the number badge */
  font-family: 'Cinzel', serif;
  font-size: 15px;
  letter-spacing: 0.14em;
  color: var(--ink-faded);
}

.suggested-action-dc {
  font-family: 'Cinzel', serif;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
  padding: 2px 8px;
  border: 1px solid var(--accent);
  border-radius: 2px;
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the palette blocks */
  background: var(--tint-flat-dc);
}
.suggested-action-dc-skill {
  font-style: normal;
  color: var(--ink-2);
}

.suggested-action:hover {
  border-color: var(--accent);
  border-left-color: var(--accent);
  /* P34 (THEME-02): consolidated from the per-palette fork — values now in the palette blocks */
  background: var(--wash-suggest-hover);
  transform: translateX(2px);
  box-shadow: var(--shadow-hover);
}

.suggested-action:active {
  transform: translateX(2px) scale(0.995);
}

.suggested-action-num {
  flex: 0 0 auto;
  font-family: 'Cinzel', serif;
  font-size: 15px;
  letter-spacing: 0.18em;
  color: var(--accent);
  padding: 4px 9px;
  border: 1px solid var(--rule);
  border-radius: 2px;
  min-width: 28px;
  text-align: center;
  transition: border-color 150ms, color 150ms;
}

.suggested-action:hover .suggested-action-num {
  border-color: var(--accent);
  color: var(--paper);
  background: var(--accent);
}

.suggested-action-text {
  flex: 1 1 auto;
  font-style: normal;
  font-weight: 500;
  /* Wrap freely — the player must be able to read the whole choice. The
     two-row layout pushes meta info below, so the action text owns the
     primary row regardless of length. */
  white-space: normal;
  overflow-wrap: break-word;
  min-width: 0;
}

/* P14 (D-05, D-07): trigger-badge — bracketed inline prefix for chips
   carrying a triggers[] array. Brackets are part of the rendered text
   content (not pseudo-elements) so screen readers read "Magic Watch"
   inside brackets naturally. Per-type color via [data-trigger-type].
   No background, no border — color + bracket is enough differentiation
   on the existing chip card surface. */
.trigger-badge {
  font-family: 'Cinzel', serif;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.14em;
  /* Sits in the meta row under the action text; per-type color applied below. */
}
.trigger-badge[data-trigger-type="item"]         { color: var(--trigger-item); }
.trigger-badge[data-trigger-type="skill"]        { color: var(--trigger-skill); }
.trigger-badge[data-trigger-type="knowledge"]    { color: var(--trigger-knowledge); }
.trigger-badge[data-trigger-type="stat"]         { color: var(--trigger-stat); }
.trigger-badge[data-trigger-type="relationship"] { color: var(--trigger-relationship); }

/* P18 (D-03): xp-preview-badge — chip meta-row XP gain indicator. Same
   typographic family as trigger-badge but a distinct sage-green color
   (--trigger-xp). No bracket characters; the +N is enough visual distinction. */
.xp-preview-badge {
  font-family: 'Cinzel', serif;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.14em;
  color: var(--trigger-xp);
}

/* Mobile chip sizing — slightly smaller padding but same single-column layout.
   Single-line truncation continues to apply. */
@media (max-width: 768px) {
  .suggested-action { font-size: 20px; padding: 12px 14px; min-height: 52px; }
  .suggested-action-num { font-size: 14px; min-width: 24px; padding: 3px 7px; }
}

/* Scene title — chapter-break feel with rule above */
.scene-title-block {
  text-align: center;
  margin: 40px auto 18px;
  padding-top: 28px;
  border-top: 1px solid var(--rule);
  max-width: 68ch;
}

.scene-title-text {
  font-family: 'Cinzel', serif;
  font-size: 17px;
  letter-spacing: 0.35em;
  color: var(--accent);
  text-transform: uppercase;
  margin-bottom: 12px;
}

/* Roll */
.roll {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  font-family: 'Cinzel', serif;
  font-size: 14px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 6px 12px;
  border: 1px solid var(--rule);
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the palette blocks */
  background: var(--roll-bg);
  color: var(--ink);
  margin: 8px 0;
}

.roll .result {
  font-size: 18px;
  font-weight: 700;
  color: var(--accent);
  letter-spacing: 0;
}

.roll .verdict {
  font-size: 13px;
  color: var(--ink-faded);
}

.roll.crit .result { color: var(--gold); }
.roll.fail .result { color: var(--verdict-fail); }

/* ---------- Rich roll card (v1.3 dice overhaul) ---------- */
/* Replaces the slim single-line .roll badge with a HUD-style readout that
   shows the d20 face, every modifier as a labeled chip, the running total,
   and the verdict in color. Verdict-specific gradients live below. */
.roll-card {
  --rc-accent: var(--accent);
  --rc-fill:   color-mix(in srgb, var(--accent) 14%, var(--paper));
  --rc-text:   var(--ink);
  display: block;
  margin: 18px 0;
  padding: 22px 24px;
  border: 2px solid var(--rule);
  border-left: 6px solid var(--rc-accent);
  border-radius: 4px;
  background: linear-gradient(135deg, var(--rc-fill), transparent 70%);
  color: var(--rc-text);
  /* Stretch to fill the feed area — no fixed cap. The feed parent already
     caps width sensibly (780px max), so the card naturally fills it. */
  width: 100%;
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the palette blocks */
  box-shadow: var(--shadow-card);
  position: relative;
  overflow: hidden;
}
/* P34 (THEME-02): the five verdict colours were hardcoded and palette-blind at
   these sites. They now resolve through the verdict tokens so the dark palettes
   can lift them; the mix percentages and the second blend argument are untouched.
   REVIEW WR-01: crit-success is the ONE row whose border and fill were never the
   same colour before P34 — the border took var(--gold) and the fill mixed a
   literal #c9a341. --verdict-crit-fill carries that literal for the legacy
   palettes and follows --verdict-crit in the dark ones, so the row stays
   pixel-identical in parchment and observatory. Do not collapse the two names. */
.roll-card.success      { --rc-accent: var(--verdict-success); --rc-fill: color-mix(in srgb, var(--verdict-success) 14%, var(--paper)); }
.roll-card.partial      { --rc-accent: var(--verdict-partial); --rc-fill: color-mix(in srgb, var(--verdict-partial) 14%, var(--paper)); }
.roll-card.fail         { --rc-accent: var(--verdict-fail); --rc-fill: color-mix(in srgb, var(--verdict-fail) 12%, var(--paper)); }
.roll-card.crit-success { --rc-accent: var(--verdict-crit); --rc-fill: color-mix(in srgb, var(--verdict-crit-fill) 22%, var(--paper)); }
.roll-card.crit-fail    { --rc-accent: var(--verdict-crit-fail); --rc-fill: color-mix(in srgb, var(--verdict-crit-fail) 18%, var(--paper)); }

.roll-card-header {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.roll-card-label {
  font-family: 'Cinzel', serif;
  font-size: 17px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--rc-accent);
}
.roll-card-tag {
  font-family: 'Cinzel', serif;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 2px;
  border: 1px solid var(--rc-accent);
  color: var(--rc-accent);
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the
     palette blocks. SIGN-INVERTED against the row washes: parchment lightens
     this dice chrome, observatory darkens it, hence its own raised-tint token. */
  background: var(--tint-raised);
}
.roll-card-tag.adv      { background: color-mix(in srgb, var(--verdict-success) 15%, transparent); border-color: var(--verdict-success); color: var(--verdict-success); }
.roll-card-tag.dis      { background: color-mix(in srgb, var(--verdict-fail) 15%, transparent); border-color: var(--verdict-fail); color: var(--verdict-fail); }
.roll-card-tag.rerolled { background: color-mix(in srgb, var(--verdict-crit) 18%, transparent); border-color: var(--verdict-crit); color: var(--verdict-crit); }

/* Dice row — d20 face shown as a large numeric tile, with the discarded
   die (if advantage/disadvantage) rendered smaller and dimmed alongside.
   Modifier chips trail to the right so the player can scan the math. */
.roll-card-dice {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.d20-face {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-width: 72px;
  min-height: 72px;
  padding: 10px 14px;
  border: 2px solid var(--rc-accent);
  border-radius: 4px;
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the
     palette blocks. SIGN-INVERTED dice chrome, same as the roll-card tag. */
  background: var(--tint-raised-soft);
  position: relative;
}
.d20-face.secondary {
  min-width: 52px;
  min-height: 52px;
  opacity: 0.5;
  border-style: dashed;
}
.d20-face.tumbling {
  animation: diceTumble 1100ms cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite,
             diceGlow 1100ms ease-in-out infinite;
}
.d20-face.tumbling .d20-num {
  font-size: 32px;
  color: var(--accent);
  animation: pulse 600ms ease-in-out infinite;
}

/* Rolling phase visual cue on the card itself. */
.roll-card.rolling {
  --rc-accent: var(--accent);
  --rc-fill:   color-mix(in srgb, var(--accent) 12%, var(--paper));
}
.roll-card-tag.rolling-tag {
  border-color: var(--accent);
  color: var(--accent);
  animation: pulse 1100ms ease-in-out infinite;
}

/* Typewriter cursor — sits at the cut point in the streaming narration. */
.narration-cursor {
  display: inline-block;
  margin-left: 2px;
  font-weight: 400;
  color: var(--accent);
  animation: cursorBlink 600ms steps(1, end) infinite;
}

/* Reveal-mode feed: cursor shows the click is interactive (skip). */
.feed.revealing { cursor: pointer; }
.feed.revealing::after {
  content: 'Click anywhere to skip · Esc / Enter';
  position: sticky;
  bottom: 8px;
  display: block;
  width: fit-content;
  margin: 12px auto 0;
  padding: 6px 14px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--paper);
  font-family: 'Cinzel', serif;
  font-size: 14px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faded);
  pointer-events: none;
  opacity: 0.85;
}
.d20-num {
  font-family: 'Cinzel', serif;
  font-size: 36px;
  font-weight: 700;
  line-height: 1;
  color: var(--rc-accent);
}
.d20-face.secondary .d20-num { font-size: 24px; }
.d20-tag {
  font-family: 'Cinzel', serif;
  font-size: 12px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-faded);
  margin-top: 4px;
}

.roll-card-mods {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.mod-chip {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  padding: 6px 12px;
  border: 1px solid var(--rule);
  border-radius: 3px;
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the
     palette blocks. SIGN-INVERTED dice chrome, same as the roll-card tag. */
  background: var(--tint-raised-chip);
  min-width: 56px;
}
.mod-chip.negative {
  border-color: var(--verdict-fail);
}
.mod-chip-value {
  font-family: 'JetBrains Mono', monospace;
  font-size: 20px;
  font-weight: 600;
  color: var(--rc-accent);
  line-height: 1;
}
.mod-chip.negative .mod-chip-value { color: var(--verdict-fail); }
.mod-chip-label {
  font-family: 'Cinzel', serif;
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faded);
  margin-top: 4px;
  white-space: nowrap;
}

/* Result row — compact equation followed by a big total + verdict pill. */
.roll-card-result {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  padding-top: 12px;
  border-top: 1px solid var(--rule);
}
.roll-card-equation {
  flex: 1 1 auto;
  font-family: 'JetBrains Mono', monospace;
  font-size: 17px;
  color: var(--ink-2);
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px;
  min-width: 0;
}
.eq-d20      { font-weight: 700; color: var(--rc-accent); font-size: 18px; }
.eq-op       { color: var(--ink-faded); padding: 0 2px; }
.eq-mod      { font-weight: 600; }
.eq-mod-label{ color: var(--ink-faded); font-size: 15px; }
.eq-eq       { color: var(--ink-faded); padding: 0 4px; }

.roll-card-total {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-shrink: 0;
}
.total-num {
  font-family: 'Cinzel', serif;
  font-size: 32px;
  font-weight: 700;
  color: var(--rc-accent);
  line-height: 1;
}
.total-vs {
  font-family: 'Cinzel', serif;
  font-size: 14px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faded);
}
.roll-card-verdict {
  font-family: 'Cinzel', serif;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  padding: 6px 14px;
  border-radius: 2px;
  background: var(--rc-accent);
  color: var(--paper);
  flex-shrink: 0;
}
/* P34 (THEME-02): the verdict banner is a roll-card verdict site too — its
   colours resolve through the verdict tokens so the dark palettes can lift them. */
.roll-card-verdict.crit-success {
  background: linear-gradient(135deg, var(--verdict-crit), color-mix(in srgb, var(--verdict-crit) 70%, white));
  color: #2a1a08;
}
.roll-card-verdict.crit-fail {
  background: linear-gradient(135deg, var(--verdict-crit-fail), var(--verdict-fail));
}

.roll-card-actions {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--rule);
}
.btn-reroll {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 18px;
  border: 1px solid var(--gold);
  border-radius: 3px;
  background: color-mix(in srgb, var(--gold) 14%, var(--paper));
  color: var(--ink);
  font-family: 'Cinzel', serif;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 150ms ease, transform 120ms ease;
}
.btn-reroll:hover {
  background: color-mix(in srgb, var(--gold) 28%, var(--paper));
  transform: translateY(-1px);
}
.btn-reroll-icon {
  font-size: 18px;
  color: var(--gold);
}

/* ---------- Inspiration UI ---------- */
.inspiration-block {
  /* lives inside .side-pane > Self tab, as the fourth row of the vitals */
}
/* AAA SURFACES — inspiration is a row like the three vitals above it, not a
   tinted box: five gilt stars (lit / unlit) and the count in serif numerals. */
.inspiration-pips {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
.inspiration-pip {
  display: inline-flex;
  line-height: 1;
  color: color-mix(in srgb, var(--gold) 45%, transparent);
  transition: color var(--dur-base) var(--ease-out);
}
.inspiration-pip.filled {
  color: var(--gold);
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--gold) 45%, transparent));
}
.inspiration-pip.filled svg path { fill: currentColor; }
.inspiration-count { margin-left: auto; }

.inspire-bar {
  display: flex;
  justify-content: flex-end;
  margin: 6px 0 4px;
}
.btn-inspire {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border: 1px solid var(--gold);
  border-radius: 3px;
  background: transparent;
  color: var(--ink-2);
  font-family: 'Cinzel', serif;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 150ms ease, color 150ms ease;
}
.btn-inspire:hover {
  background: color-mix(in srgb, var(--gold) 12%, transparent);
  color: var(--ink);
}
.btn-inspire.active {
  background: color-mix(in srgb, var(--gold) 22%, var(--paper));
  color: var(--ink);
  font-weight: 600;
  box-shadow: 0 0 8px color-mix(in srgb, var(--gold) 40%, transparent);
}
/* REVIEW (iteration 3) CR-01 — the full label is the default and the short one is the
   exception, so every surface that is NOT the capped play-screen strip keeps today's
   wording byte-for-byte. `display: none` (not visibility/clip) so the unused span leaves
   the accessibility tree as well as the box tree and a screen reader is never offered
   both spellings of the same control. The swap happens in the compact arm of "THE STRIP
   BUDGET". */
.btn-inspire-label-short { display: none; }

.btn-inspire-icon {
  font-size: 17px;
  color: var(--gold);
}
.btn-inspire-count {
  font-family: 'JetBrains Mono', monospace;
  letter-spacing: 0;
  color: var(--gold);
}

@media (max-width: 768px) {
  .roll-card { padding: 14px 16px; }
  .d20-face { min-width: 60px; min-height: 60px; }
  .d20-num { font-size: 30px; }
  .total-num { font-size: 26px; }
}

/* Stat block */
.stat-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 10px 0;
  border-bottom: 1px dotted var(--rule);
  font-family: 'Cinzel', serif;
  font-size: 16px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.stat-row .value {
  font-family: 'EB Garamond', serif;
  font-size: 24px;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--ink);
  text-transform: none;
}

/* Meter */
.meter {
  height: 6px;
  background: var(--paper-deep);
  border: 1px solid var(--rule);
  position: relative;
  overflow: hidden;
}

.meter-fill {
  height: 100%;
  background: var(--accent);
  transition: width 400ms ease;
}

/* AAA design system — the three resources speak the colour ROLES (DESIGN.md §2):
   health is the danger red (low health IS danger), mana is the cool magic blue,
   stamina is quiet ink. Gold stays reserved for agency; the rose accent and the
   sea-green are retired from resource bars. */
/* r2: the four-job palette has no blue and no grey-that-reads-as-disabled.
   Mana and stamina share ONE brass fill (identity is the icon); health is a
   muted rose that only turns full danger red when low (see .is-low). */
.meter-fill.hp { background: var(--rose-muted); }
.meter-fill.mana { background: var(--brass); }
.meter-fill.stam { background: var(--brass); }

/* Modal */
.modal-bg {
  position: fixed;
  inset: 0;
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the palette blocks */
  background: var(--scrim);
  backdrop-filter: blur(3px);
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  animation: fadeIn 250ms ease both;
}

.modal {
  /* P34 (THEME-02): the opaque paper surface moves onto --panel-bg, which
     resolves to var(--paper) in parchment and observatory (byte-identical
     render) and to var(--surface) in the two glass palettes. */
  background: var(--panel-bg);
  /* P34 (THEME-02): frosted glass. --panel-backdrop is `none` in parchment and
     observatory, so this is a no-op there and no component acquires a palette
     fork. Both prefixes, and the sketch's own var(--x, none) fallback shape. */
  -webkit-backdrop-filter: var(--panel-backdrop, none);
          backdrop-filter: var(--panel-backdrop, none);
  /* v1.7 P39 (CHROME-02): --border-strong, was --ink. P34 moved the SURFACE onto
     a token and left the border on the ink ramp, which put a 1px near-white
     outline around a frosted panel on all three dark palettes — the modal read as
     a cut-out rather than as a raised sheet. --border-strong is the tier the
     contract already defines for exactly this (a panel edge one step up from
     --border-soft), and --shadow-lg is what does the lifting now.
     Border WIDTH is untouched: .modal is in the cross-palette geometry gate's
     BOX_SELECTORS, and a 2px edge would move its box. */
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-lg);
  max-width: 640px;
  width: 100%;
  padding: 36px;
  position: relative;
  max-height: 86vh;
  max-height: 86dvh;
  overflow-y: auto;
  box-shadow: var(--shadow);
}
/* The shared dialog title — the size that used to be an inline style on the h2
   (components.jsx Modal), so a screen's own dialog class can restyle it. */
.modal .modal-title { font-size: 20px; }
/* The dialog sheet is focusable (tabIndex -1) so long dialogs can take focus on
   open; the sheet itself is not a control and never paints a ring. */
.modal:focus, .modal:focus-visible { outline: none; }

/* Screen-reader-only text (a dialog's name when the dialog draws its own
   heading, a clarifying clause that the visual layout already implies). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Tweaks panel — v1.9: a settings sheet hanging from the cog. Geometry, rows
   and options live in the FRONT DOOR block; the surface stays here because
   --panel-bg / --panel-backdrop document it (see the palette blocks above). */
.tweaks-panel {
  position: fixed;
  z-index: 90;
  /* P34 (THEME-02): see .modal — --panel-bg is var(--paper) in the two shipped
     palettes and var(--surface) in the two glass ones. */
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop, none);
          backdrop-filter: var(--panel-backdrop, none);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-lg);
}

/* Utilities */
.flex { display: flex; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: 8px; }
.gap-3 { gap: 12px; }
.gap-4 { gap: 16px; }
.gap-6 { gap: 24px; }
.mt-2 { margin-top: 8px; }
.mt-4 { margin-top: 16px; }
.mt-6 { margin-top: 24px; }
.mt-8 { margin-top: 32px; }
.mb-2 { margin-bottom: 8px; }
.mb-4 { margin-bottom: 16px; }
.mb-6 { margin-bottom: 24px; }
.mb-8 { margin-bottom: 32px; }
.w-full { width: 100%; }
.text-center { text-align: center; }
.text-muted { color: var(--ink-faded); }
.text-accent { color: var(--accent); }

/* Layout */

/* ⚠ REVIEW CR-01 — `.app-shell` HAS NO CALL SITE. play.jsx's shell root used to be
   `<div className="app-shell play-shell">`; the `app-shell` half was removed because
   `min-height: 100vh` below applied to the SAME element as .play-shell's
   `height: 100dvh`. Those are different properties, so there is no cascade contest —
   the used height was max(100vh, 100dvh), i.e. taller than the visible viewport on
   any browser with a collapsing URL bar, and html[data-screen="play"] #root's
   `overflow: hidden` then clipped the composer dock away with no scrollbar.
   DO NOT put this class back on the play shell root. test/v17-shell-contract.test.js
   A1 reads the shell root's className out of play.jsx and will go red if you do.
   The rules are kept (not deleted) only because they are inert with no call site;
   `.app-shell.solo` was already dead before this phase.                            */
.app-shell {
  display: grid;
  grid-template-columns: 1fr 320px;
  min-height: 100vh;
  gap: 0;
}

.app-shell.solo {
  grid-template-columns: 1fr;
}

.main-pane {
  padding: 48px 56px 64px;
  position: relative;
}

.side-pane {
  border-left: 1px solid var(--rule);
  /* P34 (THEME-02): consolidated from the per-palette fork — value now in the palette blocks */
  background: var(--pane-bg);
  /* P34 (THEME-02): frosted glass. No-op under parchment and observatory
     (--panel-backdrop: none). ⚠ backdrop-filter makes this element a
     containing block for `position: fixed` DESCENDANTS and opens a new
     stacking context. .side-pane hosts the mobile drawer, so if the
     cross-palette bounding-box comparison ever flags drift on a fixed
     element, THIS is the first backdrop to remove — do not relax the
     geometry assertion instead. */
  -webkit-backdrop-filter: var(--panel-backdrop, none);
          backdrop-filter: var(--panel-backdrop, none);
  padding: 28px 24px;
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
  overflow-y: auto;
}

/* ============================================================================
   v1.7 P35 SHELL-01/SHELL-02 — THE SHELL SCROLL CONTRACT
   ----------------------------------------------------------------------------
   Ported in intent from .claude/skills/sketch-findings-TRPG/references/
   layout-chrome.md:71-93 and sources/005-full-turn-loop/index.html:12,39-53,89-92.

   Everything below is either scoped by html[data-screen="play"] (set by the
   resolvedScreen effect in app.jsx) or keyed on the .play-shell / .shell-* class
   names, which only exist on the play screen's render tree. Menu, session setup,
   both wizards and every modal keep today's document scroll exactly — that is
   SHELL SC5, and the fail-safe direction is "attribute absent -> every rule here
   is inert -> the document scrolls". There is deliberately NO pre-paint <head>
   script for data-screen (unlike data-palette): resolving it before paint needs
   three storage keys plus auth state, and getting it wrong LOCKS the page.
   ============================================================================ */

/* 1. DOCUMENT LOCK. The page can never scroll on the play screen — the shell is
      clamped to the viewport and the feed is the only scroller.
      ONE unit chain across html / body / #root / .play-shell (35-RESEARCH
      Pitfall 7): 100vh fallback FIRST, 100dvh override SECOND. Mixing `100%` on
      one and `100dvh` on another silently clips the dock on a real phone with a
      collapsing URL bar while the headless harness — which has no browser
      chrome — stays green. See also the html/body note at styles.css:796-805.  */
html[data-screen="play"],
html[data-screen="play"] body {
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}
/* No rubber-band on the document itself; the feed keeps its own bounce
   (overscroll-behavior: contain, rule 7) because Phase 37 reads those deltas. */
html[data-screen="play"] body { overscroll-behavior: none; }

/* 2. #root NEUTRALISE. The shipped rule at styles.css:859-864 gives #root
      `min-height: 100vh/100dvh`. Left alone, the shell can exceed the viewport
      even with a correct .play-shell height, and #root's own overflow: hidden
      then clips the bottom of the shell — i.e. the composer dock — with no CSS
      to blame. Same unit chain as rule 1 (Pitfall 7).
      NOT merged into the #root rule at :859: that one is global and must keep
      its min-height for every non-play screen.                                */
html[data-screen="play"] #root {
  height: 100vh;
  height: 100dvh;
  min-height: 0;
  overflow: hidden;
}

/* 3. THE SHELL. `height`, NEVER a minimum-height — this is the single
      highest-risk line in the phase and the exact v1 composition failure
      recorded in layout-chrome.md:73. MEASURED (Chrome 147, 375x667, 3000px of
      feed): with `min-height: 100dvh` the shell grows to 3084 and the dock lands
      at top: 3042 — off-screen and unreachable — WHILE THE HEADER STAYS AT
      top: 0. That is why the dock's rect, not the header's, is the detector, and
      why test/v17-shell-contract.test.js A1 exists at all.
      grid-template-columns: none — .app-shell's `1fr 320px` moves down to
      .shell-body (rule 5) so the rail sits inside the bounded middle row.
      NOTHING here may declare backdrop-filter / filter / transform / perspective
      / will-change / contain(layout|paint|content|strict): any of them makes the
      shell a containing block for its `position: fixed` descendants — the mobile
      drawer, the tab bar and every modal. MEASURED as UNDETECTABLE by a rendered
      drawer-geometry check on .play-shell (drawer bottom 667 both ways) because
      the shell's box coincides with the viewport, so the source-grep assertion
      A4 is the only enforcement that exists. Frosting belongs on .shell-hdr and
      .shell-dock, which are not ancestors of the drawer (plan 35-06).          */
.play-shell {
  /* REVIEW CR-05 — the rail's track width, declared ONCE. .shell-body's column grid
     (rule 5) and the dock's rail reservation are two expressions of the same number
     and silently misalign the composer against the prose column if they drift. A
     custom property is inert for A4's purposes (it forms no containing block). */
  --shell-rail-w: 320px;
  position: relative;
  height: 100vh;
  height: 100dvh;
  display: grid;
  grid-template-rows: auto 1fr auto;
  grid-template-columns: none;
  overflow: hidden;
}

/* 4. NAMED FLOW CHILDREN. The three rows are listed EXPLICITLY. Never write a
      blanket descendant positioning rule (`.play-shell > *`, `.play-shell *`) at
      ANY position in this file. REPRODUCED (Chrome 147): a blanket rule placed
      after .shell-frame wins on source order at equal specificity (0,1,0), turns
      the absolutely-positioned frame into a flow child — frame position: relative,
      height 4px (borders only) — and shoves the header to top: 4, where it then
      CLIMBS as prose fills the 1fr row. Placed BEFORE .shell-frame the same rule
      does not reproduce, so the guard is "never write it", not "write it earlier".
      z-order: header/body 2, dock 3 (the dock paints over the prose so its top
      border and upward shadow read, and Phase 37's `bottom: 100%` overlay panel
      already has a positioned ancestor above the feed). The frame lands at z 4 in
      plan 35-06; the texture layers stay at z 0 behind #root (z 1).            */
.shell-hdr, .shell-body, .shell-dock { position: relative; }
.shell-hdr { z-index: 2; }
.shell-body { z-index: 2; }
.shell-dock { z-index: 3; }

/* 5. THE MIDDLE ROW. min-height: 0 AND overflow: hidden are REDUNDANT by
      measurement — either one alone prevents the collapse and the failure only
      reproduces when BOTH are absent (dock top 3042 vs 625). Both stay anyway, so
      that a future edit removing one cannot silently regress the other into being
      load-bearing without anyone noticing. A grid item's default `min-height:
      auto` is what refuses to shrink below its content.
      The 1fr/320px column grid lives here, not on .play-shell.                 */
.shell-body {
  min-height: 0;
  overflow: hidden;
  display: grid;
  /* 320px, via .play-shell's --shell-rail-w — the dock reserves the same track to keep
     the composer under the prose column (REVIEW CR-05). Change it in ONE place. */
  grid-template-columns: 1fr var(--shell-rail-w);
}

/* 6. THE PROSE COLUMN. Scoped with html[data-screen="play"] DELIBERATELY, not
      written as a bare `.shell-main`: the @media (max-width: 768px) block later
      in this file sets `.main-pane { min-height: 100dvh; padding: ... }` at equal
      specificity (0,1,0) and LATER source order, so a bare .shell-main rule would
      lose and hand the middle row a 100dvh floor on every phone — Pitfall 1,
      reproduced at exactly the breakpoint SHELL-04 cares about.
      The BASE .main-pane rule (:2012-2015) is untouched and stays frozen: it is
      shared with auth.jsx, app.jsx:219, app.jsx:309, components.jsx and all three
      wizards.                                                                   */
html[data-screen="play"] .shell-main {
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  padding: 0;
}

/* 7. THE SOLE SCROLLER. Owned entirely here rather than half-inline in play.jsx:
      the shell needs flex, min-height, overflow-y, overscroll-behavior and
      scrollbar-width on the SAME element, and split ownership invites drift (an
      inline style also beats every rule in this file).
      overscroll-behavior is `contain`, NOT `none`: Phase 37's gesture contract
      reads the overscroll deltas and `none` kills the bounce it detects
      (35-UI-SPEC § Downstream Contract).
      NEVER set overflow-anchor: none here or on any child. MEASURED: Chrome's
      default scroll anchoring moved scrollTop 801 -> 1301 when 500px was
      prepended, holding the reader's visual position exactly — which is what
      makes "Show earlier" survive a no-snap policy.                            */
.play-shell .feed {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}
/* The feed became tabIndex={0} in plan 35-05 — a scrollable region must be
   keyboard-reachable (WCAG 2.1.1) — so it needs a visible focus ring. Inset,
   because the scroller is flush with its row. */
.play-shell .feed:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: -2px;
}

/* 8. THE RAIL. .side-pane's base rule (:2017-2036) is `position: sticky;
      height: 100dvh` — correct when the PAGE scrolls, wrong inside a grid row
      that is shorter than the viewport by header + dock.
      THE @media (min-width: 769px) BRACKET IS LOAD-BEARING (Pitfall 8): the
      @media (max-width: 768px) drawer block later in this file converts
      .side-pane to `position: fixed` and hosts the ENTIRE mobile drawer. This
      override carries html[data-screen="play"] and would therefore beat it on
      specificity regardless of source order — unbracketed, it silently kills the
      drawer, which reads to a player as "the Ledger button does nothing at
      375px" and is the only ledger access a phone has.                          */
@media (min-width: 769px) {
  html[data-screen="play"] .side-pane {
    position: static;
    height: auto;
    min-height: 0;
    overflow-y: auto;
  }
}

/* 9. THE COMPOSER, now a child of the dock rather than the tail of the prose
      column. The @media (max-width: 768px) `.play-composer { position: sticky }`
      rule later in this file justifies itself with "The page is the scroll
      surface (no inner scroll container)" — a statement the shell FALSIFIES.
      Inside .shell-dock it would offset the composer against the wrong scroll
      surface and stack a second border + opaque plate against the dock's own.
      The base rule is OVERRIDDEN, not deleted: it has one call site and removing
      it is a different, unnecessary risk this phase.                            */
html[data-screen="play"] .shell-dock .play-composer {
  position: static;
  background: transparent;
  border-top: none;
  margin-top: 0;
}

/* 10. PHONE. Two rules, both consequences of the restructure rather than new
       design. (a) The column grid moved from .app-shell to .shell-body, so the
       existing `.app-shell { grid-template-columns: 1fr }` at <=768px no longer
       reaches it — without this the shell reserves a dead 320px track for a rail
       that is `position: fixed` at this breakpoint, squeezing the prose to
       calc(100% - 320px) at 375px. (b) .shell-main zeroes .main-pane's padding,
       so the mobile side inset (16px) is restated here; combined with the feed's
       inline paddingRight: 8 this reproduces today's 16/24 insets exactly.
       Deliberately NOT the 860px meter-wrap bracket — that is a separate
       breakpoint and belongs to plan 35-06.                                     */
@media (max-width: 768px) {
  html[data-screen="play"] .shell-body { grid-template-columns: 1fr; }
  html[data-screen="play"] .shell-main { padding-left: 16px; padding-right: 16px; }
}

/* 11. THE STRIP BUDGET — REVIEW CR-03 / D-35-04. THE READING COLUMN GETS A FLOOR.
       .shell-main is `display: flex; flex-direction: column` with THREE children:
       .feed, the turn-summary card, and the chip/inspire strip. A column flex
       item's default `min-height: auto` refuses to shrink below its content, and
       .feed was the ONLY child declaring `min-height: 0` (rule 7) — so .feed was
       the only child that yielded, all the way down.
       MEASURED (headless Chromium, 375x667, this build, 4 chips + a summary card):
       header 141.4 + dock 253 leaves a 272.6px middle row; the strip's content
       wanted 491.6px and the card 288px, and .feed collapsed to 16px — 2.4% of the
       viewport, i.e. NO NARRATION ON SCREEN. That is D-35-04 exactly.
       THE CHIPS STAY PINNED BELOW THE FEED (locked 35-CONTEXT decision, user-decided
       2026-07-28; Phase 37's overlay panel is what finally absorbs them). The fix is
       therefore to BOUND the two non-prose siblings and let them scroll internally.

       WHY PERCENTAGES AND NOT dvh. The obvious spelling is `max-height: 34dvh`, and
       it does almost nothing here: 34dvh is 227px at 667, but the middle row these
       items live in is only 272.6px, so the cap would land above the box it is meant
       to bound and .feed would still starve. A percentage resolves against
       .shell-main's own content box, so the budget follows the middle row however
       tall the header and dock turn out to be — and both of those are variable
       (the header wraps to a third row at <=860px, the dock reserves the 68px tab
       bar plus env(safe-area-inset-bottom)).

       WHY 26% + 16% AND NOT SOMETHING ROUNDER. These were TUNED against the probe,
       not guessed. 32% + 16% + the card's margins measured a 98.9px feed — better
       than 16px, but .feed also carries .scroll-fade, whose mask ramps 24px at BOTH
       ends, so a 98.9px column renders only ~50px at full alpha and the top of the
       narration still reads as washed out. 26% + 16% leaves .feed >= ~50% of the
       middle row (MEASURED 142.6px at 375x667), which clears both ramps with real
       prose between them. Raising either cap trades directly against that.

       min-height: 0 is what makes the caps reachable at all — without it the default
       `auto` still floors each item at its content height and max-height never binds.
       overscroll-behavior: contain on the strip keeps a chip-list flick from chaining
       into the feed behind it (the feed keeps its own contain, rule 7).
       T4's SHELL_PHONE_METERS_WRAP_AND_DRAWER now ASSERTS the resulting floor rather
       than printing it as evidence — see REVIEW WR-09.

       THE BUDGET IS UNBRACKETED — REVIEW (iteration 2) CR-01. This block SHIPPED
       inside `@media (max-width: 768px)` and that was the bug: NOTHING in the
       mechanism above is width-dependent. .shell-main is a bounded flex column at
       EVERY width (rule 6), .feed is the only child carrying min-height: 0 at every
       width (rule 7), so above 768px the siblings starved the reading column exactly
       as they did at 375. MEASURED on the bracketed build (headless Chromium, 3 chips
       + a summary card): 800x600 -> .feed 40px (11.8% of a 340px middle row);
       1024x768 -> 40px (7.4%); 1280x800 -> 40px (7.0%); 1400x600 -> 40px (10.7%);
       1400x900 -> 40px (5.9%); 812x375 -> 40px (34.8%). The phone segment stayed
       green throughout because it only ever runs at 375x667 and 375x812 — the two
       widths where the caps applied.
       40px is below .scroll-fade's two 24px mask ramps (:2924-2927), so those pixels
       render at partial alpha end to end: a laptop showing no legible narration.
       The percentages resolve against .shell-main, which already tracks whatever the
       middle row turns out to be, so ONE unbracketed pair covers phone, tablet and
       desktop. Only the card's margin still needs a breakpoint arm (below).
       Do not re-bracket these. T4's SHELL_CHROME_FIXED_ACROSS_TURNS clause 6 now
       samples .feed against the middle row on the DESKTOP loop as well, so a third
       regression goes red instead of silent.

       THE 100px ABSOLUTE FLOOR IS UNREACHABLE ON A LANDSCAPE PHONE, AND THAT IS A
       CHROME BUDGET PROBLEM, NOT A CAP PROBLEM — the reviewer's secondary point,
       DECIDED here rather than left for the next reader to re-derive. MEASURED at
       812x375: header + dock consume 260 of 375px, leaving a 115px middle row; at
       667x375 they consume 332 and leave 43px. A percentage cap cannot rescue a row
       that small — 100px of prose would require the feed to be 87% of 115px with two
       siblings and their margins still in the box. What the caps CAN deliver there is
       the RATIO floor, and the short-viewport margin arm below is what gets 812x375
       over it (MEASURED: .feed 40px / 34.8% -> 48px / 41.7% of the middle row).
       667x375 stays unusable at ~24px and is ACCEPTED AS SUCH: the fix is a smaller
       header and dock, which is a chrome redesign and is Phase 37's slim-dock work,
       not something to smuggle in behind a max-height. T4 asserts the 100px + 40%
       floors on the PORTRAIT phone segment and the 40% floor on the desktop loop;
       landscape phone is deliberately asserted nowhere.                             */

/* The summary card's own margin, lifted VERBATIM out of play.jsx's inline style. It had
   to move: a margin sits OUTSIDE max-height, so those 32px came straight off the feed's
   share of the middle row — and an inline style beats every rule in this file (the A11
   lesson), so the overrides below could not reach it. Byte-identical to the old inline
   value on a tall desktop; `auto` keeps the 780px measure centred.                   */
/* REVIEW (iteration 3) CR-01 — the PADDING follows the margin out of play.jsx, one
   iteration later and for the same reason. 32 of the 43.6px the 16% cap renders at
   375x667 were this padding, so the short-middle-row arm below could not make the card
   smaller without leaving a box that is nothing BUT padding. It beats the base
   `.card { padding: 24px }` rule on source order at equal specificity — the same
   arrangement that already held while the value was inline.
   NOTE ON CITATIONS: this block names RULES and HEADINGS, not line numbers. REVIEW
   (iteration 3) IN-02 found four of the pointers added last iteration aimed at the wrong
   lines within one commit of being written; a selector does not drift the way a line
   number does.                                                                       */
.turn-summary-card { margin: 24px auto 8px; padding: 16px 20px; }

/* THE CAP IS A CEILING, NOT A LICENCE TO RENDER LESS THAN ONE CHOICE — REVIEW
   (iteration 3) CR-01. The iteration-2 fix above capped the STRIP and made the STRIP
   the scroller, and that shipped a scroll box smaller than its own first child at every
   viewport. MEASURED on the shipped build with the shortest possible chip text and
   `inspiration: 2`: at 375x667 a 70.9px strip against 380px of content showed 0 of 3
   chips (a 28.8px "Choices" header plus 25px of a 98.4px chip) and put .inspire-bar —
   the ONLY inspiration-spend control in the app, the mode-conditional block in
   play.jsx that renders .btn-inspire or NarrativeTokenSpendCluster — entirely below the
   fold. .inspire-bar was BELOW THE FOLD AT EVERY VIEWPORT FROM 375x667 TO 1920x1080,
   because it is the strip's LAST child and the strip is always shorter than its
   content. No fade mask (.scroll-fade is on .feed only) and no reserved gutter, so
   nothing on screen said "this scrolls".

   THE FIX IS A ROLE SPLIT, NOT A BIGGER NUMBER. The strip stops being a scroller and
   becomes a three-part column:
     .suggested-actions-header  pinned  — says how many choices there are
     .suggested-actions         SCROLLS — the chip list, floored at one whole chip
     .inspire-bar               pinned  — the only inspiration UI; bounded, never hidden
   so the two things that must never disappear never do, and the one thing that may
   legitimately overflow (a list of N chips) is the only thing that scrolls.

   THE FLOOR IS AN EXPLICIT PIXEL SUM, AND `min-content` WAS TRIED AND REJECTED. The
   obvious spelling is `min-height: min-content` — header + chip floor + inspire floor,
   with the chip list contributing only its own min-height because it is a scroll
   container. It does not work: `overflow` zeroes the AUTOMATIC minimum size of a flex
   item, but the min-content KEYWORD in the block axis is still the full content height,
   so the strip's floor came out as header + ALL the chips + inspire. MEASURED on that
   build: .shell-strip 360.5px and .feed 71px / 13.1% at 1024x768 — the reading-column
   collapse this review has now rescued twice, re-created by the fix for the third. Hence
   the arithmetic below, restated per arm alongside the parts it is a sum of.

   BOTH ENDS ARE SELF-LIMITING, WHICH IS THE POINT.
     min-height: min(var(--strip-floor), 55%)   floor, but never more than half the row
     max-height: max(26%, var(--strip-floor))   26% ceiling, but never under the floor
   A floor that can eat its own container is not a floor: at 812x375 the middle row is
   115px and the compact sum is 116px, so an unclamped floor takes the WHOLE row and
   .feed goes to zero. The `min(…, 55%)` clamp is what makes the rule safe to state
   unconditionally instead of bracketing it away from every short viewport by hand.

   .inspire-bar IS BOUNDED, NOT PINNED-AT-ANY-COST. In narrative mode it is not a button
   but NarrativeTokenSpendCluster — a label + a 2-row textarea + a submit row, MEASURED
   at 173-201px. An unbounded floor there would hand the strip a 250px min-content and
   starve the reading column that this same review had to rescue twice. So it gets a
   floor (one tap target) AND a cap, and scrolls internally between them: always ON
   SCREEN and always reachable, which is what CR-01 is about, without a compound widget
   dictating the whole budget.

   WHAT THIS COSTS THE READING COLUMN, STATED PLAINLY. One whole chip is not free. The
   review that raised CR-01 named the two honest ways to pay for it — "(a) raise the
   strip cap and take the pixels from .feed ... or (b) lift .inspire-bar into
   .shell-dock" — and this is (a), with the compact arm below spending as little of it as
   the type will allow. MEASURED, 3 chips + a summary card, before -> after:
     375x667   52.1% -> 44.0%      800x600   52.9% -> 49.5%
     390x844   54.4% -> 60.2%      1024x768  53.5% -> 47.1%
     375x812   54.2% -> 59.3%      1280x800  54.6% -> 50.0%
     768x1024  60.2% -> 63.7%      1400x900  57.5% -> 57.4%
     1400x600  53.7% -> 56.0%      1920x1080 61.0% -> 61.0%
   Every one clears the 40% ratio floor and the 100px absolute floor T4 asserts, with the
   worst case (375x667, 119.9px) 20 points clear of the first and 20px clear of the
   second. The three that FELL are the three whose middle row is under ~550px; the phone
   fell furthest because at 375x667 the header (141.4) and dock (253) take 394 of 667 —
   the chrome-budget problem recorded under "THE 100px ABSOLUTE FLOOR IS UNREACHABLE ON A
   LANDSCAPE PHONE" above and owned by Phase 37's slim dock. Without
   a summary card on screen the phone is at 58.9%. Do not fund a bigger floor out of
   .feed; fund it out of the chrome, in Phase 37.

   WHAT THE FLOOR DOES NOT PROMISE. It is one chip's worth of SPACE, not one chip
   whatever its height. MEASURED with four wrapping choices ("Push through the collapsed
   archway and call for the archivist by her true name."): a chip is 64.4px on a phone
   when its text is one line and 115.7px when it is three, against a 68px floor — so a
   long choice shows about 60% of itself there and the rest is a scroll away, with the
   fade mask saying so. Flooring at the three-line height instead costs .feed 10 more
   points (MEASURED 33.7% at 375x667), i.e. it re-creates the collapse this cap exists to
   stop. Chip text is deliberately NOT clamped to N lines: an ellipsis in the middle of a
   CHOICE hides the thing the player is choosing. */
html[data-screen="play"] .shell-strip {
  /* Tuned against the probe, not guessed; the compact arm below restates both.
     --strip-chip-floor is one WHOLE chip INCLUDING its optional DC/XP meta row
     (MEASURED 98.4px at <=768px, 102.4px above it — a chip without a meta row is
     ~61px, and flooring at 61 would show two thirds of the commonest chip there is). */
  --strip-chip-floor: 104px;
  --strip-inspire-floor: 44px;
  --strip-inspire-cap: 148px;
  /* = padding-top 16 + border 1 + row-1 max(caption 29, inspire floor 44) + header
       margin-bottom 8 + chip floor 104, rounded up. Re-measure with the probe; do not
       adjust one term by eye — every one of them is a rect this file's comments quote. */
  --strip-floor: 176px;
  flex: 0 1 auto;
  min-height: min(var(--strip-floor), 55%);
  max-height: max(26%, var(--strip-floor));
  /* The STRIP no longer scrolls — its chip list does. Kept as `hidden` rather than
     dropped so a future child added below .inspire-bar cannot silently escape the cap. */
  overflow: hidden;
  /* GRID, AND THE SAME SHAPE AT EVERY WIDTH. Row 1 is the caption and the inspire
     control SIDE BY SIDE; row 2 is the chip list. Stacking them instead costs one whole
     extra row — MEASURED: a 212px floor rather than 176px, which is .feed at 40.4% of a
     542.8px middle row at 1024x768 against 53.0% side by side, for a caption that has
     640px of unused width next to it. One layout at every width also means one thing to
     reason about; the compact arm below changes only the four custom properties and the
     type sizes, never the shape.
     Row 1 is `minmax(0, auto)`, NOT `auto`: an `auto` row takes its content height FIRST
     and leaves the 1fr row whatever is over, which in narrative mode (a 146-201px
     cluster) is nothing — MEASURED 0 of 3 chips on exactly that build. An explicit
     minimum on row 2 makes the chip floor win the distribution and the inspire row take
     the remainder, which is the priority CR-01 asks for.
     align-items STAYS `stretch` (the default) and must NOT be set to `center`. MEASURED
     with `center`: a grid item is sized to its CONTENT rather than to its track, so the
     chip list came out 240.8px centred in a 77px row and the inspire bar 104px centred
     in a 39px one — both overflowing `overflow: hidden` at BOTH ends, i.e. CR-01 again
     with tidier arithmetic. Stretched, each item is exactly its track and its own
     overflow-y does the rest. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: minmax(0, auto) minmax(var(--strip-chip-floor), 1fr);
  column-gap: 16px;
  /* Lifted VERBATIM out of play.jsx's inline style (REVIEW iteration 3 CR-01), for the
     same reason the card's margin and padding moved: a margin/padding sits outside
     max-height and an inline style beats every rule in this file. */
  padding-top: 16px;
  border-top: 1px solid var(--rule);
}
html[data-screen="play"] .shell-strip > .suggested-actions-header { grid-column: 1; grid-row: 1; }
html[data-screen="play"] .shell-strip > .inspire-bar { grid-column: 2; grid-row: 1; }
html[data-screen="play"] .shell-strip > .suggested-actions { grid-column: 1 / -1; grid-row: 2; }
/* NARRATIVE MODE IS NOT A BUTTON. `.inspire-bar` holds NarrativeTokenSpendCluster there
   — a heading, a 2-row textarea and a submit row, MEASURED 146-201px — and a composer
   does not belong in a caption's column. So it takes the whole first row, and the
   "Choices / press 1-9 or click" caption yields to it: the cluster carries its own
   heading ("Spend Token to Request Plot Turn — ✦ N available"), so what is lost is a
   duplicate label rather than information, and only in the mode that cannot afford both.
   The cluster keeps its own floor and cap and scrolls between them, so its heading and
   the top of its textarea are always on screen — which is the reachability CR-01 is
   about. `:has()` is the right tool here: the distinction is a CHILD's identity, and the
   alternative is threading a modifier class out of a render-time IIFE in play.jsx. */
html[data-screen="play"] .shell-strip:has(.narrative-token-spend) > .suggested-actions-header { display: none; }
html[data-screen="play"] .shell-strip:has(.narrative-token-spend) > .inspire-bar { grid-column: 1 / -1; }
html[data-screen="play"] .shell-strip > .suggested-actions {
  min-height: var(--strip-chip-floor);
  /* The base rule's `margin: 6px 0 18px` is 24px of a 70.9px budget. Inside the capped
     strip the header and the inspire bar already supply the separation. */
  margin: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  /* THE AFFORDANCE. Two of them, deliberately, because neither is sufficient alone.
     `scrollbar-gutter: stable` reserves a classic-scrollbar track so a desktop Chrome
     shows a real thumb, and MEASURES 0px in headless Chromium and on every mobile
     browser, all of which use OVERLAY scrollbars that only appear DURING a scroll —
     which is exactly why the shipped `scrollbar-width: thin` carried no cue on the
     phone this finding rests on. The mask is the cue that renders everywhere: the same
     two-ended ramp .feed gets from the .scroll-fade rule, at 12px rather than 24px
     because this box is a sixth of the feed's height. A clipped list reads as clipped. */
  scrollbar-gutter: stable;
  mask-image: linear-gradient(to bottom, transparent 0, black 12px, black calc(100% - 12px), transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 12px, black calc(100% - 12px), transparent 100%);
}
html[data-screen="play"] .shell-strip > .inspire-bar {
  min-height: var(--strip-inspire-floor);
  max-height: var(--strip-inspire-cap);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  /* Matches the caption's own margin-bottom so the two row-1 items sit on one baseline
     band and the chip list clears both by the same 8px. */
  margin: 0 0 8px;
  /* flex-start, NOT center. `.inspire-bar` is a bounded scroll box and in narrative mode
     its child is a 146-201px cluster, so centring shows the MIDDLE of that cluster when
     the box is short — SCREENSHOTTED at 375x667: a 31px bar rendering the second line of
     the textarea's placeholder, i.e. a text fragment with nothing to say it is a control.
     Top-aligned, a clipped cluster shows its own heading ("Spend Token to Request Plot
     Turn — ✦ N available"), which is the affordance. For the strict-mode button the row
     is button-height anyway, so this changes nothing there. */
  align-items: flex-start;
}
/* The caption is a grid item now, so it is exactly one track tall and its text would
   otherwise PAINT OVER the first chip when it wraps — SCREENSHOTTED at 375x667, where
   "press 1-3 or click" wrapped in a column narrowed by the inspire button and the word
   "click" landed on top of the chip below. Clipped rather than allowed to grow: growing
   is what the floor is paying for. */
html[data-screen="play"] .shell-strip > .suggested-actions-header { overflow: hidden; }
/* THE TURN-SUMMARY CAP IS GONE, and so is the rule that needed it. The card
   used to render as a sibling of .feed inside .shell-main, which forced this
   whole apparatus: a percentage cap, its own scroller, a focus ring and a tab
   stop, all to fit "This turn, you gained" into a slice of a row it was
   competing with the narration for. MEASURED at 375x667 under the 12% arm: 54px
   visible, 135px of content — a porthole. The card now renders INSIDE the feed
   (see play.jsx), so it scrolls with the prose it belongs to and costs the
   narration nothing, which is what the cap was buying in the first place.
   The selector below is retained as a NO-OP GUARD, not as dead code: it fires
   only if the card is ever moved back out to .shell-main, where an uncapped
   sibling would grow without limit and push the feed out of its row. */
html[data-screen="play"] .shell-main > .turn-summary-card {
  flex: 0 1 auto;
  min-height: 0;
  max-height: 16%;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* NOTHING IN THE FEED MAY BE SQUEEZED. .feed is `display: flex; flex-direction:
   column` and flex items default to `flex-shrink: 1`. `min-height: auto`
   normally protects them from collapsing below their content — but it resolves
   to 0 for any child that is itself a SCROLL CONTAINER, i.e. any child with
   `overflow` other than `visible`. .roll-card sets `overflow: hidden` (it clips
   the verdict stamp's entrance), so it was silently crushable.
   MEASURED at 375x667 with a full feed: a success roll card rendered
   clientHeight 28px against scrollHeight 177px — 149px of dice, modifiers,
   total and verdict clipped away, and because the overflow is `hidden` rather
   than `auto` there was no way to scroll to any of it. The card looked like a
   broken 28px sliver with its label cut through the middle.
   Scoped to the feed's own children rather than written as a blanket
   descendant rule — the 35-RESEARCH Pitfall 2 lesson. */
html[data-screen="play"] .feed > * { flex-shrink: 0; }
/* REVIEW (iteration 2) WR-02 — the same inset ring .play-shell .feed:focus-visible gets, for
   the same reason: capping this card made it a scroll container, play.jsx gives it
   tabIndex={0} so a keyboard can reach it (WCAG 2.1.1), and a focusable element needs a
   visible focus indicator. The chip strip needs neither — it holds real <button>
   children, so tab-to-focus scrolls it into view and each chip rings itself.
   16% IS KEPT AT EVERY WIDTH WHERE THE MIDDLE ROW CAN AFFORD IT. The iteration-2 note
   here read "16% IS KEPT, DELIBERATELY ... revisit it in Phase 37, not before", and the
   trade it weighed was CARD vs .feed — the reviewer had asked for the card's pixels and
   the answer was that narration needed them more. REVIEW (iteration 3) CR-01 asked a
   DIFFERENT question that iteration never put: card vs THE ONLY CHOICE AND THE ONLY
   INSPIRATION CONTROL. At 375x667 the 16% cap rendered 43.6px OF WHICH 32 WAS PADDING —
   about eleven pixels of summary — while the chip strip next to it showed zero complete
   choices and no inspire bar at all. Trading a padded fragment for a whole choice is not
   the trade that was refused. So 16% stands everywhere the row can pay for it, and the
   short-middle-row arm below takes it to 12% WITH the padding compacted to match, so
   what is left is a readable line rather than a thicker frame around nothing. Phase 37's
   slim dock is still what makes this stop being a zero-sum row. */
html[data-screen="play"] .turn-summary-card:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: -2px;
}

/* The 24px top margin is OUTSIDE max-height and comes straight off the feed's share of
   the middle row. On a tall screen it is 24 of 600+ px and buys real separation; on a
   short row it is the difference between prose and a caption. Keyed on BOTH axes
   because both produce a short middle row: a 375-wide phone (where the header wraps to
   a third row) and any viewport under ~640px tall, which is where 800x600 and 1400x600
   live. MEASURED at 800x600: 32px of margin against a 340px row.
   REVIEW (iteration 3) CR-01 — this arm's `margin-top: 8px` is now folded into the
   card's compact rule at the FOOT of the block, together with its cap and its padding,
   so the three numbers that decide how many pixels the card costs are stated in one
   place instead of two. Stating a margin twice in one media block is the WR-06 failure
   mode with a shorter fuse. */
@media (max-width: 768px), (max-height: 640px) {
  /* THE COMPACT STRIP — REVIEW (iteration 3) CR-01, and the whole of how the
     one-whole-chip floor is PAID FOR. Same two-axis key as the margin above, and for the
     same reason: both a 375-wide phone (header wraps to a third row) and any viewport
     under ~640px tall produce a short middle row.

     Every value here is a BUDGET decision, not a taste decision. The strip's floor is
     header + one chip + the inspire control, and at the shipped sizes that sum is 17 +
     28.8 + 98.4 + 60 = 204px against a 272.6px middle row at 375x667 — which would have
     left .feed at 25%, i.e. traded one starved surface for another. Compacted it is the
     number MEASURED in the block below. The typography is NOT shrunk anywhere else on
     the screen: these rules are scoped to `.shell-strip`, so the prose column, the
     ledger and the composer keep the v1.6 readability bump verbatim.

     THE INSPIRE BUTTON SWAPS LABELS RATHER THAN SHRINKING TYPE. At 15px Cinzel/0.16em
     "Spend Inspiration for Advantage (2)" WRAPS TO TWO LINES at 375px (MEASURED:
     .inspire-bar 60px on a phone, 42px on a desktop). Squeezing the type to fit one line
     lands at ~11px, which is below this app's floor everywhere else. `display: none` on
     the unused span drops it from the accessibility tree too, so exactly one label is
     ever exposed — see the comment at the JSX (play.jsx, .btn-inspire).

     THE SHAPE DOES NOT CHANGE HERE — only the four custom properties, the type sizes and
     the padding. The caption/inspire row, the grid tracks and the narrative placement are
     all in the base rule and apply at every width; restating them under a breakpoint is
     how the CR-01 bracket got shipped in the first place.
     The caption keeps its keyboard hint HERE: this arm also matches 800x600 and
     1400x600, which are desktops with keyboards, and "press 1-9 or click" is the only
     place that shortcut is documented. It is dropped at <=480px WIDTH — a separate arm
     below, because the reason is width, not budget: the caption column is what is left
     of 375px after the inspire button, the hint wraps into it, and a keyboard shortcut
     is not addressed to a phone in the first place.                                  */
  html[data-screen="play"] .shell-strip {
    /* 68px is the MEASURED compact chip (64.4px at 375x667 with these type sizes)
       INCLUDING its DC/XP meta row, plus a few pixels of slack. Flooring at the ~50px a
       meta-LESS chip needs would show three quarters of the commonest chip there is,
       which is the CR-01 failure in miniature — the floor has to clear the WORST
       ORDINARY case, not the best. It does NOT clear a chip whose text wraps to two
       lines; that is what the CHOICES CLIPPED clause in t4-fullrun.js is for, and a red
       there means re-measure this number, not delete the clause. */
    --strip-chip-floor: 68px;
    --strip-inspire-floor: 30px;
    --strip-inspire-cap: 104px;
    /* = padding-top 8 + border 1 + row-1 max(caption 27, inspire floor 30)
         + caption margin-bottom 4 + chip floor 68, rounded up. */
    --strip-floor: 112px;
    padding-top: 8px;
    column-gap: 12px;
  }
  html[data-screen="play"] .shell-strip > .inspire-bar { margin-bottom: 4px; }
  html[data-screen="play"] .shell-strip .suggested-actions-header { margin: 0 0 4px; }
  html[data-screen="play"] .shell-strip .suggested-actions-hint { font-size: 14px; }
  html[data-screen="play"] .shell-strip .suggested-actions { gap: 6px; }
  html[data-screen="play"] .shell-strip .suggested-action {
    min-height: 44px;
    padding: 6px 12px;
    gap: 2px;
  }
  html[data-screen="play"] .shell-strip .suggested-action-row.primary { gap: 10px; }
  html[data-screen="play"] .shell-strip .suggested-action-row.meta {
    gap: 8px;
    padding-left: 36px;
    line-height: 1.1;
  }
  html[data-screen="play"] .shell-strip .suggested-action-text { font-size: 18px; }
  html[data-screen="play"] .shell-strip .suggested-action-num { font-size: 15px; padding: 2px 6px; min-width: 22px; }
  html[data-screen="play"] .shell-strip .suggested-action-dc,
  html[data-screen="play"] .shell-strip .trigger-badge,
  html[data-screen="play"] .shell-strip .xp-preview-badge { font-size: 14px; }
  html[data-screen="play"] .shell-strip .suggested-action-dc-skill { font-size: 13px; }
  html[data-screen="play"] .shell-strip .btn-inspire {
    padding: 5px 10px;
    gap: 6px;
    font-size: 12px;
    letter-spacing: 0.10em;
  }
  html[data-screen="play"] .shell-strip .btn-inspire-icon { font-size: 14px; }
  /* Narrative mode's cluster is clipped to the inspire row here, so its HEADING is the
     part the player actually sees and it has to fit on one line. SCREENSHOTTED at
     375x667 at the shipped 15px: "Spend Token to Request Plot Turn — ✦ 2 available"
     wrapped and the second line was cut mid-height. */
  html[data-screen="play"] .shell-strip .narrative-token-spend > .label { font-size: 13px; }
  html[data-screen="play"] .shell-strip .btn-inspire-label-full { display: none; }
  html[data-screen="play"] .shell-strip .btn-inspire-label-short { display: inline; }
  /* The card pays part of the floor too — 12% instead of 16%, and 8/14 padding instead
     of 16/20 so what is left is a readable line rather than a thicker frame around
     nothing. Its margins come down with it: 6px of separation reads at this size and
     the other 4 are prose. MEASURED at 375x667 with 3 chips: card block 32.7 + 12
     against 59.6 before. */
  html[data-screen="play"] .shell-main > .turn-summary-card {
    max-height: 12%;
    padding: 8px 14px;
    margin-top: 4px;
    margin-bottom: 4px;
  }
}
/* LANDSCAPE PHONE — a tighter budget for a row that has none. The card cannot shrink
   past its own padding + border (MEASURED: a 34px floor with the shipped 16px/20px
   padding; REVIEW (iteration 3) CR-01 moved that padding out of play.jsx into this file
   and the compact arm above takes it to 8px/14px, so the floor is now ~18px — the
   sentence that used to sit here, "that padding is INLINE in play.jsx so no rule in this
   file can reach it", is no longer true and is corrected rather than left to mislead).
   On a 115px middle row the only pixels left to reclaim are the card's margins and the
   chips' share. 26% of 115px is 29.9px — one chip, which is not worth 7px of prose when
   the chip list is a scroller anyway. MEASURED at 812x375, 3 chips + a card: .feed 40px / 34.8% of the row on the
   unbracketed build -> 50px / 43.5% here, i.e. over the 40% ratio floor rather than
   under it. 667x375 is unaffected either way (both siblings sit on their padding floors
   there, and 43px of middle row is the chrome-budget problem described above).
   Keyed on height ALONE: a landscape phone and a short desktop window have the same
   problem and neither of them is a width. */
/* NARROW PHONE — the caption's keyboard hint goes, and only here. REVIEW (iteration 3)
   CR-01 put the inspire control in the caption's row to save a whole row of the middle
   row's budget; the price is that at 375px the caption column is ~200px, and "press
   1-3 or click" wraps into it. SCREENSHOTTED before this rule: the word "click" painted
   on top of the first chip. Keyed on WIDTH ALONE because the cause is width — 800x600
   matches the compact arm above and has both the room and a keyboard, so it keeps the
   hint. Nothing is lost on a phone: there is no keyboard to press 1-3 on, the chips
   carry their own number tiles, and the list keeps its role="group" aria-label. */
@media (max-width: 480px) {
  html[data-screen="play"] .shell-strip .suggested-actions-hint { display: none; }
}

@media (max-height: 480px) {
  /* THE ONE-WHOLE-CHIP FLOOR IS RELEASED HERE, DELIBERATELY — REVIEW (iteration 3)
     CR-01. On a 115px middle row the compact floor (116px) is the WHOLE ROW; the
     `min(…, 55%)` clamp above already stops it eating .feed, but 55% of 115px is 63px
     and spending it on one chip is not worth 63px of the 50px of prose that is all this
     viewport has. Landscape phone is ALREADY decided (see "THE 100px ABSOLUTE FLOOR IS UNREACHABLE"
     and the block above) as
     a chrome-budget problem owned by Phase 37's slim dock, and is deliberately asserted
     nowhere — so this arm returns to the iteration-2 behaviour (bounded strip, chip list
     scrolls, nothing guaranteed whole) rather than inventing a third policy.
     `--strip-floor: 0px` is what releases BOTH clamps: min-height goes to 0 and
     max-height back to the plain 20%. This is the ONLY regime where the floor is
     released; every viewport with a middle row over ~200px pays it in full. */
  html[data-screen="play"] .shell-strip { --strip-floor: 0px; max-height: 20%; }
  html[data-screen="play"] .shell-main > .turn-summary-card { margin-top: 4px; margin-bottom: 4px; }
}

/* P34 (THEME-02): DELIBERATELY NO backdrop-filter here — deferred to Phase 35.
   The 34-UI-SPEC lists .top-bar among the panels to frost, but this rule has no
   background declaration at all: only a bottom border. Blurring it would be
   cosmetically null while still making it a containing block for `position:
   fixed` descendants and opening a new stacking context — cost with no visual
   payoff. The shell header gets a real surface in Phase 35 (SHELL-02); that is
   where the frosting decision belongs. Do not add it here first. */
/* v1.7 P39 (CHROME-03): the pre-play header rule is a GILT hairline — the 1px
   echo of the play shell's 2px `border-bottom: 2px solid var(--shell-gilt)`.
   Border WIDTH is untouched (a 2px rule here would move every wizard's content
   down one pixel and change nothing else); only the colour moves. On the play
   screen this declaration is overridden by html[data-screen="play"] .shell-hdr
   at (0,2,1) — see the block at styles.css:2925. */
.top-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-bottom: 20px;
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 55%, var(--rule));
  margin-bottom: 28px;
}

.brand {
  font-family: 'Cinzel', serif;
  font-size: 16px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: 10px;
}

/* v1.7 P39 (CHROME-03): the brand lozenge takes the metal, not the wax accent.
   It is the first mark on every pre-play screen and the play shell's own ❦ is
   gilt; a rose diamond there was the loudest remaining "different app" signal. */
.brand-mark {
  display: inline-block;
  width: 10px;
  height: 10px;
  background: var(--gold);
  transform: rotate(45deg);
}

/* ============================================================================
   v1.7 P35 SHELL-01/SHELL-02/SHELL-04 — THE SHELL CHROME
   ----------------------------------------------------------------------------
   The visual half of the shell: gilt frame + corner flourishes, the mirrored
   header/dock plates, meter + pip geometry, and the 860px header-wrap bracket.
   The GEOMETRY half is the Shell Scroll Contract at styles.css:2056-2252; these
   two blocks are deliberately separate and this one sits DOWN HERE ON PURPOSE:
   the header element is <header class="top-bar shell-hdr">, and `.top-bar`
   (:2261) plus its @media (max-width: 768px) override (:3035) declare
   padding-bottom / border-bottom / margin-bottom at class specificity (0,1,0).
   A bare `.shell-hdr` written ABOVE .top-bar would lose every one of those on
   source order. The plate rules below are therefore both placed after .top-bar
   AND scoped html[data-screen="play"] (0,2,1), which is what beats the LATER
   mobile .top-bar block too — the same cascade lesson plan 35-05 recorded for
   .shell-main. Ported in intent from
   .claude/skills/sketch-findings-TRPG/sources/005-full-turn-loop/index.html:
   41-53 (frame + corners) and :62-87 (plate, meters, pips, 860px wrap), with
   every deviation declared in 35-UI-SPEC.md § "Sketch Deltas".
   ============================================================================ */

/* GILT TIERS — DERIVED, NOT TOKENISED.
   Sketch 005 hardcodes `--color-gilt-dim: #a8894a` and bakes `%23c9a227` into
   the corner SVG. Both are per-palette forks by another name, and P34 drove the
   fork count to zero (SC2). These four properties are declared HERE, on
   .play-shell, and NOT in :root or a palette block, for two independent reasons:

     1. test/v17-theme-contract.test.js:314-328 asserts EXACT SET EQUALITY
        against a frozen 71-token TOKEN_CONTRACT. One new --gilt-* token is a
        four-palette-block edit PLUS a test-fixture amendment, and it would have
        to be re-valued by hand for a fifth palette. color-mix on --gold is
        correct for every palette that exists and every palette that ever will.
     2. The v17 palette-block parser UNIONS `:root` blocks and has already
        misread one (STATE.md, P34 decision on styles.css:2047). A `:root`
        declaration here would feed that parser a non-palette property.

   VERIFIED: --shell-gilt under dark-glass computes to #a68c51 against the
   sketch's #a8894a — Δ ≤ 8/255 per channel, visually identical, and every other
   palette gets a gilt muted toward its own ground.
   These are BORDER/BACKGROUND values only. Never write `color: var(--shell-*)`:
   a --shell-* name appearing as a `color:` value would be scanned by
   test/v17-theme-contrast.test.js's WR-03 derivation as a component alias and
   break its pinned alias set. */
.play-shell {
  --shell-gilt:        color-mix(in srgb, var(--gold) 70%, var(--paper));
  --shell-gilt-bright: color-mix(in srgb, var(--gold) 85%, transparent);
  --shell-gilt-echo:   color-mix(in srgb, var(--gold) 25%, transparent);
  /* AAA SHELL — the gilt HAIRLINE: the one ornament tier the shell keeps
     (header bottom, dock top, rail left, list separators). */
  --shell-hair:        color-mix(in srgb, var(--gold) 30%, transparent);
  --shell-hair-soft:   color-mix(in srgb, var(--gold) 14%, transparent);
  /* The candle: a warm pool of light over the reading column (the ambient
     layer's CSS fallback). Background only — see the `color:` note above. */
  --shell-candle:      color-mix(in srgb, var(--gold) 9%, transparent);
}

/* AAA SHELL — THE FRAME AND ITS CORNER BRACKETS ARE RETIRED (.shell-frame /
   .shell-corner no longer render; play.jsx). The double gilt border read as a
   software window and its brackets sat on content (the phone HP label). The
   ornament that remains is the hairline system in the "AAA SHELL LAYER"
   section near the end of this file. */

/* THE HEADER PLATE. NEVER position: sticky — it is a grid row, already pinned
   by the shell (styles.css:2120-2128); sticky on a bounded row is a no-op that
   reads as a fix and prevents the real one from being looked for.
   `margin-bottom: 0` is not tidiness: the base .top-bar (:2261) ships
   `margin-bottom: 28px` and its mobile override (:3035) ships 18px, both of
   which would open a dead gap between row 1 and row 2 of the grid.
   THIS RESOLVES P34's DEFERRED FROSTING NOTE at styles.css:2254-2260, which
   said verbatim: "The shell header gets a real surface in Phase 35 (SHELL-02);
   that is where the frosting decision belongs." It now has one — a real
   gradient plate — so the blur is no longer cosmetically null, and
   --panel-backdrop: none makes it an exact no-op on parchment and observatory.
   ⚠ The backdrop pair is legal HERE and on .shell-dock and NOWHERE ELSE in the
   shell: backdrop-filter makes an element a containing block for its
   `position: fixed` descendants, and neither the header nor the dock is an
   ancestor of the mobile drawer / tab bar / modals. On .shell-body it MEASURED
   the drawer bottom moving 667 -> 625; on .play-shell it measured as
   UNDETECTABLE, which is why contract assertion A4 is source-grep and not a
   rendered check. Do not move this pair up a level. */
html[data-screen="play"] .shell-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 24px;
  margin-bottom: 0;
  border-bottom: 2px solid var(--shell-gilt);
  background: linear-gradient(180deg, var(--surface-raised), var(--surface));
  box-shadow: var(--shadow-md);
  -webkit-backdrop-filter: var(--panel-backdrop, none);
          backdrop-filter: var(--panel-backdrop, none);
}

/* Title slot. `min-width: 0` is what lets the flex item shrink so the ellipsis
   can engage at all — without it the title refuses to go below its intrinsic
   width and pushes the meters off the plate instead of truncating.
   The ❦ is a ::before rather than text so it never enters the accessible name
   and never participates in the ellipsis calculation. */
html[data-screen="play"] .shell-title {
  font-family: 'Cinzel', serif;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--ink);
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
html[data-screen="play"] .shell-title::before {
  content: "❦ ";
  color: var(--gold);
}
html[data-screen="play"] .shell-subtitle {
  font-size: 14px;
  font-weight: 400;
  line-height: 1.3;
  color: var(--ink-faded);
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* METERS. Geometry from 35-UI-SPEC § Meters. The fills reuse the existing
   palette-aware .meter-fill.hp / .mana / .stam classes (styles.css:1939-1941)
   so no new colour enters the file. */
html[data-screen="play"] .shell-meters {
  display: flex;
  align-items: center;
  gap: 12px;
}
html[data-screen="play"] .shell-meter {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 76px;
}
/* font-variant-numeric: tabular-nums CONSEQUENCE, not description: with
   proportional digits the label's intrinsic width CHANGES when 24/30 becomes
   9/30, which changes the meter's width, which changes the header's bounding
   box — and T4 SHELL_CHROME_FIXED_ACROSS_TURNS asserts that box is IDENTICAL
   across 20 turns. Removing this line does not make the header look slightly
   worse; it makes SC1 fail on the first turn that spends HP. */
html[data-screen="play"] .shell-meter .m-label {
  display: flex;
  justify-content: space-between;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 14px;
  line-height: 1.2;
  letter-spacing: 0.08em;
  font-variant-numeric: tabular-nums;
}
html[data-screen="play"] .shell-meter .m-key { color: var(--ink-faded); font-weight: 400; }
html[data-screen="play"] .shell-meter .m-val { color: var(--ink); font-weight: 600; }
html[data-screen="play"] .shell-meter .m-bar {
  height: 6px;
  border-radius: 3px;
  background: var(--paper-deep);
  overflow: hidden;
}
html[data-screen="play"] .shell-meter .m-fill {
  height: 100%;
  border-radius: 3px;
  transition: width 180ms ease;
}

/* The turn counter rides the same tabular-nums requirement: T9 -> T10 changes
   the plate's bounding box otherwise. */
html[data-screen="play"] .shell-turn {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 14px;
  font-weight: 400;
  line-height: 1.2;
  color: var(--ink-faded);
  font-variant-numeric: tabular-nums;
}

/* INSPIRATION PIPS. --glow-gilt is the token P34 reserved for exactly this
   (34-UI-SPEC "Downstream Token Consumers": --glow-gilt -> P35 inspiration
   pips). white-space: nowrap keeps five glyphs on one line so the pip cluster
   can never become a second header row on its own. */
html[data-screen="play"] .shell-pips {
  font-size: 14px;
  line-height: 1;
  letter-spacing: 0.18em;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
html[data-screen="play"] .shell-pips .pip { color: var(--ink-faded); }
html[data-screen="play"] .shell-pips .pip.filled {
  color: var(--gold);
  text-shadow: var(--glow-gilt);
}

/* THE DOCK PLATE — the header's mirror: gilt border on TOP, gradient running
   the other way, shadow thrown upward.
   calc(12px + env(safe-area-inset-bottom)) is the SHELL-04 phone requirement.
   It needs NO breakpoint gate: env() resolves to 0px wherever no inset exists,
   so the expression is exactly 12px on desktop and on every non-notched device.
   Same backdrop-filter standing as the header — see the note above. */
html[data-screen="play"] .shell-dock {
  padding: 12px 24px calc(12px + env(safe-area-inset-bottom));
  border-top: 2px solid var(--shell-gilt);
  background: linear-gradient(0deg, var(--surface-raised), var(--surface));
  box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.35);
  -webkit-backdrop-filter: var(--panel-backdrop, none);
          backdrop-filter: var(--panel-backdrop, none);
}
/* The composer keeps the prose column's 780px measure and centres in the dock.
   Placed AFTER rule 9 of the scroll contract (styles.css:2232) at the same
   specificity so `margin: 0 auto` wins the tie over its `margin-top: 0` — the
   computed margin-top is 0 either way. `padding: 0` beats the mobile
   .play-composer 2px side padding (:3087) on specificity, not source order. */
html[data-screen="play"] .shell-dock .play-composer {
  max-width: 780px;
  margin: 0 auto;
  padding: 0;
}

/* REVIEW CR-05 — RESERVE THE RAIL'S TRACK ON THE DOCK.
   The dock is grid ROW 3 of .play-shell, and .play-shell is `grid-template-columns:
   none`, so the dock spans the FULL viewport width. The feed does not: it lives in
   .shell-main, which is column 1 of .shell-body's `1fr var(--shell-rail-w)` grid.
   Both then centre a 780px measure inside their own container — against different
   containers. MEASURED at 1400x900 before this rule: .feed centred at x=540 (inside
   the 0-1080 column) and .play-composer at x=700 (inside the 0-1400 row) — the
   composer sat 160px to the RIGHT of the prose it belongs to, at every desktop width
   where the rail is shown. Before Phase 35 the composer was the tail of the prose
   column and was aligned by construction; lifting it into a full-width dock row broke
   that. No checkpoint saw it: SHELL_CHROME_FIXED_ACROSS_TURNS asserts the dock rect is
   invariant ACROSS TURNS, never that it is aligned TO THE READING COLUMN, and the
   phone segment runs at 375px where the rail is a drawer and the offset is zero.
   The 24px term is the dock's own side padding, so the composer's container becomes
   exactly the prose column's box.
   THE HEADER IS DELIBERATELY LEFT FULL-BLEED. It is not a 780px measure — it is a
   space-between row whose right cluster (turn counter, cog, "Back to menu") belongs at
   the viewport edge, and column-aligning it would strand 320px of empty gilt plate
   over the rail. Only the composer shares the prose column's measure, so only the
   composer needs the reservation.
   BRACKETED at min-width: 769px because below that the rail is `position: fixed` and
   .shell-body drops to a single column — reserving a track there would indent the
   composer against nothing. The @media (max-width: 768px) dock rule further down sets
   padding-left/right: 16px and is mutually exclusive with this one.              */
@media (min-width: 769px) {
  html[data-screen="play"] .shell-dock {
    padding-right: calc(24px + var(--shell-rail-w));
  }
}

/* THE 860px HEADER-WRAP BREAKPOINT — header layout ONLY.
   READ THIS BEFORE "FIXING" WHAT LOOKS LIKE A BUG: between 769px and 860px the
   desktop rail is STILL VISIBLE and the meters have ALREADY wrapped to their own
   row. That is the intended state, not an oversight. TWO INDEPENDENT
   BREAKPOINTS IS THE WHOLE POINT — 860 governs header wrap, 768 governs the
   drawer conversion and everything that hangs off it. Collapsing them "for
   consistency" moves the phone drawer bracket, which 35-CONTEXT locks as
   untouched this phase.
   Sketch 005 (index.html:80-87) ALSO hides the rail, swaps the body to one
   column and shows a character-sheet button at 860. None of that is ported
   (35-UI-SPEC Sketch Deltas 7 and 8): ledger access on a phone stays the
   existing tab-bar + drawer, which are byte-unchanged.
   Placed AFTER the plate rules above at equal specificity (0,2,1) so the
   overrides win on source order, and BEFORE the 768px block below so that
   block's padding-top longhand survives this one's padding shorthand.
   T4 SHELL_PHONE_METERS_WRAP_AND_DRAWER asserts both halves at 375px. */
@media (max-width: 860px) {
  html[data-screen="play"] .shell-hdr {
    flex-wrap: wrap;
    row-gap: 8px;
    padding: 12px 16px;
  }
  /* The character name is the first thing to go: the world name (the title) is
     what identifies the chronicle, and the ledger carries the character. */
  html[data-screen="play"] .shell-subtitle { display: none; }
  html[data-screen="play"] .shell-meters {
    width: 100%;
    justify-content: space-between;
    order: 3;
  }
  html[data-screen="play"] .shell-meter {
    width: auto;
    flex: 1 1 0;
    max-width: 92px;
  }
}

/* PHONE INSETS. A separate small bracket rather than an edit inside the big
   @media (max-width: 768px) block at styles.css:3013 — that block is shared
   with five screens and every rule in it is deliberately left byte-unchanged.
   padding-top must be a LONGHAND declared after the 860px shorthand above, or
   the wrap bracket resets the notch inset back to 12px on exactly the devices
   that have one. */
@media (max-width: 768px) {
  html[data-screen="play"] .shell-hdr {
    padding-top: calc(12px + env(safe-area-inset-top));
  }
  html[data-screen="play"] .shell-dock {
    padding-left: 16px;
    padding-right: 16px;
    padding-top: 8px;
    /* TAB-BAR CLEARANCE, RETIRED. This used to read
           calc(12px + 68px + env(safe-area-inset-bottom))
       because .mobile-tabbar was `position: fixed; bottom: 0; z-index: 210` and
       painted straight over the composer — MEASURED (Chrome 147, 375x667): tab
       bar 600->667, textarea 566->636, i.e. the bottom 36px of the textarea and
       the whole send button were covered.
       The tab bar is gone (its four buttons duplicated the drawer's own tab row
       and cost 67px of fixed strip plus this 68px reservation — 135px, more
       than the phone browser's URL bar and toolbar combined). Nothing is
       painting over the dock any more, so the reservation goes back to the
       plain inset the desktop rule already uses. */
    padding-bottom: calc(12px + env(safe-area-inset-bottom));
  }
}

/* ============================================================================
   PHONE CHROME BUDGET — give the prose column back the screen.
   ----------------------------------------------------------------------------
   MEASURED before this block (headless Chromium, 390x760 — the viewport Brave
   actually hands the app on a 1080x2340 phone once its URL bar and toolbar are
   subtracted):
       header  141px (18.6%)   4 stacked rows: title 26 / meters 27 / pips 14 /
                               turn+gear+drawer 47
       FEED    336px (44.2%)   the game
       dock    283px (37.2%)   handle 30 + composer 159 + 68 tab-bar clearance
       tab bar  67px           fixed, painted inside the dock's reserved band
   i.e. 424px of chrome against 336px of reading, and with the actions panel
   open (259px) only 77px of narration stayed visible. The header rows are the
   cheapest to reclaim: the pips duplicate the ledger's Inspiration row, the
   meters duplicate the ledger's Self tab, and the composer's label row
   duplicates its own placeholder.
   Every value here is a length, identical in all four palettes — no colour, no
   token, no per-palette selector (SC4 geometry invariant holds by
   construction). SHELL-04's contract is preserved: the meters still wrap to
   their own header row and the drawer is still reachable; they are compacted,
   not removed.
   ========================================================================= */
@media (max-width: 768px) {
  /* Header: 12px -> 8px plate padding, tighter row gap. */
  html[data-screen="play"] .shell-hdr {
    padding-bottom: 8px;
    padding-left: 14px;
    padding-right: 14px;
    row-gap: 5px;
    column-gap: 10px;
  }
  html[data-screen="play"] .shell-title { font-size: 15px; }
  /* Meters keep their own row (SHELL-04) but at strip height: 14px -> 12px
     label, 6px -> 4px bar, 4px -> 2px internal gap. */
  html[data-screen="play"] .shell-meter { gap: 2px; }
  html[data-screen="play"] .shell-meter .m-label { font-size: 12px; }
  html[data-screen="play"] .shell-meter .m-bar { height: 4px; }
  /* Inspiration pips: the ledger's Self tab carries the same 0/5 reading and
     the panel's inspire bar is the only place the player can SPEND them, so on
     phone the header copy is decoration. Hidden, not removed — desktop keeps
     it, and `display: none` cannot leave a half-row behind the way opacity or
     visibility would. */
  html[data-screen="play"] .shell-pips { display: none; }

  /* Composer: the label row said "Kal Serath's action" over a placeholder that
     said the same thing, and the pair wrapped to 24px+ at this width. */
  html[data-screen="play"] .shell-dock .composer-label-row { display: none; }
  /* v1.7 P39 (READ-02) — the `padding-top: 0 !important; padding-bottom: 0
     !important` pair that lived here is GONE. Its whole purpose was to claw
     vertical pixels back off the DOCK's height, and the composer is no longer a
     dock child — it is inside .actions-panel, an absolutely-positioned overlay
     that contributes nothing to the dock. The !important was there to beat the
     composer's inline `padding: '8px 0'` (also deleted this phase), so nothing
     needs beating any more either.
     Leaving it would have been actively wrong, not merely stale: .actions-panel
     IS inside .shell-dock, so this selector still MATCHES, and an !important
     zero here overrode the sticky composer's bottom inset and reopened the gap
     that chips scroll through (styles.css:3700 documents that pairing). */
  html[data-screen="play"] .shell-dock .play-composer .textarea {
    min-height: 42px;
    padding-top: 9px;
    padding-bottom: 9px;
  }
  /* The send button stretched to the composer's full height (align-items: end
     does not constrain a flex item that declares its own height): pin it to the
     textarea's resting height so it reads as a button, not a column. */
  html[data-screen="play"] .shell-dock .play-composer > .btn {
    align-self: flex-end;
    height: 42px;
    min-height: 0;
    padding-left: 16px;
    padding-right: 16px;
  }
}

/* ============================================================================
   THE THIN HEADER — meters and one door.
   ----------------------------------------------------------------------------
   The play header used to carry seven things: chronicle title, character
   subtitle, three meters, inspiration pips, turn counter, settings cog and exit
   button. On a phone that wrapped to four rows and cost 141px of a 667px
   viewport. Everything except the meters is reference or navigation — none of
   it changes during a turn — so it moved into the ledger's Chronicle tab behind
   the single ☰ this block styles.
   The header is now two flex children at every width: the meter cluster and the
   menu button. That is why the 860px wrap rule is unwound here rather than
   edited in place — it exists to give a THREE-item header somewhere to put the
   meters when the title crowds them, and with the title gone there is nothing
   left to wrap around.
   ========================================================================= */
html[data-screen="play"] .shell-menu-btn {
  flex: 0 0 auto;
  min-height: 40px;
  min-width: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
@media (max-width: 860px) {
  html[data-screen="play"] .shell-hdr {
    flex-wrap: nowrap;
    row-gap: 0;
  }
  /* Back inline, and tighter: `order: 3` + `width: 100%` from the wrap rule
     above would still push the meters onto a second row of their own. */
  html[data-screen="play"] .shell-meters {
    order: 0;
    width: auto;
    flex: 1 1 auto;
    justify-content: flex-start;
    gap: 8px;
    min-width: 0;
  }
  html[data-screen="play"] .shell-meter { max-width: 84px; }
}
@media (max-width: 768px) {
  html[data-screen="play"] .shell-hdr { padding-top: calc(7px + env(safe-area-inset-top)); padding-bottom: 7px; }
  html[data-screen="play"] .shell-meters { gap: 7px; }
  html[data-screen="play"] .shell-meter { max-width: 76px; }
}

/* THE PLAY SCREEN OWNS ITS OWN TOP-RIGHT CORNER. .tweaks-fab (fixed, z-index
   50) and .login-badge (fixed, z-index 49) are rendered by App at root, share
   #root's stacking context with the header (z-index 2) and win outright — the
   D-35-05 overlap, which the thin header only made worse: at 390px they painted
   over the plate and the chronicle title. Both of their jobs now live in the
   Chronicle tab (palette lives in Chronicle Settings via the P34 GlobalRow; Log
   out is a row of its own), so on THIS screen they are redundant, not merely
   misplaced. Every other screen still renders both. */
html[data-screen="play"] .tweaks-fab,
html[data-screen="play"] .login-badge { display: none; }

/* THE OPEN DRAWER MUST OUT-PAINT THE DOCK. See the shell-drawer-open comment in
   play.jsx: .shell-body's `z-index: 2` makes it a stacking context, so the
   drawer's own 200 never reaches past it and the dock's 3 wins. Lifting the
   whole body for the duration of the drawer is the only fix that does not
   restructure the shell tree, and it is safe because the drawer covers the feed
   underneath it anyway. */
html[data-screen="play"] .play-shell[data-drawer="open"] .shell-body { z-index: 6; }

/* Five tabs where there were four. Tighten them so the row still fits one line
   at 375px instead of orphaning "Menu" onto a second row. */
html[data-screen="play"] .side-pane .tab-row {
  flex-wrap: wrap;
  row-gap: 4px;
  gap: 2px;
}
/* Applied at EVERY width, not just phone: the desktop rail is 320px, which fit
   four Cinzel tabs at the old tracking and orphans the fifth onto its own row. */
html[data-screen="play"] .side-pane .tab-row .tab {
  font-size: 13px;
  letter-spacing: 0.06em;
  padding-left: 7px;
  padding-right: 7px;
}

/* The labelled way back out of the full-screen drawer. */
.drawer-back {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  justify-content: center;
  margin-bottom: 12px;
  min-height: 44px;
  font-size: 13px;
  border-color: var(--shell-gilt, var(--rule));
  color: var(--gold);
}
.drawer-back .icon, .drawer-back svg { transform: rotate(180deg); }

/* ============================================================================
   THE CHRONICLE TAB — see ChronicleTab in play.jsx for why it exists.
   Colour comes entirely from existing tokens; no palette selector, no new hex.
   ========================================================================= */
/* AAA SURFACES — a half-title page, then a short ruled list of doors.
   Identity is centred (world in Cinzel — the tab's one title — who · when in
   italic, the player's inspiration, a gilt lozenge between hairlines); the
   actions are one list: a diegetic icon (quill / scroll / book) in a hairline
   ring, a sentence, a line of context in roman, a chevron — ruled with gilt
   hairlines. Log out is the same row, demoted. */
.chronicle-tab { display: flex; flex-direction: column; gap: 0; padding-bottom: var(--space-6); }
.chronicle-ident {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-2) 0;
}
.chronicle-world {
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: 0.04em;
  line-height: var(--leading-tight);
  color: var(--ink);
  text-wrap: balance;
}
.chronicle-sub {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: baseline;
  gap: var(--space-2);
  font-family: var(--font-serif);
  font-style: italic;
  font-size: var(--text-lg);
  color: var(--ink-2);
}
.chronicle-char { font-style: italic; }
.chronicle-sep { color: var(--ink-faded); font-style: normal; }
.chronicle-turn {
  font-style: normal;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink-2);
}
.chronicle-insp {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-1);
}
.chronicle-insp-label {
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  color: var(--ink-faded);
}
.chronicle-insp-stars { display: inline-flex; gap: var(--space-1); }
.chronicle-insp-star { display: inline-flex; color: color-mix(in srgb, var(--gold) 45%, transparent); }
.chronicle-insp-star.filled { color: var(--gold); }
.chronicle-insp-star.filled svg path { fill: currentColor; }
/* The fleuron: a gilt lozenge between two hairlines that fade to the edges —
   the same ornament the prose column uses at scene breaks. */
.chronicle-fleuron {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  margin: var(--space-4) 0 var(--space-5);
}
.chronicle-fleuron::before,
.chronicle-fleuron::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--gold) 40%, transparent));
}
.chronicle-fleuron::after { transform: scaleX(-1); }
.chronicle-lozenge {
  display: block;
  width: 6px;
  height: 6px;
  background: var(--gold);
  opacity: 0.8;
  transform: rotate(45deg);
}
.chronicle-actions {
  display: flex;
  flex-direction: column;
  border-top: 1px solid color-mix(in srgb, var(--gold) 22%, transparent);
}
/* (0,2,0) beats .btn / .btn-ghost / .btn:hover at (0,1,0)-(0,2,0) by source
   order, so the row keeps its own box instead of inheriting button chrome. */
.chronicle-btn.btn {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-4);
  width: 100%;
  min-height: 68px;
  padding: var(--space-3) var(--space-1) var(--space-3) var(--space-1);
  /* .btn is Cinzel / uppercase / 0.12em — a label vocabulary for two-word
     buttons. These are sentences, and uppercasing them wrapped every one to two
     lines. Restated explicitly rather than relying on source order. */
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  text-transform: none;
  letter-spacing: 0.005em;
  font-weight: 500;
  text-align: left;
  line-height: 1.25;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 22%, transparent);
  border-radius: 0;
  transition: background var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.chronicle-btn.btn:hover {
  background: linear-gradient(90deg, color-mix(in srgb, var(--gold) 9%, transparent), transparent 85%);
  color: var(--ink);
  border-color: color-mix(in srgb, var(--gold) 22%, transparent);
}
.chronicle-btn-icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-round);
  border: 1px solid color-mix(in srgb, var(--gold) 40%, transparent);
  color: var(--gold);
  background: color-mix(in srgb, var(--gold) 6%, transparent);
  transition: box-shadow var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
.chronicle-btn.btn:hover .chronicle-btn-icon {
  border-color: color-mix(in srgb, var(--gold) 70%, transparent);
  box-shadow: 0 0 12px color-mix(in srgb, var(--gold) 30%, transparent);
}
.chronicle-btn-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.chronicle-btn-label { color: var(--ink); }
.chronicle-btn-sub {
  font-size: var(--text-sm);
  font-style: normal;
  color: var(--ink-faded);
}
.chronicle-btn-chev { flex: 0 0 auto; color: var(--ink-faded); transition: transform var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out); }
.chronicle-btn.btn:hover .chronicle-btn-chev { color: var(--gold); transform: translateX(2px); }
.chronicle-btn-quiet.btn {
  margin-top: var(--space-5);
  min-height: 56px;
  font-size: var(--text-md);
  color: var(--ink-2);
  border-top: 1px solid color-mix(in srgb, var(--gold) 14%, transparent);
  border-bottom: 0;
}
.chronicle-btn-quiet .chronicle-btn-icon {
  border-color: var(--border-soft);
  color: var(--ink-faded);
  background: transparent;
}
.chronicle-btn-quiet .chronicle-btn-label { color: var(--ink-2); }
.chronicle-note {
  font-family: var(--font-serif);
  font-size: var(--text-md);
  color: var(--ink);
  padding: var(--space-3) var(--space-1) 0;
}
.chronicle-hint {
  font-family: var(--font-serif);
  font-style: normal;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--ink-faded);
  margin: var(--space-4) var(--space-1) 0;
}
/* The desktop rail (~270px of content): one type step down, so every label and
   its line of context stay on one line each. */
@container pane (max-width: 330px) {
  .chronicle-world { font-size: 23px; }
  .chronicle-sub { font-size: var(--text-md); }
  .chronicle-btn.btn { gap: var(--space-3); min-height: 64px; font-size: 18px; }
}

/* Desktop keeps the keyboard hint in the composer's label row; phone never
   renders that row at all, so the span costs nothing there. Both halves of the
   row are nowrap: at the dock's 780px measure the label ("Kal Serath's action"
   in letterspaced Cinzel) and the hint each wrapped to a second line, turning a
   one-line row into three and inflating the desktop composer to 156px. */
html[data-screen="play"] .composer-key-hint { letter-spacing: 0.04em; font-size: 12px; }
html[data-screen="play"] .shell-dock .composer-label-row { gap: 16px; white-space: nowrap; }
html[data-screen="play"] .shell-dock .composer-label-row > * { min-width: 0; }
html[data-screen="play"] .shell-dock .composer-label-row .label { overflow: hidden; text-overflow: ellipsis; }
/* The desktop composer does not need two rows of breathing room either. */
@media (min-width: 769px) {
  html[data-screen="play"] .shell-dock .play-composer .textarea { min-height: 46px; }
  html[data-screen="play"] .shell-dock .play-composer > .btn { align-self: flex-end; height: 46px; min-height: 0; }
}

/* THE PANEL'S OWN HEADER. "CHOICES" + "press 1–4 or click" sat above the first
   cluster and cost ~34px of a panel the cap now measures in the low hundreds.
   On phone the hint is also FALSE — there is no number row on a soft keyboard,
   and the digit hotkeys are gated on the panel being open anyway — so the whole
   row goes. Desktop keeps it (the hotkeys are real there) at a smaller size. */
@media (max-width: 768px) {
  html[data-screen="play"] .actions-panel .suggested-actions-header { display: none; }
}
@media (min-width: 769px) {
  html[data-screen="play"] .actions-panel .suggested-actions-header { margin-bottom: 4px; }
  html[data-screen="play"] .actions-panel .suggested-actions-hint { font-size: 13px; }
}

/* ============================================================================
   v1.7 P37 PANEL-03/04 — THE COLLAPSIBLE OVERLAY ACTION DOCK
   ----------------------------------------------------------------------------
   Ported from sketch 005 (sources/005-full-turn-loop/index.html:168-181), with
   every colour re-expressed through palette tokens (SC2 fork ceiling: zero
   per-palette selectors here; geometry identical across all four palettes).
   The dock at rest is ONLY the .actions-handle + the composer, at a constant
   height whatever the panel state or chip count. The panel is an absolutely-
   positioned overlay (bottom: 100% of .shell-dock, whose position: relative +
   z-index: 3 were pre-laid in P35 for exactly this element) — it is NEVER a
   grid/flex row, so opening it cannot resize the prose column or push the
   header (the P35 overlay-as-grid-row pitfall).
   CLOSED = visibility: hidden, which removes the chips from the a11y tree and
   the tab order without aria juggling; the visibility transition is delayed
   until the slide/fade finishes on close and immediate on open. The panel
   NEVER auto-opens — play.jsx opens it only on a gesture decision from
   lib/panel-gesture.js or the handle (test/v17-panel-contract.test.js).
   `.settling` is the 500ms pointer-events guard after an open so the opening
   gesture's tail cannot insta-click a chip (KNOWN CAVEAT: headless-automation
   input latency ~700ms can outlive it — a harness artifact, do not enlarge).
   Scoped html[data-screen="play"] (0,2,1) so these rules beat the bare-class
   mobile overrides (0,1,0) regardless of source order — the cascade lesson
   from plans 35-05/35-06.
   The retired .shell-strip rules above (THE STRIP BUDGET block) are now INERT:
   play.jsx no longer renders a .shell-strip element. They are left in place
   this phase (removing them is churn with its own risk); the chip list's
   internal scrolling below reuses the already-allow-listed .suggested-actions
   scroller shape from that block's role split.
   ============================================================================ */
html[data-screen="play"] .actions-handle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  max-width: 780px;
  margin: 0 auto;
  min-height: 30px;
  padding: 6px 0 4px;
  cursor: pointer;
  font-family: 'Cinzel', serif;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-faded);
  background: none;
  border: none;
  transition: color 150ms ease;
}
html[data-screen="play"] .actions-handle:hover:not(:disabled) { color: var(--gold); }
html[data-screen="play"] .actions-handle:disabled { opacity: 0.45; cursor: default; }
html[data-screen="play"] .actions-handle:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: -2px;
}
/* The gold pulse on new-actions arrival. The `fresh` class is applied even
   under reduced motion (DELIBERATE delta from sketch 005, whose pulseHandle
   bailed): the colour is the signal and must survive; only the throb is
   motion. The reduced-motion arm at the foot of this file degrades the
   animation to none, leaving a static gold marker. currentColor keeps the
   glow token-driven with zero hardcoded colour. */
html[data-screen="play"] .actions-handle.fresh { color: var(--gold); }
html[data-screen="play"] .actions-handle.fresh:not(:disabled) { animation: handlepulse 1.2s ease 2; }
@keyframes handlepulse { 50% { text-shadow: 0 0 14px currentColor; } }
html[data-screen="play"] .actions-handle-glyph {
  color: var(--gold);
  font-size: 13px;
  line-height: 1;
}

html[data-screen="play"] .actions-panel {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  z-index: 1;
  padding: 14px 24px;
  /* SOLID ground, deliberately — the panel overlays prose, and a translucent
     plate would render choice text over narration text. --paper is the flat
     page hex in every palette (the P34 contract keeps it gradient-free). */
  background: var(--paper);
  border-top: 1px solid var(--shell-gilt);
  box-shadow: var(--shadow-md);
  /* THE CAP LEAVES A READING BAND — a deliberate delta from sketch 005's flat
     55vh, MEASURED against this shell rather than the sketch's ~100px dock.
     At 375x667 the dock is 254.2px (composer + handle + the 68px tab-bar
     clearance) and the middle row 271.4px; a 55dvh panel is 366.8px, which
     covered the ENTIRE prose column plus half the header — "the end of the
     text carried clear above the panel" had nowhere to be. The arithmetic:
     the panel's containing block is .shell-dock, so 100% here IS the dock's
     height, and calc(100dvh - 100% - 220px) is the viewport minus the dock
     minus a 220px reserve (header, 141.4px at its wrapped-mobile worst, plus
     a ~80px band of prose — the last lines the carry parks above the panel).
     This also keeps the feed's clearance padding (panel height + 28) below
     the middle-row height, so the padding floor can never inflate the feed
     box out of its clipped row: padding exceeds the row only if the header
     exceeds 192px, which it never does. The 140px floor keeps the panel
     usable on viewports too short for the reserve (the P35-accepted
     landscape-phone regime); 55vh/55dvh stays as the tall-viewport ceiling.
     MEASURED at 375x667 after this cap: panel 193px, text end 28px clear
     above it, ~78px of prose readable while open. */
  /* RESERVE RETUNED (density pass). The 220px reserve was derived against a
     141px header and a 283px dock; the density pass cut those to 94 and 204,
     and because the reserve subtracts the DOCK, every pixel saved on the dock
     went straight into the panel — MEASURED 259px -> 338px, i.e. compacting
     the composer made the panel BIGGER and left only 123px of prose.
     The reserve is the right knob and it has a closed form. With
     panel = viewport - dock - RESERVE and feed = viewport - header - dock,
     the prose left visible while open is exactly RESERVE - header, at EVERY
     viewport height — a constant reading band rather than one that shrinks on
     short screens. 260px gives 166px on phone (94px header, ~5 lines at
     19px/1.68) and 191px on desktop (69px header).
     Floor and ceiling unchanged in spirit: 140px keeps the panel usable on
     landscape phones where the closed form goes negative; 50dvh (was 55) stops
     it taking more than half a tall desktop window. */
  max-height: clamp(140px, calc(100vh - 100% - 260px), 50vh);
  max-height: clamp(140px, calc(100dvh - 100% - 260px), 50dvh);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  transform: translateY(10px);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: transform 220ms ease, opacity 220ms ease, visibility 0s linear 220ms;
}
html[data-screen="play"] .actions-panel.open {
  transform: translateY(0);
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: transform 220ms ease, opacity 220ms ease, visibility 0s;
}
/* Freshly-opened panel ignores pointer input briefly so the opening gesture's
   tail falls through to the (already-bottomed) feed instead of the choices. */
html[data-screen="play"] .actions-panel.open.settling { pointer-events: none; }
/* Inside the panel the chip list keeps the base .suggested-actions flow (the
   strip-era floor/cap grid does not apply here — the PANEL is the bounded box
   now) but drops the tail margin: the inspire bar below it supplies the
   separation. */
html[data-screen="play"] .actions-panel .suggested-actions { margin-bottom: 6px; }

/* v1.7 P39 (READ-02) — THE COMPOSER IS STICKY TO THE PANEL'S BOTTOM EDGE.
   MEASURED at 375x667 the moment the composer moved inside the panel: chips +
   inspire bar + composer overflow the panel's max-height clamp, so the composer
   landed at top 923.7 in a 667px viewport — off screen, inside the panel's own
   overflow — and the open effect's `panelEl.scrollTop = 0` plus its 600ms rAF pin
   held the reader at the chips, i.e. the field was not merely below the fold, it
   was actively pinned away from. Opening "the action screen" has to SHOW the
   action screen.
   WHY STICKY AND NOT A FLEX FOOTER: making .actions-panel a flex column with an
   inner scroll region would move the scroller off panelEl, and panelEl.scrollTop
   is read by the gesture machine (`atTop` for the swipe-up-to-close) and written
   by the open effect. An inner scroller leaves panelEl.scrollTop pinned at 0
   forever, so every upward swipe would read as "at top" and close the panel.
   Sticky keeps ONE scroller and one set of scroll semantics.
   The background is the panel's own --paper (solid by design — see the plate
   comment above; a translucent value here would render chip text through the
   composer), and the gilt hairline separates the two zones the way the dock
   plate's own top border does. */
html[data-screen="play"] .actions-panel .play-composer {
  position: sticky;
  bottom: 0;
  z-index: 1;
  background: var(--paper);
  border-top: 1px solid var(--shell-gilt);
  /* Replaces the inline `padding: 8px 0` the composer carried as a dock child —
     that inline style was DELETED in the same change (play.jsx), which is what
     lets this be a plain declaration instead of an !important. It needs slightly
     more top room here so the sticky edge is not flush against the last chip.
     Specificity note: this ties `html[data-screen="play"] .shell-dock
     .play-composer { padding: 0 }` at (0,3,1) — .actions-panel IS inside
     .shell-dock, so both match — and wins on source order by sitting after it.
     Keep it below that rule. */
  padding: 10px 0 14px;
  margin-top: 4px;
}

/* …and the panel gives its bottom padding UP to the composer, which is the pair
   that closes the sliver. `bottom: 0` pins a sticky element to its scroll
   container's PADDING box, not its border box, so while the panel kept a bottom
   padding the composer floated that far clear of the plate's bottom edge and
   chips scrolled visibly through the gap underneath it (MEASURED at 375x667:
   panel bottom 617, composer bottom 607, and a SAFE badge plus a chip border
   showing in the 10px between them).
   ⚠ THE PANEL'S VERTICAL PADDING IS NOT UNIFORM: the base rule is `14px 24px`
   (:3594) and the narrow-phone bracket re-sets it to 10px top AND bottom
   (:3921). The bottom half therefore has to be zeroed in BOTH places, and the
   composer's matching inset restated per width, or the width whose bracket was
   missed reopens the gap. That is exactly how this shipped broken the first
   time. Horizontal padding is untouched at both widths. */
html[data-screen="play"] .actions-panel { padding-bottom: 0; }
/* Desktop: mirror the dock's rail-track reservation (REVIEW CR-05) so the
   panel's content column sits exactly over the prose column and the composer.
   The plate itself stays full-bleed, like the dock plate under it. */
@media (min-width: 769px) {
  html[data-screen="play"] .actions-panel { padding-right: calc(24px + var(--shell-rail-w)); }
}
@media (max-width: 768px) {
  html[data-screen="play"] .actions-panel { padding-left: 16px; padding-right: 16px; }
}

/* AAA SHELL — the P38 cluster boxes, per-group trigger hues, risk left edges and the
   "PANEL DENSITY" pass that used to live here are SUPERSEDED by the numbered choice list
   in the "AAA SHELL LAYER" section near the end of this file (the cluster markup is gone
   from play.jsx; the risk tags keep their lib-derived classes). */

/* REVIEW WR-01 (D-35-05) — THE FIXED TOP-RIGHT CHROME vs THE NOW-PINNED HEADER.
   .tweaks-fab (position: fixed; z-index: 50) and .login-badge (fixed; z-index: 49) are
   rendered by App's topChrome INSIDE #root, so they share #root's stacking context with
   .shell-hdr (z-index 2) and win outright. The header reserved no right-hand gutter.
   MEASURED (headless Chromium, this build, both viewports):
     1400x900 — badge 1032.8..1340 (307.2px WIDE), fab 1350..1386, header cluster ends
                at 1376. The badge covers ALL THREE controls: "T7", "Chronicle settings"
                and "Back to menu"; the fab covers "Back to menu" as well.
     375x667  — badge 26.7..315 (288.3px), fab 321..365, cluster ends at 359. Same three.
   Before Phase 35 this was cosmetically survivable because .top-bar scrolled away with
   the document. It is now a permanently pinned grid row carrying live meters and pips,
   so the overlap can never be scrolled out from under.

   WHY THE BADGE IS HIDDEN RATHER THAN GIVEN A GUTTER. Reserving space was the first
   thing tried and the measurement kills it: the pill is 307px on desktop and 288px on a
   375px phone, so the reservation would be ~367px of a 1400px header and ~348px of a
   375px one — most of the header, to display a key suffix during play. The pill and its
   "Log out" button stay on EVERY other screen and are one tap away from the play screen
   via the header's existing "Back to menu" button, which is now unobstructed.

   THE FAB STAYS. It is 36-44px, it is the only route to the Tweaks panel (palette/tone)
   from the play screen, and a 60px gutter clears it with room to spare — cluster right
   edge 1340 vs fab left 1350 on desktop, 315 vs 321 at 375px.

   padding-right is a LONGHAND declared AFTER the 860px block's `padding: 12px 16px`
   shorthand, for exactly the reason that block's own comment gives about padding-top:
   a shorthand later in source resets it. Do not fold this into either bracket.       */
html[data-screen="play"] .login-badge { display: none; }
html[data-screen="play"] .shell-hdr { padding-right: 60px; }

/* Compass / ornamental corners */
/* v1.7 P39 (CHROME-03): the pre-play screens' answer to .shell-corner. These
   brackets were --rule at 50% opacity — grey ghosts, invisible on the dark
   palettes — while the play screen shipped real gilt flourishes. Same derivation
   as .play-shell's --shell-gilt (color-mix on --gold, muted toward the palette's
   own ground) and the opacity lifts because the colour is no longer a hairline
   grey. --rule keeps its `color:` consumer at .ornament, which matters:
   test/v17-theme-contrast.test.js's LIT_TEXT_EXCLUSIONS entry for --rule asserts
   the token still appears as a `color:` value somewhere. */
.folio-corner {
  position: absolute;
  width: 40px;
  height: 40px;
  color: color-mix(in srgb, var(--gold) 70%, var(--paper));
  opacity: 0.75;
  pointer-events: none;
}
.folio-corner.tl { top: 18px; left: 18px; }
.folio-corner.tr { top: 18px; right: 18px; transform: scaleX(-1); }
.folio-corner.bl { bottom: 18px; left: 18px; transform: scaleY(-1); }
.folio-corner.br { bottom: 18px; right: 18px; transform: scale(-1,-1); }

/* Tabs */
.tab-row {
  display: flex;
  gap: 0;
  border-bottom: 1px solid var(--rule);
  margin-bottom: 18px;
}

.tab {
  font-family: 'Cinzel', serif;
  font-size: 13px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  padding: 10px 14px;
  cursor: pointer;
  color: var(--ink-faded);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: all 150ms;
}

.tab:hover { color: var(--ink); }

/* v1.7 P39 (CHROME-03): the active tab's underline is gilt, matching the
   selected-chip and selected-choice edges. */
.tab.active {
  color: var(--ink);
  border-bottom-color: var(--gold);
}

/* Selection grid (wizard) */
.choice-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 14px;
}

/* v1.7 P39 (CHROME-02): wizard choice cards join the panel tier.
   SELECTED is a gilt double-rule (border + inset hairline) over the raised
   surface, NOT the --cta-* triad: a .choice contains --ink title text and
   --ink-2 body text, and --cta-fill is the dark ink slab on parchment, which
   would put ink on ink. The triad is for elements whose LABEL is --cta-ink. */
.choice {
  border: 1px solid var(--border-soft);
  padding: 18px;
  cursor: pointer;
  transition: all 180ms;
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop, none);
          backdrop-filter: var(--panel-backdrop, none);
  position: relative;
}

.choice:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-hover);
  transform: translateY(-1px);
}

.choice.selected {
  border-color: var(--cta-edge);
  background: var(--paper-deep);
  box-shadow: inset 0 0 0 1px var(--cta-edge), var(--glow-gilt);
}

.choice .title {
  font-family: 'Cinzel', serif;
  font-size: 16px;
  letter-spacing: 0.1em;
  margin-bottom: 6px;
}

.choice .desc {
  font-size: 17px;
  color: var(--ink-2);
  line-height: 1.5;
  font-family: 'EB Garamond', serif;
}

/* Item list */
.item-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px dotted var(--rule);
  font-size: 17px;
}

.item-row .name { font-family: 'EB Garamond', serif; font-weight: 500; }
.item-row .qty { font-family: 'JetBrains Mono', monospace; font-size: 15px; color: var(--ink-faded); }
.item-row .note { font-size: 15px; color: var(--ink-faded); font-style: italic; grid-column: 1 / -1; margin-top: 2px; }

/* Sticker / wax seal */
.seal {
  position: absolute;
  top: -14px;
  right: -14px;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #a02828, var(--accent) 60%, #4a0e0e);
  box-shadow: 0 4px 14px rgba(0,0,0,0.3);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--paper);
  font-family: 'Cinzel', serif;
  font-size: 12px;
  letter-spacing: 0.15em;
  transform: rotate(-8deg);
}

/* Thinking / loading state */
.thinking {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: 'EB Garamond', serif;
  font-style: italic;
  color: var(--ink-faded);
  padding: 8px 0;
}

/* Fade mask scroll */
.feed {
  display: flex;
  flex-direction: column;
  gap: 28px;
  padding-bottom: 40px;
}

/* API key banner */
.banner {
  background: var(--paper-deep);
  border: 1px solid var(--rule);
  padding: 12px 18px;
  font-size: 16px;
  color: var(--ink-2);
  font-style: italic;
  margin-bottom: 20px;
}

/* World creation specific */
.hero-title {
  font-family: 'Cinzel', serif;
  font-size: clamp(42px, 6vw, 72px);
  letter-spacing: 0.08em;
  font-weight: 600;
  line-height: 1.05;
  margin-bottom: 10px;
  color: var(--ink);
}

.hero-sub {
  font-family: 'Cormorant Garamond', serif;
  font-style: italic;
  font-size: 24px;
  color: var(--ink-2);
  max-width: 560px;
  line-height: 1.4;
}

/* Stepper */
.stepper {
  display: flex;
  align-items: center;
  gap: 18px;
  font-family: 'Cinzel', serif;
  font-size: 13px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-faded);
  margin-bottom: 32px;
}

.stepper .step {
  display: flex;
  align-items: center;
  gap: 8px;
}

.stepper .dot {
  width: 7px;
  height: 7px;
  background: var(--rule);
  transform: rotate(45deg);
}

.stepper .step.active {
  color: var(--ink);
}

/* v1.7 P39 (CHROME-03): the stepper reads as a gilt progress rail — the current
   step is the full metal, completed steps are its muted echo, pending steps stay
   --rule. Derived with color-mix on --gold rather than a new token, the idiom
   .play-shell's gilt tiers established at styles.css:2809. */
/* NO --glow-gilt here, deliberately. A 14px blur behind a 7px rotated square
   renders as a visible halo BOX rather than a bloom (measured at 390px: a grey
   rounded square around the active diamond), because the shadow is cast by the
   rotated border box and the stepper's `overflow-x: auto` clips it unevenly. The
   metal alone is enough signal at this size. */
.stepper .step.active .dot {
  background: var(--gold);
}

.stepper .step.done .dot {
  background: color-mix(in srgb, var(--gold) 55%, var(--paper));
}

.stepper .sep {
  flex: 1;
  height: 1px;
  background: var(--rule);
  max-width: 40px;
  opacity: 0.5;
}

/* Menu screen — MOVED. v1.9 front door: the menu, the key screen, the world
   picker and the save rows (.menu-screen, .world-card, .save-row) are styled in
   the FRONT DOOR block at the end of this file, beside the interview they share
   a design system with. Nothing about them is declared here any more. */

/* World lore cards */
.lore-entry {
  margin-bottom: 20px;
  padding-bottom: 16px;
  border-bottom: 1px dotted var(--rule);
}

.lore-entry:last-child { border-bottom: none; }

.lore-entry .cat {
  font-family: 'Cinzel', serif;
  font-size: 13px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 6px;
}

.lore-entry .name {
  font-family: 'Cormorant Garamond', serif;
  font-weight: 600;
  font-size: 21px;
  margin-bottom: 6px;
  line-height: 1.25;
  color: var(--ink);
}

.lore-entry .desc {
  font-size: 18px;
  color: var(--ink-2);
  line-height: 1.55;
}

/* Scroll mask */
.scroll-fade {
  mask-image: linear-gradient(to bottom, transparent 0, black 24px, black calc(100% - 24px), transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 24px, black calc(100% - 24px), transparent 100%);
}

/* P12: 3-state cache-health pill in SettingsModal Token Ledger.
   cacheHealthState(c) computes the data-state from the telemetry ledger.
   'healthy'    = green (var(--accent-2) — aged green; observatory palette
                  switches to muted blue automatically via :root override)
   'warming'    = amber #d97706 (no palette token; literal across both palettes
                  to keep the in-between state visually distinct)
   'not_active' = red (var(--danger, #b03030) — same fallback shape as the
                  P9 chip's danger color)
*/
.cache-health-pill {
  display: inline-block;
  font-size: 15px;
  padding: 2px 10px;
  border-radius: 999px;
  border: 1px solid currentColor;
  font-family: 'EB Garamond', serif;
  letter-spacing: 0.01em;
  cursor: help;
}
.cache-health-pill::before {
  content: '\25CF';
  margin-right: 4px;
  font-size: 17px;
  vertical-align: middle;
  line-height: 0;
}
.cache-health-pill[data-state="healthy"] {
  color: var(--accent-2);
}
.cache-health-pill[data-state="warming"] {
  color: #d97706;
}
.cache-health-pill[data-state="not_active"] {
  color: #fff;
  background: var(--danger, #b03030);
  border-color: var(--danger, #b03030);
}

/* ============================================================================
   AAA SURFACES — SHEETS: the settings sheet, the level-up sheet and the confirm
   dialog share one anatomy (DESIGN.md §4 Modal/sheet):
     · an OPAQUE sheet (--sheet-fill: the raised surface flattened over the
       page colour) — the r1 glass let prose and the chronicle list read
       through every dialog;
     · a darker, blurred scrim (.sheet-backdrop);
     · a 1px gilt hairline on all four sides, --radius-2, --elev-3;
     · a 44px round close with a 20px, 1.5-stroke ×, gold focus ring;
     · buttons in the design system's anatomy: serif, sentence case, 44px;
     · sticky bars fade the content that scrolls under them instead of slicing
       it (a gradient in the sheet's own colour), and on a phone every sheet is
       a bottom sheet with the scrim visible above it.
   ============================================================================ */
.modal-bg.sheet-backdrop {
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
}
.modal.settings-modal,
.modal.levelup-modal,
.modal.confirm-modal {
  background: var(--sheet-fill);
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  border: 1px solid color-mix(in srgb, var(--gold) 30%, var(--border-soft));
  border-radius: var(--radius-2);
  box-shadow: var(--elev-3), 0 0 0 1px rgba(0, 0, 0, 0.25);
}
/* The sheets carry their own frame; the folio corner ticks collided with the
   round close button and orphaned under the bars. */
.settings-modal .folio-corner,
.levelup-modal .folio-corner,
.confirm-modal .folio-corner { display: none; }
.modal .modal-close.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: var(--tap);
  height: var(--tap);
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0;
  border-radius: var(--radius-round);
  border: 1px solid var(--border-soft);
  color: var(--ink-2);
  background: transparent;
}
.modal .modal-close.btn:hover {
  border-color: color-mix(in srgb, var(--gold) 60%, transparent);
  color: var(--ink);
  background: var(--hover-wash);
}
/* Buttons inside the sheets and notices: the design system's anatomy
   (DESIGN.md §4 Button). The base .btn stays Cinzel caps for the screens
   other workstreams own. */
.settings-modal .btn:not(.modal-close),
.levelup-modal .btn:not(.modal-close),
.confirm-modal .btn:not(.modal-close),
.banner .banner-actions .btn,
.soft-delete-toast .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: 0 var(--space-5);
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 500;
  letter-spacing: 0.01em;
  text-transform: none;
  border-radius: var(--radius-1);
}
.settings-modal .btn-primary,
.levelup-modal .btn-primary,
.banner .banner-actions .btn-primary { font-weight: 600; }

/* ---- The confirm dialog (menu: "Burn this chronicle?") ---- */
.modal.confirm-modal {
  max-width: 520px;
  padding: var(--space-5) var(--space-6) var(--space-6);
}
.confirm-modal .modal-head { margin-bottom: var(--space-3); align-items: flex-start; }
.confirm-modal .modal-title { font-size: 22px; line-height: 1.25; padding-top: 10px; }
.confirm-body {
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  line-height: 1.55;
  color: var(--ink-2);
}
.confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-3);
  margin-top: var(--space-6);
}
.confirm-actions .btn:not(.btn-danger) { border-color: var(--border-strong); color: var(--ink); }

/* ---- The settings sheet (SettingsModal, play.jsx) ----
   A sticky title bar, a one-line lede, three groups (Look / The story / Saves)
   whose rows are label | control | hint, and a closed "Advanced" disclosure
   whose row states the live model, effort and cost. The sheet (.modal) stays the
   scroller; the title bar and the Done footer are sticky and fade what passes
   under them. */
.modal.settings-modal {
  max-width: 760px;
  padding: 0 var(--space-6);
}
.settings-modal .modal-head {
  position: sticky;
  top: 0;
  z-index: 3;
  margin: 0 calc(-1 * var(--space-6));
  padding: var(--space-4) var(--space-4) var(--space-3) var(--space-6);
  background: var(--sheet-fill);
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 24%, transparent);
}
/* Fade in the sheet's own colour: the raised surface over the page colour,
   each fading out — so on the glass palettes the fade composites exactly like
   the opaque bar above it. */
.settings-modal .modal-head::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: var(--space-5);
  pointer-events: none;
  background:
    linear-gradient(to bottom, var(--surface-raised), transparent),
    linear-gradient(to bottom, var(--paper), transparent);
}
.settings-modal .modal-title {
  font-size: var(--text-title);
  letter-spacing: 0.03em;
  color: var(--ink);
}
.settings-lede {
  margin: var(--space-5) 0 0;
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  line-height: 1.45;
  color: var(--ink-2);
}
.settings-group { margin-top: var(--space-6); }
/* Section label: serif small caps (>= 17px, per the design system), a gilt
   lozenge in the margin and a hairline that fades out. */
.levelup-section-title,
.settings-group-title {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0 0 var(--space-2);
  font-family: var(--font-serif);
  font-size: 18px;
  font-weight: 500;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.1em;
  line-height: 1.2;
  color: var(--ink-2);
}
.settings-group-title::before {
  content: '';
  flex: 0 0 auto;
  width: 5px;
  height: 5px;
  background: var(--gold);
  opacity: 0.8;
  transform: rotate(45deg);
}
.levelup-section-title::after,
.settings-group-title::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--gold) 38%, transparent), transparent);
}
.setting-row {
  display: grid;
  grid-template-columns: 150px minmax(0, 1fr);
  column-gap: var(--space-5);
  align-items: start;
  padding: var(--space-4) 0;
  border-top: 1px solid color-mix(in srgb, var(--gold) 12%, transparent);
}
.settings-group-title + .setting-row { border-top: 0; }
.settings-modal .setting-label {
  margin: 0;
  padding-top: 10px;
  font-family: var(--font-serif);
  font-size: 18px;
  font-weight: 500;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.08em;
  text-transform: none;
  line-height: 1.3;
  color: var(--ink-2);
}
.setting-control { min-width: 0; }
.setting-chips { flex-wrap: wrap; }
.settings-modal .chip {
  gap: var(--space-2);
  min-height: var(--tap);
  padding: var(--space-2) var(--space-4);
  font-family: var(--font-serif);
  font-size: var(--text-md);
  letter-spacing: 0.01em;
  text-transform: none;
  line-height: 1.2;
  color: var(--ink-2);
  background: var(--tint-sunken);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-1);
}
.settings-modal .chip:hover {
  color: var(--ink);
  background: color-mix(in srgb, var(--gold) 6%, transparent);
  border-color: color-mix(in srgb, var(--gold) 45%, transparent);
}
.settings-modal .chip.active {
  background: var(--cta-fill);
  color: var(--cta-ink);
  border-color: var(--cta-edge);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--gold) 30%, transparent);
}
.settings-modal .chip .chip-check { flex: 0 0 auto; margin-left: auto; }
/* Palette choices are swatch tiles on a 2x2 grid at every width: a real preview
   of each palette (its page, a line of its ink, a dot of its metal). The hexes
   are literal on purpose — a swatch shows ANOTHER palette's colours, so it
   cannot read the current palette's tokens. data-swatch, never data-palette
   (the palette selector count is gated by the theme contract). */
.settings-modal .setting-swatches {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
}
.settings-modal .setting-swatches .chip {
  justify-content: flex-start;
  min-height: 52px;
  padding: var(--space-2) var(--space-3);
  gap: var(--space-3);
}
.swatch {
  position: relative;
  flex: 0 0 auto;
  width: 34px;
  height: 24px;
  border-radius: var(--radius-1);
  box-shadow: inset 0 0 0 1px rgba(128, 128, 128, 0.45);
  overflow: hidden;
}
.swatch-ink {
  position: absolute;
  left: 6px;
  right: 12px;
  top: 8px;
  height: 2px;
  border-radius: 1px;
  box-shadow: 0 5px 0 0 currentColor;
  background: currentColor;
  opacity: 0.85;
}
.swatch-metal {
  position: absolute;
  right: 5px;
  bottom: 5px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
}
.swatch[data-swatch="parchment"]   { background: #f1e8d4; color: #1c1410; }
.swatch[data-swatch="parchment"] .swatch-metal   { background: #7a1e1e; }
.swatch[data-swatch="observatory"] { background: #111836; color: #e7e3d6; }
.swatch[data-swatch="observatory"] .swatch-metal { background: #e6c97a; }
.swatch[data-swatch="dark-glass"]  { background: #1a1e25; color: #dfe6ef; }
.swatch[data-swatch="dark-glass"] .swatch-metal  { background: #e8c06a; }
.swatch[data-swatch="arcane"]      { background: #191328; color: #e6e0f2; }
.swatch[data-swatch="arcane"] .swatch-metal      { background: #c9a0ff; }
.setting-hint {
  margin-top: var(--space-2);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-style: normal;
  line-height: 1.45;
  color: var(--ink-faded);
}
.setting-count { text-align: right; font-variant-numeric: lining-nums tabular-nums; }
.settings-modal .setting-textarea {
  display: block;
  min-height: 96px;
  padding: var(--space-3) var(--space-4);
  font-family: var(--font-serif);
  font-size: 18px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--tint-sunken);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-1);
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.settings-modal .setting-textarea::placeholder { color: var(--ink-faded); font-style: italic; opacity: 1; }
.settings-modal .setting-textarea:focus {
  border-color: var(--gold);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 18%, transparent);
}
.setting-select {
  appearance: none;
  -webkit-appearance: none;
  min-width: 260px;
  max-width: 100%;
  min-height: var(--tap);
  padding: 0 44px 0 var(--space-4);
  font-family: var(--font-serif);
  font-size: var(--text-md);
  color: var(--ink);
  background-color: var(--tint-sunken);
  /* a two-stroke chevron drawn in currentColor, so it follows the palette */
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 22px) 50%, calc(100% - 16px) 50%;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-1);
  cursor: pointer;
}
.setting-select option { background: var(--paper); color: var(--ink); }
/* A drawn checkbox: the real <input> is stretched invisibly over the WHOLE row
   (so the row is the tap target), and the box beside the title shows its state
   — hairline when off, a gilt plate with a check when on. */
.setting-toggle {
  position: relative;
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr);
  column-gap: var(--space-4);
  padding: var(--space-4) 0;
  border-top: 1px solid color-mix(in srgb, var(--gold) 12%, transparent);
  cursor: pointer;
}
.setting-toggle .setting-toggle-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}
.setting-toggle-box {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  margin-top: 3px;
  border-radius: var(--radius-1);
  border: 1.5px solid var(--border-strong);
  background: var(--tint-sunken);
  color: transparent;
  transition: background var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.setting-toggle-input:checked + .setting-toggle-box {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--bg);
}
.setting-toggle:hover .setting-toggle-box { border-color: color-mix(in srgb, var(--gold) 60%, transparent); }
.setting-toggle-input:focus-visible + .setting-toggle-box { outline: 2px solid var(--gold); outline-offset: 2px; }
.setting-toggle-text { display: flex; flex-direction: column; }
.setting-toggle-head { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--space-3); row-gap: var(--space-1); }
.setting-toggle-title {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 500;
  color: var(--ink);
}
.setting-toggle-text .setting-hint { margin-top: var(--space-1); }
.setting-tag {
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-style: italic;
  color: var(--ink-faded);
}
/* Saves */
.save-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-5);
  padding: var(--space-3) 0 var(--space-4);
}
.save-action { display: flex; flex-direction: column; }
.settings-modal .save-btn.btn {
  width: 100%;
  min-height: 48px;
  border-color: color-mix(in srgb, var(--gold) 40%, transparent);
  color: var(--ink);
}
.settings-modal .save-btn.btn svg { color: var(--gold); }
.settings-modal .save-btn.btn:hover { border-color: var(--gold); background: color-mix(in srgb, var(--gold) 8%, transparent); }
.setting-empty { margin-top: 0; padding-top: 10px; }
/* Advanced — a closed <details> the size of one row, whose subtitle carries the
   live values it hides. */
.settings-advanced {
  margin-top: var(--space-6);
  border-top: 1px solid color-mix(in srgb, var(--gold) 24%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 24%, transparent);
}
.settings-advanced-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: 68px;
  padding: var(--space-3) var(--space-1) var(--space-3) 0;
  list-style: none;
  cursor: pointer;
}
.settings-advanced-summary::-webkit-details-marker { display: none; }
.settings-advanced-summary::marker { content: ''; }
.settings-advanced-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.settings-advanced-title {
  font-family: var(--font-serif);
  font-size: 18px;
  font-weight: 500;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.1em;
  color: var(--ink-2);
}
.settings-advanced-sub {
  font-family: var(--font-serif);
  font-size: var(--text-md);
  color: var(--ink);
  font-variant-numeric: lining-nums tabular-nums;
}
.settings-advanced-summary .chevron { flex: 0 0 auto; color: var(--ink-faded); transition: transform var(--dur-base) var(--ease-out); }
.settings-advanced[open] > .settings-advanced-summary .chevron { transform: rotate(180deg); color: var(--ink); }
.settings-advanced-summary:hover .settings-advanced-title { color: var(--ink); }
.settings-advanced-body { padding-bottom: var(--space-4); }
.settings-advanced-body > .setting-row:first-child { border-top: 1px solid color-mix(in srgb, var(--gold) 12%, transparent); }
/* Token ledger inside Advanced: serif lining figures, gilt links (were UA blue). */
.token-ledger {
  margin-top: var(--space-1);
  padding-top: var(--space-4);
  border-top: 1px solid color-mix(in srgb, var(--gold) 12%, transparent);
  font-size: var(--text-md);
}
.token-ledger .setting-label { padding-top: 0; margin-bottom: var(--space-3); }
.token-ledger .mono {
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: 0;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink);
}
.ledger-stat-tag {
  display: inline-block;
  padding: 2px 10px;
  font-size: var(--text-sm);
  color: var(--ink-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-round);
}
.settings-modal a {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--gold) 55%, transparent);
  text-underline-offset: 3px;
}
.settings-modal a:hover { text-decoration-color: var(--gold); }
.settings-foot {
  position: sticky;
  bottom: 0;
  z-index: 3;
  display: flex;
  justify-content: flex-end;
  margin: var(--space-6) calc(-1 * var(--space-6)) 0;
  padding: var(--space-3) var(--space-6) var(--space-4);
  background: var(--sheet-fill);
  border-top: 1px solid color-mix(in srgb, var(--gold) 24%, transparent);
}
.settings-foot::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: var(--space-6);
  pointer-events: none;
  background:
    linear-gradient(to top, var(--surface-raised), transparent),
    linear-gradient(to top, var(--paper), transparent);
}
.settings-modal .settings-done.btn { min-width: 140px; }
/* Phone: a group of 4 is 2x2, of 3/5/6 is rows of 3, of 2 (long phrases) is a
   column — never one chip orphaned on its own row. */
@media (max-width: 768px) {
  .setting-row { grid-template-columns: 1fr; row-gap: var(--space-2); }
  .settings-modal .setting-label { padding-top: 0; }
  .settings-modal .setting-chips:not(.setting-swatches) { display: grid; gap: var(--space-2); }
  .settings-modal .setting-chips[data-n="2"] { grid-template-columns: 1fr; }
  .settings-modal .setting-chips[data-n="4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .settings-modal .setting-chips[data-n="3"],
  .settings-modal .setting-chips[data-n="5"],
  .settings-modal .setting-chips[data-n="6"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .settings-modal .setting-chips:not(.setting-swatches) .chip { justify-content: center; padding: var(--space-2); }
  /* the selected check sits in the corner so the label stays centred */
  .settings-modal .setting-chips:not(.setting-swatches) .chip { position: relative; }
  .settings-modal .setting-chips:not(.setting-swatches) .chip .chip-check { position: absolute; right: var(--space-2); top: 50%; transform: translateY(-50%); }
  .save-actions { grid-template-columns: 1fr; gap: var(--space-4); }
  .setting-select { min-width: 0; width: 100%; }
  .settings-modal .settings-done.btn { width: 100%; }
}
/* Phone sheets: anchored to the bottom (thumb reach) with the scrim visible
   above; the level-up sheet overrides this to full height. */
@media (max-width: 480px) {
  .modal-bg.sheet-backdrop {
    align-items: flex-end;
    padding: calc(var(--space-4) + env(safe-area-inset-top)) 0 0;
  }
  .modal-bg.sheet-backdrop > .modal {
    width: 100%;
    max-width: none;
    max-height: 100%;
    border-radius: var(--radius-2) var(--radius-2) 0 0;
    border-bottom: 0;
  }
  .modal.settings-modal { padding: 0 var(--space-4); }
  .settings-modal .modal-head { margin: 0 calc(-1 * var(--space-4)); padding: var(--space-3) var(--space-2) var(--space-3) var(--space-4); }
  .settings-modal .modal-title { font-size: 22px; }
  .settings-foot {
    margin: var(--space-5) calc(-1 * var(--space-4)) 0;
    padding: var(--space-3) var(--space-4) calc(var(--space-3) + env(safe-area-inset-bottom));
  }
  .modal.confirm-modal { padding: var(--space-4) var(--space-4) calc(var(--space-5) + env(safe-area-inset-bottom)); }
  .confirm-actions .btn { flex: 1; }
}

/* ============================================================
   P15 — Ledger Overhaul (Phase 15)
   Side-pane typography rise, container query for stat compact
   mode, and foundational classes consumed by Plans 03-06.
   All rules strictly qualified with .side-pane per CONTEXT D-20.
   ============================================================ */

/* Container-query host + base typography rise (D-14, D-18). */
.side-pane {
  container-type: inline-size;
  container-name: pane;
  font-size: 20px;
  line-height: 1.55;
}

/* HUD aesthetic — tab row is the persistent nav across the four readouts.
   Larger tap target + heavier letter-spacing makes it read as a
   game-status bar rather than a tab strip. */
.side-pane .tab-row .tab {
  font-size: 18px;
  letter-spacing: 0.18em;
  padding-top: 10px;
  padding-bottom: 10px;
}

/* ============================================================================
   AAA SURFACES — THE LEDGER'S CONTENTS (Self / Pack / Vows / Book).
   Built to DESIGN.md: EB Garamond for everything read (Cinzel only for the
   character's name and the level numeral), labels as serif small caps at
   18px, numbers lining + tabular, colour by ROLE — HP --danger, MP
   --trigger-skill, SP --ink-2, gold only where it means "you" (level,
   inspiration, the waiting level-up, active vows). One heading system:
     · .ledger-eyebrow — small caps --ink-2, a gilt lozenge in the margin and a
       hairline that fades out; collapsible section headers are the same thing
       plus a count and a ⌄ chevron (rotated when open);
     · entries are NOT boxes: a title over its line of context, ruled off from
       the next by a gilt hairline.
   ============================================================================ */
.side-pane .ledger-eyebrow {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0 0 var(--space-3);
  font-family: var(--font-serif);
  font-size: 18px;
  font-weight: 500;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.1em;
  line-height: 1.2;
  color: var(--ink-2);
}
.side-pane .ledger-eyebrow::before {
  content: '';
  flex: 0 0 auto;
  width: 5px;
  height: 5px;
  background: var(--gold);
  opacity: 0.8;
  transform: rotate(45deg);
}
.side-pane .ledger-eyebrow::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--gold) 38%, transparent), transparent);
}

/* Ledger entries — Vows, Book, Threads, agreements. */
.side-pane .item-row,
.side-pane .lore-entry {
  padding: var(--space-3) 0 var(--space-4);
  margin: 0;
  border: 0;
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 16%, transparent);
  background: none;
  border-radius: 0;
}
.side-pane .lore-entry:last-child { border-bottom: 0; }
/* Pack/Book item names are the visible content when a section is expanded.
   The collapsibility we shipped in P15 exists exactly to let these breathe
   — bigger here, hidden when the category is collapsed. */
.side-pane .item-row .name,
.side-pane .lore-entry .name {
  font-family: var(--font-serif);
  font-size: 20px;
  font-weight: 500;
  line-height: 1.28;
  color: var(--ink);
  margin-bottom: var(--space-1);
  text-wrap: pretty;
}
.side-pane .item-row .qty {
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--ink-2);
  font-family: var(--font-serif);
  font-variant-numeric: lining-nums tabular-nums;
}
.side-pane .item-row .note,
.side-pane .lore-entry .desc {
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--ink-2);
  font-style: normal;
  margin-top: 2px;
  text-wrap: pretty;
}
.side-pane .lore-entry .cat {
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--ink-faded);
  text-transform: none;
  font-family: var(--font-serif);
  font-style: italic;
  margin-bottom: var(--space-1);
  display: block;
}

/* Vows: an ACTIVE vow is marked with a small gilt lozenge hung in the margin —
   the player's open commitments are the one gold thing on the page. */
.side-pane .vow-entry.is-active { position: relative; padding-left: 20px; }
.side-pane .vow-entry.is-active::before {
  content: '';
  position: absolute;
  left: 2px;
  top: 23px;
  width: 7px;
  height: 7px;
  background: var(--gold);
  transform: rotate(45deg);
  box-shadow: 0 0 8px color-mix(in srgb, var(--gold) 55%, transparent);
}
.side-pane .vow-entry.is-done .name { color: var(--ink-2); }
.side-pane .vow-entry.is-done .desc { color: var(--ink-faded); }
/* Status words are quiet annotations, not boxes (tags are boxed only when they
   are interactive — DESIGN.md §4). */
.side-pane .vow-tag {
  display: inline;
  margin-left: var(--space-2);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-style: italic;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-faded);
  white-space: nowrap;
}
.side-pane .vow-tag::before { content: '· '; font-style: normal; }
.side-pane .vow-entry.is-failed .vow-tag { color: var(--danger); }

/* Section labels in the side-pane (Attributes / Traits / Flaws / Level & XP)
   read bigger and bolder so they anchor each HUD block. */
.side-pane .label {
  font-size: 16px;
  letter-spacing: 0.22em;
  color: var(--ink-2);
}

/* ---------- Self tab: the character sheet ---------- */
.side-pane .self-tab { display: flex; flex-direction: column; padding-bottom: var(--space-6); }
.side-pane .self-section { margin-top: var(--space-6); }
.side-pane .self-head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding-bottom: var(--space-5);
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 22%, transparent);
}
.side-pane .self-ident { flex: 1; min-width: 0; }
.side-pane .self-name {
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: 0.03em;
  line-height: 1.15;
  color: var(--ink);
  text-wrap: balance;
}
.side-pane .self-epithet {
  margin-top: var(--space-2);
  font-family: var(--font-serif);
  font-style: italic;
  font-size: var(--text-lg);
  line-height: 1.35;
  color: var(--ink-2);
  text-wrap: balance;
}
/* Vitals — the header HUD's anatomy (SHELL): role-coloured icon, sentence-case
   label, lining value with a receding max, a 4px round bar on a sunken track
   (the fills come from the base .meter-fill.* role colours). */
.side-pane .self-vitals { margin-top: var(--space-5); display: flex; flex-direction: column; gap: var(--space-4); }
.side-pane .vital-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}
.side-pane .vital-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--ink-2);
}
.side-pane .vital-icon { display: inline-flex; width: 20px; justify-content: center; }
.side-pane .vital-hp .vital-icon   { color: var(--danger); }
.side-pane .vital-mana .vital-icon { color: var(--trigger-skill); }
.side-pane .vital-stam .vital-icon { color: var(--ink-2); }
.side-pane .inspiration-block .vital-icon { color: var(--gold); }
.side-pane .vital-val {
  font-family: var(--font-serif);
  font-size: 20px;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink);
}
.side-pane .vital-max {
  font-weight: 400;
  font-size: var(--text-sm);
  color: var(--ink-faded);
  margin-left: 1px;
}
.side-pane .inspiration-block .vital-row { margin-bottom: 0; }
.side-pane .vital-meter {
  height: 4px;
  border: 0;
  border-radius: var(--radius-round);
  background: var(--tint-sunken);
}
.side-pane .vital-meter .meter-fill { border-radius: var(--radius-round); }
/* Attributes — an engraved plate. Interior hairlines only, one gilt tint in both
   directions: each cell draws its left and top rule shifted 1px out, and the
   grid clips the outer ones. */
.side-pane .attr-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  overflow: hidden;
}
.side-pane .attr-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-height: 80px;
  padding: var(--space-3) var(--space-1);
  margin: -1px 0 0 -1px;
  border-left: 1px solid color-mix(in srgb, var(--gold) 18%, transparent);
  border-top: 1px solid color-mix(in srgb, var(--gold) 18%, transparent);
  text-align: center;
}
.side-pane .attr-val {
  font-family: var(--font-serif);
  font-size: 30px;
  font-weight: 500;
  line-height: 1;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink);
}
.side-pane .attr-name {
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--ink-faded);
  max-width: 100%;
  overflow-wrap: anywhere;
}
/* The desktop rail is ~270px of content: two columns there, three in the phone
   drawer. */
@container pane (max-width: 330px) {
  .side-pane .attr-grid { grid-template-columns: repeat(2, 1fr); }
  .side-pane .attr-cell { min-height: 76px; }
}
/* Traits, weaknesses, flaws */
.side-pane .trait-list { list-style: none; margin: 0; padding: 0; }
.side-pane .trait-item {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: var(--space-3) 0;
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 14%, transparent);
}
.side-pane .trait-item:first-child { padding-top: 2px; }
.side-pane .trait-item:last-child { border-bottom: 0; }
.side-pane .trait-name {
  font-family: var(--font-serif);
  font-size: 19px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
}
.side-pane .trait-tag {
  margin-left: var(--space-2);
  font-weight: 400;
  font-style: italic;
  font-size: var(--text-sm);
  color: var(--ink-faded);
}
.side-pane .trait-desc {
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--ink-2);
  text-wrap: pretty;
}
/* Rested / session-pool notes: a quiet line with a gilt tick, not a banner. */
.side-pane .self-note-title {
  font-family: var(--font-serif);
  font-style: normal;
  font-size: 18px;
  font-weight: 500;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.08em;
  margin-right: var(--space-2);
  color: var(--ink);
}

/* ---------- Foundational classes for Plans 03-06 ---------- */

/* Section wrapper used by Pack categories + Book categories + Vows
   sub-sections. Spacing scale per UI-SPEC (xl = 24px between sub-sections). */
.side-pane .ledger-section {
  margin-top: 28px;
}
.side-pane .ledger-section:first-child {
  margin-top: 0;
}

/* Section header — clickable button-styled-as-heading. Tap target ≥ 44pt
   at <768px (P16-friendly). AAA SURFACES: it is the SAME label as
   .ledger-eyebrow (serif small caps, --ink-2, a gilt lozenge) plus a tabular
   count and a ⌄ that turns over when open — one chevron convention across the
   ledger, the skills list and the settings sheet. */
.side-pane .ledger-section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  min-height: 48px;
  background: none;
  border: none;
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 26%, transparent);
  padding: var(--space-2) var(--space-1) var(--space-2) 0;
  margin-bottom: var(--space-1);
  cursor: pointer;
  font-family: var(--font-serif);
  font-size: 18px;
  font-weight: 500;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.1em;
  text-transform: none;
  color: var(--ink-2);
  transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
  text-align: left;
}
.side-pane .ledger-section-header:hover {
  color: var(--ink);
  border-bottom-color: color-mix(in srgb, var(--gold) 50%, transparent);
}
.side-pane .ledger-section-header[aria-expanded="false"] {
  border-bottom-color: color-mix(in srgb, var(--gold) 14%, transparent);
}
.side-pane .ledger-section-header[aria-expanded="true"] { color: var(--ink); }
.side-pane .ledger-section-title { display: inline-flex; align-items: center; gap: var(--space-3); }
.side-pane .ledger-section-title::before {
  content: '';
  width: 5px;
  height: 5px;
  background: var(--gold);
  opacity: 0.8;
  transform: rotate(45deg);
}
.side-pane .ledger-section-header .chevron {
  transition: transform var(--dur-base) var(--ease-out);
  flex-shrink: 0;
  color: var(--ink-faded);
}
.side-pane .ledger-section-header[aria-expanded="true"] .chevron {
  transform: rotate(180deg);
}
.side-pane .ledger-section-header .count {
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-style: normal;
  font-variant-caps: normal;
  color: var(--ink-faded);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  font-variant-numeric: lining-nums tabular-nums;
}

/* Sticky variant used by Book sections only (D-07). Pack does NOT use
   sticky headers (D-04). AAA SURFACES: the desktop rail is a TRANSLUCENT
   glass pane, so an opaque var(--paper) strip read as a black slab pinned over
   it. The header is now a frosted strip of the same glass (the pane's own
   blur and a --paper tint), which also stays opaque enough to hide the entries
   scrolling under it. On the phone drawer --paper is the drawer itself. */
.side-pane .ledger-section-header.sticky {
  position: sticky;
  top: -1px;
  background: linear-gradient(var(--panel-bg), var(--panel-bg)), var(--paper);
  z-index: 1;
  padding-left: 0;
  padding-right: 2px;
}
/* P34 (THEME-02): the per-palette fork that once sat here declared the same
   `background: var(--paper)` as the base rule above — a pure no-op — and was
   deleted rather than tokenised. Do not re-add it. */

/* Section body — collapsible content with subtle 150ms ease-out motion. */
.side-pane .ledger-section-body {
  overflow: hidden;
  transition: max-height 150ms ease-out, opacity 150ms ease-out;
  max-height: 0;
  opacity: 0;
}
.side-pane .ledger-section-body.open {
  /* REQ HYGN-03 (P27) — max-height is now set inline by play.jsx's
     LedgerCollapsibleBody helper, using ref.current.scrollHeight measured
     per section. Was: max-height: 4000px (which clipped Pack/Book sections
     longer than 4000px of content at v1.4-scale skill catalogs / multi-page lore).
     The transition declaration above still animates the inline max-height. */
  opacity: 1;
}

/* ---------- Pack item card (Plan 03 consumes) ---------- */

/* AAA SURFACES: a Pack item is a ROW in a ruled list, not a boxed card — name
   in the reading serif, a tabular ×N on EVERY row, and its note clamped to two
   lines with a ⌄ when the clamp hides text (PackItemRow measures it). The whole
   row is the toggle; open = the full note, a gilt wash and a 2px gilt tick. */
.side-pane .ledger-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-1) var(--space-3) var(--space-4);
  margin: 0;
  background: none;
  border: 0;
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 14%, transparent);
  border-radius: 0;
  cursor: pointer;
  transition: background var(--dur-base) var(--ease-out);
  width: 100%;
  text-align: left;
  font-family: inherit;
  color: inherit;
  font-size: 20px;
  line-height: 1.4;
  min-height: 56px;
}
.side-pane .ledger-card::before {
  content: '';
  position: absolute;
  left: 0;
  top: var(--space-3);
  bottom: var(--space-3);
  width: 2px;
  border-radius: 1px;
  background: var(--gold);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
.side-pane .ledger-card:hover {
  background: linear-gradient(90deg, color-mix(in srgb, var(--gold) 8%, transparent), transparent 80%);
}
.side-pane .ledger-card[aria-expanded="true"] {
  background: linear-gradient(90deg, color-mix(in srgb, var(--gold) 10%, transparent), transparent 85%);
}
.side-pane .ledger-card:hover::before,
.side-pane .ledger-card[aria-expanded="true"]::before { opacity: 0.85; }
.side-pane .ledger-card .ledger-card-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-3);
}
.side-pane .ledger-card .ledger-card-name {
  flex: 1;
  min-width: 0;
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: 19px;
  line-height: 1.3;
  color: var(--ink);
}
.side-pane .ledger-card .ledger-card-qty {
  flex-shrink: 0;
  min-width: 2.6ch;
  text-align: right;
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 500;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink-faded);
}
.side-pane .ledger-card .ledger-card-note-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 20px;
  column-gap: var(--space-2);
  align-items: end;
}
.side-pane .ledger-card .ledger-card-note {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: var(--text-md);
  color: var(--ink-2);
  font-style: italic;
  line-height: 1.45;
  margin: 0;
}
.side-pane .ledger-card[aria-expanded="true"] .ledger-card-note {
  display: block;
  -webkit-line-clamp: unset;
  overflow: visible;
  color: var(--ink);
}
.side-pane .ledger-card .ledger-card-chev {
  color: var(--ink-faded);
  margin-bottom: 3px;
  transition: transform var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.side-pane .ledger-card[aria-expanded="true"] .ledger-card-chev { transform: rotate(180deg); color: var(--ink); }

/* The one keepsake: a card (DESIGN.md §4 Card) with a gilt hairline edge —
   the only frame on the Pack tab. Left-aligned throughout; the effect line is a
   16px icon column + text column so wrapped lines hang under the text. */
.side-pane .treasured {
  position: relative;
  margin: 0 0 var(--space-6);
  padding: var(--space-5);
  border: 1px solid color-mix(in srgb, var(--gold) 32%, transparent);
  border-radius: var(--radius-2);
  background:
    radial-gradient(120% 90% at 0% 0%, color-mix(in srgb, var(--gold) 9%, transparent), transparent 70%),
    var(--surface);
  box-shadow: var(--elev-1);
  text-align: left;
}
.side-pane .treasured-eyebrow {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-serif);
  font-size: 18px;
  font-weight: 500;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.1em;
  color: var(--ink-2);
}
.side-pane .treasured-eyebrow::before {
  content: '';
  width: 5px;
  height: 5px;
  background: var(--gold);
  opacity: 0.8;
  transform: rotate(45deg);
}
.side-pane .treasured-name {
  margin-top: var(--space-2);
  font-family: var(--font-serif);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--ink);
  text-wrap: balance;
}
.side-pane .treasured-desc {
  margin-top: var(--space-2);
  font-family: var(--font-serif);
  font-style: italic;
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--ink-2);
  text-wrap: pretty;
}
.side-pane .treasured-hint {
  display: grid;
  grid-template-columns: 16px minmax(0, 1fr);
  column-gap: var(--space-3);
  align-items: start;
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid color-mix(in srgb, var(--gold) 20%, transparent);
  font-size: var(--text-md);
  line-height: 1.45;
  color: var(--ink);
}
.side-pane .treasured-hint-mark { display: inline-flex; margin-top: 3px; color: var(--gold); }

/* ---------- Book search input (Plan 04 consumes) ---------- */

.side-pane .ledger-search {
  position: relative;
  margin-bottom: var(--space-5);
}
.side-pane .ledger-search input {
  width: 100%;
  min-height: 48px;
  background: var(--tint-sunken);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-1);
  padding: var(--space-3) var(--space-4) var(--space-3) 46px;
  font-family: var(--font-serif);
  font-size: 18px;
  color: var(--ink);
  line-height: 1.4;
  outline: none;
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.side-pane .ledger-search input::placeholder { color: var(--ink-faded); opacity: 1; }
.side-pane .ledger-search input:focus {
  border-color: var(--gold);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 18%, transparent);
}
.side-pane .ledger-search .ledger-search-icon {
  position: absolute;
  left: 14px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  color: var(--ink-faded);
  pointer-events: none;
}

/* ---------- Empty-state copy (Plans 03/04/05 consume) ---------- */

.side-pane .ledger-empty {
  font-size: var(--text-md);
  font-style: italic;
  color: var(--ink-faded);
  line-height: 1.5;
  padding: var(--space-2) 0;
}
/* A whole-tab empty state: a centred gilt star over one sentence. */
.side-pane .scene > .ledger-empty:only-child {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-7) var(--space-4);
  text-align: center;
  color: var(--ink-2);
}
.side-pane .ledger-empty-glyph { display: inline-flex; font-style: normal; color: var(--gold); }
.side-pane .ledger-empty-glyph svg path { fill: currentColor; }
/* Every tab's list ends with breathing room, so its last line never sits on the
   panel's bottom edge or under the pane's fade. */
.side-pane .pack-tab,
.side-pane .book-tab,
.side-pane .vows-tab { padding-bottom: var(--space-6); }

/* ---------- XP progress bar (Plan 06 consumes) ---------- */

.side-pane .xp-bar-row {
  margin-top: 6px;
}
.side-pane .xp-bar-label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 17px;
  font-weight: 500;
  margin-bottom: 6px;
  color: var(--ink);
}
.side-pane .xp-bar-label .xp-numeric {
  font-family: 'JetBrains Mono', monospace;
  font-size: 16px;
  color: var(--ink-faded);
  font-weight: 400;
}
.side-pane .xp-bar {
  height: 8px;
  background: var(--paper-deep);
  border: 1px solid var(--rule);
  position: relative;
  overflow: hidden;
}
.side-pane .xp-bar-fill {
  height: 100%;
  background: var(--accent);
  transition: width 400ms ease;
}

/* ---------- Show-more toggle (Plan 06 consumes for traits/flaws) ---------- */

.side-pane .ledger-toggle {
  background: none;
  border: none;
  padding: 6px 0;
  margin-top: 4px;
  font-family: 'EB Garamond', serif;
  font-style: italic;
  font-size: 17px;
  color: var(--ink-faded);
  cursor: pointer;
  transition: color 150ms ease;
}
.side-pane .ledger-toggle:hover {
  color: var(--ink);
}

/* The container-query rule above continues to fire inside the mobile
   bottom-drawer because container-type:inline-size still tracks the
   drawer's actual width. So compact mode triggers correctly on phone-
   width drawers (320/375/414px) without any extra @media work. */

/* Responsive — P16 migration: 980px → 768px (CONTEXT D-01/D-02). The
   block below is the foundation mobile-pass styling. P14's chip-strip
   rules and P16's drawer rules (added later in this same file) coexist
   within the same 768px bracket — different selectors, no conflict. */
@media (max-width: 768px) {
  :root { font-size: 18px; }

  .app-shell {
    grid-template-columns: 1fr;
    min-height: 100dvh;
  }
  .main-pane {
    padding: calc(18px + env(safe-area-inset-top)) 16px 24px;
    min-height: 100dvh;
  }

  /* .hero-title / .hero-sub phone sizes live in the FRONT DOOR block (v1.9):
     every consumer is a front-door screen, and the old !important here beat
     every per-screen size. */
  h1, h2 { letter-spacing: 0.04em; }

  .folio-corner { width: 24px; height: 24px; }
  .folio-corner.tl, .folio-corner.tr { top: 10px; }
  .folio-corner.bl, .folio-corner.br { bottom: 10px; }
  .folio-corner.tl, .folio-corner.bl { left: 10px; }
  .folio-corner.tr, .folio-corner.br { right: 10px; }

  .top-bar {
    padding-bottom: 12px;
    margin-bottom: 18px;
    gap: 8px;
  }
  .brand { font-size: 14px; letter-spacing: 0.2em; }

  .choice-grid { grid-template-columns: 1fr; gap: 10px; }
  /* `.choice, button.choice` — the (0,0,1,1) box re-declaration near the end of
     this file (styles.css:5195) would otherwise beat a bare `.choice` here and
     hold phone cards at the desktop 18px. */
  .choice, button.choice { padding: 14px; }

  .stepper {
    font-size: 12px;
    gap: 8px;
    margin-bottom: 20px;
    overflow-x: auto;
    white-space: nowrap;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 4px;
  }
  .stepper .sep { max-width: 20px; }

  .btn { padding: 12px 18px; font-size: 15px; min-height: 44px; }
  .btn-ghost { padding: 10px 10px; min-height: 44px; min-width: 44px; }
  .chip { padding: 8px 12px; font-size: 14px; min-height: 36px; }

  .input, .textarea { font-size: 18px; } /* prevents iOS zoom */

  .card { padding: 20px 18px; }
  .card.deep { padding: 24px 20px; }

  /* .menu-screen / .save-row phone layout: see the FRONT DOOR block (v1.9). */

  /* v1.7 P39 (CHROME-04): the badge collapses to its ONE affordance.
     "Logged in — key …TEST" is 288px of a 390px viewport (measured, D-35-05) and
     it is fixed, so it cannot be scrolled out from under the hero title. The key
     suffix is a desktop nicety; "Log out" is the thing that has to stay tappable
     on a phone, and it is the only route out of a session from this screen.
     (The badge's own padding / offsets stay with its later rule in this same
     bracket at styles.css:5090 — a second padding here would just be overridden
     by it.) */
  .login-badge > .mono { display: none; }

  .narration { font-size: 23px; line-height: 1.68; }
  .player-action { font-size: 22px; padding: 14px 14px 14px 38px; }

  /* Frontispiece: stack stats + two-col grid collapses */
  .card.deep > div[style*="grid-template-columns"] { grid-template-columns: 1fr !important; gap: 16px !important; }

  .modal { padding: 24px 20px; max-height: 90dvh; overflow-y: auto; }

  /* Pin the action composer just above the mobile tab bar so it's always
     reachable without scrolling past the whole narration. The page is the
     scroll surface (no inner scroll container), so sticky pins it to the
     viewport bottom; it degrades to normal flow if sticky can't engage. The
     tab bar is ~64px tall plus the home-indicator safe area. */
  .play-composer {
    position: sticky;
    bottom: calc(64px + env(safe-area-inset-bottom));
    z-index: 6;
    background: var(--paper);
    border-top: 1px solid var(--rule);
    margin-top: 8px;
    padding-left: 2px;
    padding-right: 2px;
  }

  /* --- Mobile side pane becomes bottom drawer --- */
  /* REVIEW WR-06 — the drawer stays OPAQUE (var(--paper), which is already
     palette-aware: #0d1118 under dark-glass, #010102 under arcane), and the
     inherited backdrop-filter is therefore NULLED here.
     The base .side-pane rule sets `backdrop-filter: var(--panel-backdrop)`,
     which under the two glass palettes composited a full-screen blur
     UNDERNEATH an opaque surface: pure GPU cost for zero visible effect. It
     also made .side-pane a containing block for any `position: fixed`
     descendant and opened a stacking context — on the one breakpoint where
     .side-pane is itself `position: fixed` and hosts the whole drawer.
     Opaque is the right call here rather than pointing the drawer at
     --panel-bg: at <768px .side-pane.drawer-open covers the entire viewport,
     and --panel-bg is rgba(255,255,255,.055) / rgba(150,110,255,.075) — far
     too thin to read stats over blurred prose. A genuinely frosted phone
     drawer needs a heavier tint token, which is Phase 35's surface work.
     For the same reason .play-composer, .mobile-tabbar and .drawer-close-bar
     below keep var(--paper): they overlay scrolling prose and carry NO
     backdrop-filter at all, so a translucent token there would let the feed
     read straight through them. */
  .side-pane {
    display: none;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    top: auto;
    height: 78dvh;
    width: 100%;
    border-left: none;
    border-top: 1px solid var(--ink);
    background: var(--paper);
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
    z-index: 200;
    padding: 14px 18px 80px;
    overflow-y: auto;
    box-shadow: 0 -12px 40px rgba(0,0,0,0.35);
    animation: drawerUp 240ms cubic-bezier(0.2, 0.6, 0.2, 1);
  }
  /* P16 (D-04): drawer-open promotes to a true full-screen modal at
     <768px. The slide-up animation is preserved (drawerUp keyframes still
     fire); once the slide completes, the drawer occupies the entire
     viewport. Tap-outside-to-close is removed since there is no outside
     region — closing affordances are the sticky close-bar (D-06), the
     swipe-down gesture (D-07, JSX-side), and the browser back button. */
  .side-pane.drawer-open {
    display: block;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    height: 100dvh;
    width: 100vw;
    max-width: none;
    border-top: none;
    border-radius: 0;
    padding: 14px 18px calc(14px + env(safe-area-inset-bottom));
  }

  /* P16 (D-12): drawer-handle visual stays 40x4 but the parent <button>
     gains a 44pt hit-target wrapper. The visual is rendered as a ::before
     pseudo so the button itself is a clean tap zone with adequate padding. */
  .side-pane .drawer-handle {
    display: block;
    width: 100%;
    height: auto;
    background: transparent;
    border: 0;
    padding: 12px 0;
    min-height: 44px;
    margin: 0 auto 4px;
    cursor: grab;
  }
  .side-pane .drawer-handle::before {
    content: '';
    display: block;
    width: 40px;
    height: 4px;
    background: var(--rule);
    border-radius: 4px;
    margin: 0 auto;
  }

  /* P16 (D-06): sticky close-bar at the bottom of the drawer's scroll
     container. Always visible while drawer is open; safe-area-inset
     padding keeps the button above the home-indicator on iOS. */
  .drawer-close-bar {
    position: sticky;
    bottom: 0;
    left: 0;
    right: 0;
    min-height: 44px;
    padding: 8px 18px calc(8px + env(safe-area-inset-bottom));
    background: var(--paper);
    border-top: 1px solid var(--rule);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 5;
    /* Stretch full bleed inside the side-pane padding so the rule reads as
       a true bottom edge, not a centered button island. */
    margin: 12px -18px calc(-14px - env(safe-area-inset-bottom));
  }
  .drawer-close-bar button {
    min-height: 44px;
    min-width: 88px;
    padding: 10px 24px;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-family: 'Cinzel', serif;
    font-size: 14px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    background: transparent;
    color: var(--ink);
    border: 1px solid var(--rule);
    border-radius: 4px;
    cursor: pointer;
    transition: background 150ms ease, border-color 150ms ease;
  }
  .drawer-close-bar button:hover {
    background: var(--paper-2);
    border-color: var(--ink);
  }

  /* P16 (D-12): touch-target audit — every interactive element inside the
     drawer is ≥ 44pt. Existing rules already meet this for some (e.g.,
     mobile-tabbar buttons via min-height:52px). The selectors below
     guarantee compliance for the in-drawer tab-row, section-header
     disclosure buttons, item cards, and the search input. */
  .side-pane .tab-row .tab,
  .side-pane .ledger-section-header,
  .side-pane .ledger-card,
  .side-pane .ledger-search input {
    min-height: 44px;
  }

  /* P16: long-name wrap default — fictional names like "Reliquary of the
     Forgotten Saint of Vhalmir" can occupy 2-3 lines instead of breaking
     layout. Single-line contexts (chip badge, close-bar text) are
     fixed-length and unaffected. */
  .side-pane .ledger-card .ledger-card-name,
  .side-pane .ledger-empty {
    word-wrap: break-word;
    overflow-wrap: anywhere;
  }

  /* Bottom tab bar — shown only on mobile */
  .mobile-tabbar {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 210;
    background: var(--paper);
    border-top: 1px solid var(--rule);
    padding: 6px 8px calc(6px + env(safe-area-inset-bottom));
    justify-content: space-around;
  }
  .mobile-tabbar button {
    flex: 1;
    background: transparent;
    border: none;
    padding: 8px 4px;
    color: var(--ink-faded);
    font-family: 'Cinzel', serif;
    font-size: 12px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    cursor: pointer;
    min-height: 52px;
  }
  .mobile-tabbar button.active { color: var(--ink); }
  .mobile-tabbar button.active::after {
    content: '';
    position: absolute;
    top: 0;
    width: 24px;
    height: 2px;
    background: var(--accent);
  }
  .mobile-tabbar button { position: relative; }

  /* Play feed gets bottom padding so tabbar doesn't cover input */
  .main-pane > .feed { padding-bottom: 16px; }

  /* Tweaks panel phone sheet: see the FRONT DOOR block (v1.9). */

  /* Wizard frontispiece 2-col → 1 col */
  .scene > div[style*="grid-template-columns"] { grid-template-columns: 1fr !important; }
}

@keyframes drawerUp {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}

/* Hide mobile tabbar on desktop.
   v1.7 P35 (D-35-01, deferred-items.md) — THIS RULE USED TO BE UNCONDITIONAL.
   `.mobile-tabbar { display: none }` sits ~60 lines AFTER the
   @media (max-width: 768px) block that sets `display: flex`, at IDENTICAL
   specificity (0,1,0), so the later declaration won at EVERY width — including
   inside the bracket it was meant to be excluded from. The tab bar therefore
   computed `display: none` on phones too, and it is the only thing that opens
   the ledger drawer there. Present since the initial import (ee36ccc); found
   while probing the drawer for the containing-block prohibition in plan 35-05
   and fixed here because SHELL-04's drawer criterion is unsatisfiable while it
   stands. The fix is a cascade fix, not a value change: the desktop intent is
   unchanged, it is simply now stated where it belongs.
   The 769px floor deliberately mirrors the drawer bracket AND
   play.jsx:64's matchMedia('(max-width: 768px)') gate, which is what decides
   whether the component renders at all. */
@media (min-width: 769px) {
  .mobile-tabbar { display: none; }
}

/* Very small screens */
@media (max-width: 420px) {
  .main-pane { padding: calc(14px + env(safe-area-inset-top)) 12px 20px; }
  .card { padding: 16px 14px; }
  .card.deep { padding: 18px 16px; }
  .narration { font-size: 19px; }
}

/* Floating cog that opens TweaksPanel in standalone mode */
.tweaks-fab {
  position: fixed;
  top: 14px;
  right: 14px;
  z-index: 50;
  width: 36px;
  height: 36px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  /* v1.7 P39 (CHROME-01): the cog gets a real surface, matching .login-badge
     beside it so the two read as one cluster. Until P39 this button carried
     .btn-ghost WITHOUT .btn, so it inherited no background at all and painted
     the user-agent `buttonface` — a solid light circle on every dark palette.
     Adding .btn in app.jsx fixed the wrong colour; declaring the surface here is
     what makes a 36px icon-only control findable against the ground.
     Both prefixes and the var(--x, none) fallback shape, same as .modal. */
  background: var(--bg-card);
  -webkit-backdrop-filter: blur(4px);
          backdrop-filter: blur(4px);
  border: 1px solid var(--border-soft);
  color: var(--ink-2);
}
.tweaks-fab:hover {
  background: var(--hover-wash);
  border-color: var(--border-strong);
  color: var(--ink);
}
@media (max-width: 768px) {
  .tweaks-fab { top: calc(10px + env(safe-area-inset-top)); right: 10px; width: 40px; height: 40px; }
}

/* Login badge shows "Logged in — key …abc1" on every authed screen */
.login-badge {
  position: fixed;
  top: 16px;
  right: 60px;
  z-index: 49;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px 4px 12px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--bg-card, rgba(0,0,0,0.04));
  backdrop-filter: blur(4px);
}
@media (max-width: 768px) {
  .login-badge { top: calc(12px + env(safe-area-inset-top)); right: 60px; padding: 2px 6px; }
  /* v1.7 P39 (CHROME-04) hides this span at this width (styles.css:4792), so the
     size override below is now dormant. It is kept, not deleted: if the key
     suffix is ever restored on phones it must come back at 13px, and re-deriving
     that is exactly the kind of detail that gets lost. */
  .login-badge .mono { font-size: 13px !important; }
}

/* Claude error banner.
   AAA SURFACES — a designed notice, not an alarm strip: a ringed glyph in the
   margin, one plain sentence in the reading serif, one line of detail, ONE
   primary action (gilt CTA), and a quiet dismiss in the corner. Colour: the
   danger hue only on the glyph ring and a 3px left edge; the words stay ink.
   The same .banner-* anatomy is shared by .storage-banner below. */
.banner.claude-banner {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 16px;
  padding: 18px 56px 18px 20px;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-2);
  font-style: normal;
  color: var(--ink);
  /* REVIEW WR-02 — this was `rgba(var(--accent-rgb, 180 96 52), 0.06)`, which
     substitutes to `rgba(217 180 90, 0.06)`: SPACE-separated components with a
     COMMA-separated alpha. CSS Color 4's grammar accepts `rgb(R G B / A)` or
     `rgb(R, G, B, A)` and never a mix, so the declaration was invalid at
     computed-value time and dropped — this banner has had no background in any
     palette, before P34 and after. The modern slash form is the fix. The
     fallback is gone on purpose: --accent-rgb is now declared in all four
     palettes and the contract test enforces that, so a fallback could only ever
     mask a real regression. Note THEME-02's no-undefined-var() test checks
     NAMES, not call-site syntax — it passed here throughout.
     AAA SURFACES: the tint now sits on a paper plate so the notice reads as a
     raised sheet, not as a hole in the page. */
  background:
    linear-gradient(90deg, color-mix(in srgb, var(--danger) 9%, transparent), transparent 55%),
    var(--paper-2);
  box-shadow: inset 3px 0 0 var(--danger), var(--elev-2);
  margin: 14px 0;
}
.banner.claude-banner .btn { white-space: nowrap; }
/* Shared banner anatomy (.claude-banner, .storage-banner). */
.banner .banner-glyph {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  margin-top: 1px;
  border-radius: 50%;
  color: var(--danger);
  border: 1px solid color-mix(in srgb, var(--danger) 55%, transparent);
  background: color-mix(in srgb, var(--danger) 10%, transparent);
}
.banner .banner-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.banner .banner-title {
  font-family: 'EB Garamond', serif;
  font-style: normal;
  font-size: 20px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
  text-wrap: balance;
}
.banner .banner-detail {
  font-family: 'EB Garamond', serif;
  font-style: normal;
  font-size: 17px;
  line-height: 1.45;
  color: var(--ink-2);
}
.banner .banner-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
}
.banner .banner-actions .btn { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; }
.banner .banner-dismiss {
  position: absolute;
  top: 10px;
  right: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  min-width: 44px;
  padding: 0;
  border-radius: 50%;
  color: var(--ink-faded);
}
.banner .banner-dismiss:hover { color: var(--ink); }
/* Phone: the sentence and its action use the full width of the sheet — only
   the title keeps clear of the corner ×, and the action row runs under the
   glyph, so "Download a copy" never breaks onto two lines. */
@media (max-width: 768px) {
  .banner.claude-banner,
  .banner.storage-banner { padding: 16px 16px 16px 16px; gap: 14px; }
  .banner .banner-glyph { width: 36px; height: 36px; }
  .banner .banner-title { padding-right: 34px; font-size: 19px; }
  .banner .banner-detail { font-size: 17px; }
  .banner .banner-actions { margin-left: -50px; }
  .banner .banner-actions .btn { flex: 1 1 auto; justify-content: center; }
  .banner .banner-dismiss { top: 6px; right: 6px; }
}

/* Auth screen — v1.9: .auth-wrap is styled in the FRONT DOOR block. */

/* A11Y-03: visible focus ring via :focus-visible for keyboard navigation.
   Mouse clicks don't get the ring (browsers only apply :focus-visible on
   keyboard or programmatic focus). */
.btn:focus-visible,
.btn-ghost:focus-visible,
.chip:focus-visible,
.choice:focus-visible,
.tab:focus-visible,
.save-row:focus-visible,
/* v1.7 P39 (CHROME-04): .world-card left the .btn family, so it must be named
   here explicitly or the world picker loses its keyboard focus ring. */
.world-card:focus-visible,
.tweaks-fab:focus-visible,
.login-badge button:focus-visible,
.drawer-handle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* AAA SURFACES — inside the ledger contents, the settings and level-up sheets
   and the notices, the keyboard ring is the same GOLD as every selected state
   there (the base rule above stays rose for the screens other workstreams own).
   Also names the ledger controls the base list never covered, which had no
   visible focus at all. */
.side-pane .ledger-card:focus-visible,
.side-pane .ledger-section-header:focus-visible,
.side-pane .ledger-search input:focus-visible,
.skills-block .skill-group-header:focus-visible,
.pending-levelup-btn:focus-visible,
.chronicle-btn.btn:focus-visible,
.levelup-step .feat-card:focus-visible,
.levelup-step .stat-picker-row:focus-visible,
.levelup-modal .btn:focus-visible,
.settings-modal .btn:focus-visible,
.settings-modal .chip:focus-visible,
.settings-modal .setting-select:focus-visible,
.setting-toggle-input:focus-visible,
.settings-advanced-summary:focus-visible,
.levelup-steps-btn:focus-visible,
.levelup-chip:focus-visible,
.modal .modal-close.btn:focus-visible,
.banner .btn:focus-visible,
.soft-delete-toast .btn:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}
.side-pane .ledger-card:focus-visible { outline-offset: -2px; }

/* Phase 5: choice / tab are real <button>s (A11Y-02). They need user-agent
   defaults reset so they blend in, but tabs specifically need to keep their .tab
   font styling — which is why we re-declare it below.
   `.save-row` stays in the selector list for safety only: menu.jsx renders it as
   a role="button" DIV (nested <button>s for Export/Delete/Resume are invalid
   HTML), so nothing matches today. Keep it — the class is the contract, not the
   tag it happens to use.

   ⚠ v1.7 P39 (CHROME-02) — THIS BLOCK IS WHY THE WIZARD LOOKED UNSTYLED.
   At (0,0,1,1) it beats every `.choice` rule at (0,0,1,0), so `background:
   inherit`, `border: 0` and `padding: 0` silently erased .choice's own surface,
   border and 18px padding. The genre grid, archetype grid, trait grid and every
   other wizard picker rendered as a bare title/description list with no card, no
   hover edge, and NO VISIBLE SELECTED STATE — on all four palettes, since long
   before this phase. Only `.tab` had noticed (see its re-declaration below).
   THE RESET IS DELIBERATELY LEFT INTACT. Trimming `background`/`border`/`padding`
   out of it was tried first and it broke the LEDGER TABS: `.tab` declares no
   background and only a `border-bottom`, so without the reset those buttons fell
   back to the user-agent `buttonface` and a full 2px outset border — five light
   boxed slabs across the side pane (MEASURED at 1440x900). The reset is load-
   bearing for two of its three subjects. The fix is therefore ADDITIVE: .choice
   re-declares its own box below, at the same specificity, later in source. */
button.save-row,
button.choice,
button.tab {
  background: inherit;
  color: inherit;
  text-align: inherit;
  cursor: pointer;
  border: 0;
  appearance: none;
  -webkit-appearance: none;
}

button.save-row,
button.choice {
  font: inherit;
  padding: 0;
  margin: 0;
}

/* .choice's own box, re-declared at (0,0,1,1) so it ties the reset above and wins
   on cascade order — which is the whole mechanism, so KEEP IT BELOW the reset.
   Values mirror the base .choice rule at styles.css:3958; if that rule changes,
   this one moves with it, and so does the phone padding override in the 768px
   bracket (which needs the same specificity bump). Written as a separate rule
   rather than folded into the reset so the reset stays a pure normaliser for the
   two subjects that still want one. */
button.choice {
  padding: 18px;
  border: 1px solid var(--border-soft);
  background: var(--panel-bg);
}

button.choice.selected {
  border-color: var(--cta-edge);
  background: var(--paper-deep);
}

/* button.tab must re-declare .tab's font/padding because the reset above
   sits at (0,0,1,1) specificity which ties .tab and wins by cascade order.
   Without this, the inherited font (EB Garamond ~19px) replaces Cinzel 11px
   and the four tabs overflow the side pane. */
button.tab {
  font-family: 'Cinzel', serif;
  font-size: 14px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 10px 12px;
}

/* DATA-09: soft-delete Undo toast.
   AAA SURFACES — was a solid var(--ink) slab: correct as ink on parchment, a
   glaring near-white block on the three dark palettes. Now a raised paper
   sheet with a gilt hairline, the name in ink, the countdown quiet, and Undo
   as a gold text action. */
/* Centred with left/right + auto margins, NOT left:50% + translateX(-50%): the
   fadeIn entrance animation fills `both` and its `transform` would replace the
   centring transform, parking the toast half off the right edge. */
.soft-delete-toast {
  position: fixed;
  bottom: calc(24px + env(safe-area-inset-bottom));
  left: 16px;
  right: 16px;
  margin: 0 auto;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 18px;
  width: fit-content;
  max-width: 560px;
  padding: 10px 10px 10px 20px;
  background: var(--paper-deep);
  color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--gold) 34%, transparent);
  border-radius: 10px;
  box-shadow: var(--shadow-lg), 0 0 0 1px rgba(0, 0, 0, 0.2);
  font-size: 17px;
  animation: fadeIn 260ms ease both;
}
.soft-delete-toast .toast-text { line-height: 1.35; }
body.has-toast .menu-screen { padding-bottom: 128px; }
/* (integration) SURFACES' `.save-row.is-burning { opacity:.4; pointer-events:none }`
   is gone: on the menu the burning row IS the undo notice (FRONT DOOR's
   BurningRow, .fd-burning), and that rule dimmed it twice over and made its
   Undo button unclickable. */
.soft-delete-toast strong { color: var(--ink); font-weight: 600; }
.soft-delete-toast .toast-meta { color: var(--ink-faded); font-style: italic; }
.soft-delete-toast .toast-action.btn {
  min-height: 44px;
  padding: 8px 16px;
  color: color-mix(in srgb, var(--gold) 62%, var(--ink-2));
  border: 1px solid color-mix(in srgb, var(--gold) 45%, transparent);
  border-radius: 6px;
  font-size: 15px;
}
.soft-delete-toast .toast-action.btn:hover {
  color: color-mix(in srgb, var(--gold) 62%, var(--ink-2));
  background: color-mix(in srgb, var(--gold) 10%, transparent);
  border-color: var(--gold);
}

/* ============================================================================
   P18 (D-08, D-14..D-18): LevelUpModal + SidePane SkillsList + derived-level
   pip + pending-level-up button + rested-bonus stub.
   ============================================================================ */

/* P18 (D-15): derived-level-pip in SelfTab. Source-of-truth for character
   level (replaces v1.2 placeholder XP bar; the bar persists as fallback for
   unmigrated saves). AAA SURFACES: drawn as a gilt seal beside the name —
   a ringed numeral (the level is the player's, so it is gold) with a quiet
   caption. */
.derived-level-pip {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
}
.derived-level-pip .level-value {
  display: grid;
  place-items: center;
  width: 58px;
  height: 58px;
  border-radius: var(--radius-round);
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: lining-nums;
  color: var(--gold);
  border: 1.5px solid var(--gold);
  background: radial-gradient(circle at 50% 35%, color-mix(in srgb, var(--gold) 14%, transparent), transparent 70%);
  box-shadow:
    inset 0 0 0 4px color-mix(in srgb, var(--paper) 55%, transparent),
    inset 0 0 0 5px color-mix(in srgb, var(--gold) 28%, transparent),
    0 0 18px -4px color-mix(in srgb, var(--gold) 45%, transparent);
}
.derived-level-pip .label-text {
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-weight: 400;
  letter-spacing: 0.02em;
  color: var(--ink-faded);
  text-transform: none;
}

/* P18 (D-14): SidePane skills-block + collapsible-by-attribute accordion.
   AAA SURFACES: each attribute is a ruled row (name in serif small caps, a
   tabular count, a ⌄ that turns over when open); its skills hang under it as
   name · xp · level, with the level the one figure in full ink. Attributes with
   no skills are not rendered at all. */
.skills-block .skill-group {
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 14%, transparent);
}
.skills-block .skill-group-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 48px;
  padding: var(--space-2) var(--space-1) var(--space-2) 0;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-serif);
  font-size: 18px;
  font-weight: 500;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.08em;
  text-transform: none;
  color: var(--ink-2);
  text-align: left;
  transition: color var(--dur-fast) var(--ease-out);
}
.skills-block .skill-group-header:hover { color: var(--ink); }
.skills-block .skill-group-meta { display: inline-flex; align-items: center; gap: var(--space-3); }
.skills-block .skill-group-count {
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-style: normal;
  font-weight: 400;
  font-variant-caps: normal;
  letter-spacing: 0;
  color: var(--ink-faded);
  font-variant-numeric: lining-nums tabular-nums;
}
.skills-block .skill-group-header .chevron {
  color: var(--ink-faded);
  transition: transform var(--dur-base) var(--ease-out);
}
.skills-block .skill-group-header[aria-expanded="true"] { color: var(--ink); }
.skills-block .skill-group-header[aria-expanded="true"] .chevron {
  transform: rotate(180deg);
}
.skills-block .skill-rows {
  padding: 0 0 var(--space-3);
}
.skills-block .skill-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  column-gap: var(--space-3);
  padding: var(--space-1) 0 var(--space-1) var(--space-4);
  font-size: var(--text-md);
  color: var(--ink);
  border-left: 1px solid color-mix(in srgb, var(--gold) 22%, transparent);
}
.skills-block .skill-name { font-family: var(--font-serif); font-size: var(--text-md); line-height: 1.4; }
.skills-block .skill-row .skill-level {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-3);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  color: var(--ink-faded);
}
.skills-block .skill-xp {
  font-style: normal;
  font-size: var(--text-sm);
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink-faded);
}
.skills-block .skill-lv {
  min-width: 48px;
  text-align: right;
  font-size: var(--text-md);
  font-weight: 600;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink);
}
.skills-block .skill-lv-cap {
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--ink-faded);
}

/* P18 (D-16): pending-level-up affordance in SelfTab. Always visible when
   pendingLevelUps queue is non-empty; primary CTA styling. AAA SURFACES: the
   one lit object on the sheet — a gilt plate with a breathing star, a title and
   a line of plain direction. */
.pending-levelup-btn {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  min-height: 64px;
  margin-top: var(--space-4);
  padding: var(--space-3) var(--space-4);
  font-family: var(--font-serif);
  text-align: left;
  color: var(--ink);
  background: linear-gradient(90deg, color-mix(in srgb, var(--gold) 16%, transparent), color-mix(in srgb, var(--gold) 4%, transparent));
  border: 1px solid var(--gold);
  border-radius: var(--radius-2);
  box-shadow: 0 0 24px -8px color-mix(in srgb, var(--gold) 60%, transparent);
  cursor: pointer;
  transition: box-shadow var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
}
.pending-levelup-btn:hover {
  background: linear-gradient(90deg, color-mix(in srgb, var(--gold) 24%, transparent), color-mix(in srgb, var(--gold) 8%, transparent));
  box-shadow: var(--glow-gilt), 0 0 30px -6px color-mix(in srgb, var(--gold) 60%, transparent);
}
.pending-levelup-star {
  display: inline-flex;
  color: var(--gold);
  animation: sfStarBreathe 2.8s ease-in-out infinite;
}
.pending-levelup-star svg path { fill: currentColor; }
.pending-levelup-text { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pending-levelup-title {
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--ink);
}
.pending-levelup-sub { font-size: var(--text-sm); color: var(--ink-2); }
.pending-levelup-chev { flex: 0 0 auto; color: var(--gold); }
@keyframes sfStarBreathe {
  0%, 100% { opacity: 0.75; transform: scale(0.94); }
  50%      { opacity: 1;    transform: scale(1.06); }
}

/* P18 (D-17): rested-bonus stub. Truthy-only render — full state machine
   (REST-01..03) ships in P21. AAA SURFACES: a quiet annotation with a gilt
   tick in the margin, not a tinted strip. */
.rested-bonus-stub {
  margin-top: var(--space-5);
  padding: var(--space-1) 0 var(--space-1) var(--space-4);
  font-size: var(--text-md);
  line-height: 1.45;
  color: var(--ink-2);
  background: none;
  border-left: 2px solid color-mix(in srgb, var(--gold) 60%, transparent);
}

/* P18 (D-18): empty skills state (new char before chargen). */
.empty-skills {
  font-style: italic;
  font-size: var(--text-md);
  color: var(--ink-faded);
  margin-top: var(--space-1);
}

/* ============================================================================
   P18 (D-08): LevelUpModal. AAA SURFACES — a ceremony, then a choice.
   The sheet is a flex column: a scroller (hero · steps · gained · choices) that
   fades out at its bottom edge, then the decision bar as the LAST FLEX CHILD,
   so no content ever paints under it. Choices are numbered steps
   (I Feat · II Attribute) announced in the hero and repeated as status marks
   in the bar. Selected = a gold radio + a gold edge and a 6% gold fill.
   Confirm is a gold outline at .4 until every step is done, then solid gold.
   ============================================================================ */
.modal.levelup-modal {
  max-width: 720px;
  max-height: min(88vh, 920px);
  max-height: min(88dvh, 920px);
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background:
    radial-gradient(ellipse 60% 220px at 50% 0%, color-mix(in srgb, var(--gold) 11%, transparent), transparent 100%),
    var(--sheet-fill);
}
/* The title is sr-only (the seal is the heading), so the head is just the close
   button in the sheet's top-right corner. */
.levelup-modal .modal-head {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  margin: 0;
  z-index: 3;
}
.levelup-step {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.levelup-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-6) var(--space-7) var(--space-7);
  -webkit-mask-image: linear-gradient(to bottom, black calc(100% - var(--space-6)), transparent);
          mask-image: linear-gradient(to bottom, black calc(100% - var(--space-6)), transparent);
}

.levelup-hero { text-align: center; padding: 0 0 var(--space-5); }
.levelup-eyebrow {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 500;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.16em;
  padding-left: 0.16em; /* optically re-centre the tracked small caps */
  color: var(--ink-2);
}
.levelup-seal-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  max-width: 420px;
  margin: var(--space-3) auto var(--space-3);
}
.levelup-rule {
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--gold) 60%, transparent));
}
.levelup-rule:last-child { transform: scaleX(-1); }
.levelup-seal {
  display: grid;
  place-items: center;
  width: 92px;
  height: 92px;
  border-radius: var(--radius-round);
  border: 1.5px solid var(--gold);
  background: radial-gradient(circle at 50% 38%, color-mix(in srgb, var(--gold) 20%, transparent), transparent 72%);
  box-shadow:
    inset 0 0 0 5px color-mix(in srgb, var(--paper) 55%, transparent),
    inset 0 0 0 6px color-mix(in srgb, var(--gold) 40%, transparent),
    0 0 36px -6px color-mix(in srgb, var(--gold) 55%, transparent);
  animation: sfSealIn 700ms var(--ease-out) both;
}
.levelup-seal-num {
  font-family: var(--font-display);
  font-size: 44px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: lining-nums;
  color: var(--gold);
  text-shadow: 0 0 18px color-mix(in srgb, var(--gold) 50%, transparent);
  transform: translateY(1px);
}
@keyframes sfSealIn {
  from { opacity: 0; transform: scale(0.86); }
  to   { opacity: 1; transform: scale(1); }
}
.levelup-step .level-header {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 22px;
  font-weight: 400;
  letter-spacing: 0;
  line-height: 1.3;
  color: var(--ink);
  margin: 0;
}
.levelup-queue {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  color: var(--ink-faded);
}
/* The steps this level asks for — announced up front, each a jump to its section. */
.levelup-steps {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}
.levelup-steps-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: 0 var(--space-4) 0 var(--space-2);
  font-family: var(--font-serif);
  font-size: var(--text-md);
  color: var(--ink-2);
  background: var(--tint-sunken);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-round);
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.levelup-steps-btn:hover { border-color: color-mix(in srgb, var(--gold) 50%, transparent); color: var(--ink); }
.levelup-steps-item.is-done .levelup-steps-btn { color: var(--ink); border-color: color-mix(in srgb, var(--gold) 55%, transparent); }
/* The step mark: a numeral in a gilt ring; a gold disc with a check once done. */
.levelup-step-mark {
  display: inline-grid;
  place-items: center;
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-round);
  border: 1.5px solid color-mix(in srgb, var(--gold) 60%, transparent);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-weight: 600;
  font-variant-numeric: lining-nums;
  line-height: 1;
  color: var(--ink);
}
.levelup-steps-item.is-done .levelup-step-mark,
.levelup-chip.is-done .levelup-step-mark,
.levelup-step-mark.is-done {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--bg);
}

/* Section headings share the sheets' label (small caps + hairline) and carry
   the step's numeral. */
.levelup-step .levelup-section-title { margin: var(--space-6) 0 var(--space-3); }
.levelup-step .auto-rewards .levelup-section-title { margin-top: var(--space-2); }
.levelup-section-text { flex: 0 0 auto; }
.levelup-section-note {
  flex: 0 0 auto;
  font-style: italic;
  font-size: var(--text-sm);
  font-variant-caps: normal;
  letter-spacing: 0;
  color: var(--ink-faded);
}
.levelup-empty { font-size: var(--text-md); font-style: italic; color: var(--ink-faded); }

/* Gained: three tokens on one ruled line. */
.levelup-step .auto-rewards { margin: 0; }
.levelup-step .reward-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid color-mix(in srgb, var(--gold) 16%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 16%, transparent);
}
.levelup-step .reward-line {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--space-3) var(--space-1);
  color: var(--ink-2);
}
.levelup-step .reward-grid .reward-line + .reward-line {
  border-left: 1px solid color-mix(in srgb, var(--gold) 16%, transparent);
}
.levelup-step .reward-amt {
  font-family: var(--font-serif);
  font-size: 28px;
  font-weight: 600;
  line-height: 1.05;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink);
}
.levelup-step .reward-name {
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--ink-2);
}
.levelup-step .reward-was {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-1);
  font-size: var(--text-sm);
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink-faded);
}
.levelup-step .reward-arrow { color: var(--ink-faded); }
.levelup-step .reward-now { color: var(--ink); font-weight: 600; }
.levelup-step .reward-insp {
  flex-direction: row;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) 0 0;
  font-size: var(--text-md);
}
.levelup-step .reward-star { display: inline-flex; color: var(--gold); }

/* Choices: feats — one column of cards, a gold radio on the left. */
.levelup-step .feat-group + .feat-group { margin-top: var(--space-4); }
.levelup-step .feat-group-name {
  margin: 0 0 var(--space-2);
  font-family: var(--font-serif);
  font-style: italic;
  font-size: var(--text-sm);
  color: var(--ink-faded);
}
.levelup-step .feat-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2);
}
.levelup-step .feat-card {
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr);
  column-gap: var(--space-4);
  align-items: start;
  width: 100%;
  min-height: 64px;
  margin: 0;
  padding: var(--space-4) var(--space-4);
  background: var(--tint-sunken);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-2);
  cursor: pointer;
  text-align: left;
  font-family: var(--font-serif);
  color: var(--ink);
  transition: border-color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.levelup-step .feat-card:hover:not([disabled]) {
  border-color: color-mix(in srgb, var(--gold) 45%, transparent);
  background: color-mix(in srgb, var(--gold) 4%, var(--tint-sunken));
}
.levelup-step .feat-card[aria-pressed="true"] {
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 6%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--gold) 45%, transparent), 0 0 20px -8px color-mix(in srgb, var(--gold) 55%, transparent);
}
.levelup-step .feat-card[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}
.levelup-step .feat-check {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  margin-top: 1px;
  border-radius: var(--radius-round);
  border: 1.5px solid color-mix(in srgb, var(--gold) 60%, transparent);
  color: transparent;
  transition: background var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.levelup-step .feat-card[aria-pressed="true"] .feat-check {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--bg);
}
.levelup-step .feat-body { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.levelup-step .feat-name {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1.25;
  text-transform: none;
  color: var(--ink);
}
.levelup-step .feat-learned { font-size: var(--text-sm); font-style: italic; color: var(--ink-faded); }
.levelup-step .feat-desc {
  font-size: var(--text-md);
  line-height: 1.45;
  color: var(--ink-2);
  text-wrap: pretty;
}
.levelup-step .feat-prereq {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-sm);
  color: var(--ink-faded);
}
.levelup-step .feat-prereq.met { color: var(--verdict-success); }
.levelup-step .feat-prereq-mark { display: inline-flex; }

/* Choices: attribute tiles. */
.levelup-step .stat-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2);
}
.levelup-step .stat-picker-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  width: 100%;
  min-height: 72px;
  margin: 0;
  padding: var(--space-3) var(--space-2);
  background: var(--tint-sunken);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-2);
  cursor: pointer;
  font-family: var(--font-serif);
  color: var(--ink);
  transition: border-color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.levelup-step .stat-picker-row:hover {
  border-color: color-mix(in srgb, var(--gold) 45%, transparent);
  background: color-mix(in srgb, var(--gold) 4%, var(--tint-sunken));
}
.levelup-step .stat-picker-row[aria-checked="true"] {
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 6%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--gold) 45%, transparent), 0 0 20px -8px color-mix(in srgb, var(--gold) 55%, transparent);
}
.levelup-step .stat-pick-name {
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--ink-2);
}
.levelup-step .stat-picker-row[aria-checked="true"] .stat-pick-name { color: var(--ink); }
.levelup-step .stat-pick-vals {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: 20px;
  font-variant-numeric: lining-nums tabular-nums;
}
.levelup-step .stat-pick-cur { color: var(--ink-faded); }
.levelup-step .stat-pick-arrow { color: var(--ink-faded); font-size: var(--text-sm); }
.levelup-step .stat-pick-next { color: var(--ink); font-weight: 600; }

/* Choices: title — the design system's input. */
.levelup-step .levelup-title-input {
  width: 100%;
  min-height: 48px;
  padding: var(--space-3) var(--space-4);
  font-family: var(--font-serif);
  font-size: 19px;
  color: var(--ink);
  background: var(--tint-sunken);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-1);
  outline: none;
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.levelup-step .levelup-title-input:focus {
  border-color: var(--gold);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 18%, transparent);
}

/* The decision bar — the sheet's last flex child. One row: step marks, a quiet
   "Decide later", Confirm. */
.levelup-step .levelup-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 72px;
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid color-mix(in srgb, var(--gold) 24%, transparent);
  background: var(--sheet-fill);
}
.levelup-step .levelup-status { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--space-1); }
.levelup-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: 0 var(--space-2);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  color: var(--ink-2);
  background: none;
  border: 0;
  border-radius: var(--radius-1);
  cursor: pointer;
}
.levelup-chip:hover { background: var(--hover-wash); color: var(--ink); }
.levelup-chip.is-done { color: var(--ink); }
.levelup-step .levelup-buttons { display: flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; }
.levelup-step .levelup-later.btn {
  padding: 0 var(--space-3);
  color: var(--ink-2);
  background: none;
  border-color: transparent;
}
.levelup-step .levelup-later.btn:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 4px; background: none; }
/* Confirm: gold outline, quiet (.4) until ready; then solid gold with a paper
   label. The fill is the metal nudged toward the ink so the paper label clears
   AA on the light palette too (parchment 4.8:1, dark-glass 11.9:1). */
.levelup-step .levelup-confirm.btn {
  min-width: 190px;
  font-weight: 600;
  color: var(--gold);
  background: transparent;
  border: 1px solid var(--gold);
}
.levelup-step .levelup-confirm.btn:disabled {
  opacity: 0.4;
  box-shadow: none;
  cursor: not-allowed;
}
.levelup-step .levelup-actions.is-ready .levelup-confirm.btn {
  color: var(--paper);
  background: color-mix(in srgb, var(--gold) 75%, var(--ink));
  border-color: color-mix(in srgb, var(--gold) 75%, var(--ink));
  box-shadow: var(--glow-gilt), var(--elev-2);
}
.levelup-step .levelup-actions.is-ready .levelup-confirm.btn:hover {
  background: color-mix(in srgb, var(--gold) 85%, var(--ink));
}

/* Mobile touch-target rules (matches LEDGER-08 carry-forward pattern) */
@media (max-width: 768px) {
  .pending-levelup-btn { min-height: 64px; }
  .levelup-step .feat-card,
  .levelup-step .stat-picker-row { min-height: 64px; }
  .levelup-scroll { padding: var(--space-6) var(--space-5) var(--space-7); }
}
/* Phone: the level-up is a FULL-HEIGHT sheet — the ceremony gets the whole
   screen, and the choices get the height the r1 footer used to eat. */
@media (max-width: 480px) {
  .modal-bg.levelup-backdrop { padding: 0; }
  .modal-bg.levelup-backdrop > .modal.levelup-modal {
    height: 100%;
    max-height: 100%;
    border-radius: 0;
    border: 0;
  }
  .levelup-modal .modal-head { top: calc(var(--space-2) + env(safe-area-inset-top)); right: var(--space-2); }
  .levelup-scroll { padding: calc(var(--space-5) + env(safe-area-inset-top)) var(--space-4) var(--space-6); }
  .levelup-seal-row { margin: var(--space-2) auto; }
  .levelup-seal { width: 68px; height: 68px; }
  .levelup-seal-num { font-size: 34px; }
  .levelup-step .level-header { font-size: 20px; }
  .levelup-steps { margin-top: var(--space-3); }
  .levelup-step .levelup-section-title { margin-top: var(--space-5); }
  .levelup-step .reward-line { padding: var(--space-2) var(--space-1); }
  .levelup-step .reward-amt { font-size: 22px; }
  .levelup-step .feat-card { padding: var(--space-3); column-gap: var(--space-3); }
  .levelup-step .levelup-actions {
    gap: var(--space-2);
    min-height: 64px;
    padding: var(--space-2) var(--space-3) calc(var(--space-2) + env(safe-area-inset-bottom));
  }
  .levelup-chip { padding: 0; min-width: var(--tap); justify-content: center; }
  .levelup-chip-label { display: none; }
  .levelup-step .levelup-confirm.btn { min-width: 0; padding: 0 var(--space-4); }
  .levelup-step .levelup-later.btn { padding: 0 var(--space-2); }
}

/* Reduced-motion */
@media (prefers-reduced-motion: reduce) {
  .skills-block .skill-group-header .chevron { transition: none; }
  .pending-levelup-star { animation: none; opacity: 1; }
  .levelup-seal { animation: none; }
}

/* DATA-04: storage failure banner pinned under the cog */
/* AAA SURFACES — same anatomy as the Claude banner (glyph · sentence · detail
   · one action · corner dismiss), floated as a raised sheet. The old rule laid
   text, button and × out in ONE flex row, which at phone width squeezed the
   sentence into a 150px column seven lines tall. The action now sits under the
   text, so the sheet is short and wide at every width. */
.banner.storage-banner {
  position: fixed;
  top: 76px;
  right: 16px;
  left: auto;
  z-index: 250;
  width: min(460px, calc(100vw - 32px));
  max-width: none;
  display: flex;
  align-items: flex-start;
  gap: 16px;
  margin: 0;
  padding: 18px 56px 18px 20px;
  font-style: normal;
  color: var(--ink);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-2);
  background:
    linear-gradient(90deg, color-mix(in srgb, var(--danger) 9%, transparent), transparent 55%),
    var(--paper-2);
  box-shadow: inset 3px 0 0 var(--danger), var(--elev-3);
  animation: fadeIn 260ms ease both;
}
/* In flow (play screen): under the ledger's tab row, or in the feed on a phone.
   It pushes content down; it never floats over navigation. */
.banner.storage-banner.is-inline {
  position: relative;
  top: auto;
  right: auto;
  left: auto;
  z-index: auto;
  width: auto;
  margin: 0 0 var(--space-5);
  box-shadow: inset 3px 0 0 var(--danger), var(--elev-1);
}
.feed .banner.storage-banner.is-inline { margin: var(--space-4) 0; }
@media (max-width: 768px) {
  .banner.storage-banner {
    top: calc(64px + env(safe-area-inset-top));
    right: 12px;
    left: 12px;
    width: auto;
    padding: 16px;
    gap: 14px;
  }
}

/* ============================================================
   P19 — World authoring overhaul (atomic re-roll, click-to-edit,
   custom-tag chip input, dependent-field warning).
   See .planning/phases/019-world-authoring-overhaul/019-UI-SPEC.md.
   ============================================================ */

/* Per-field re-roll button (RegenButton) — icon-only.
   Desktop: opacity 0.55 baseline, lifts to 1.0 on hover/focus.
   Mobile: ≥44pt touch target. */
.btn-icon.regen-btn {
  background: transparent;
  border: 1px solid transparent;
  color: var(--ink-2);
  padding: 6px;
  cursor: pointer;
  opacity: 0.55;
  transition: opacity 150ms, color 150ms, border-color 150ms;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  margin-left: 6px;
  border-radius: 2px;
}
.btn-icon.regen-btn:hover:not(:disabled),
.btn-icon.regen-btn:focus-visible {
  opacity: 1;
  color: var(--accent);
  border-color: var(--rule);
  outline: none;
}
.btn-icon.regen-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.btn-icon.regen-btn[aria-busy="true"] {
  animation: regen-spin 1.2s linear infinite;
  opacity: 1;
  color: var(--accent);
}
@keyframes regen-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

/* Click-to-edit display surface — subtle hover hint that this is editable. */
.field-editor-display {
  cursor: text;
  border: 1px dashed transparent;
  border-radius: 2px;
  padding: 2px 4px;
  margin: -2px -4px;
  transition: border-color 150ms;
  min-height: 1em;
  display: inline-block;
  width: 100%;
  box-sizing: border-box;
}
.field-editor-display:hover,
.field-editor-display:focus-visible {
  border-color: var(--rule);
  outline: none;
}

/* Click-to-edit input/textarea — styled to match the surrounding text. */
.field-editor-input,
.field-editor-textarea {
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--accent);
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;
  line-height: inherit;
  padding: 2px 4px;
  margin: -3px -5px;
  width: 100%;
  box-sizing: border-box;
  border-radius: 2px;
}
.field-editor-textarea {
  resize: vertical;
  min-height: 3em;
}

/* Dependent-field warning banner. */
.warning-banner {
  background: var(--paper-2);
  border-left: 3px solid var(--accent);
  padding: 12px 16px;
  margin: 16px 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  border-radius: 2px;
}
.warning-banner-text {
  color: var(--ink);
  font-size: 17px;
  line-height: 1.4;
}
.warning-banner-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

/* Small button variant used in the warning banner action row. */
.btn-small {
  font-size: 14px;
  padding: 4px 10px;
}

/* Custom-tag chip-input UI (Forge step). */
.custom-tag-input {
  margin-top: 16px;
  padding: 12px 14px;
  border: 1px solid var(--rule);
  border-radius: 2px;
}
.custom-tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding-left: 4px;
}
.custom-tag-text {
  text-transform: none;
  letter-spacing: normal;
  font-family: inherit;
  font-size: 15px;
}
.custom-tag-remove {
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 2px;
  color: inherit;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 2px;
}
.custom-tag-remove:hover,
.custom-tag-remove:focus-visible {
  color: var(--accent);
  outline: none;
}
.custom-tag-text-input {
  width: 100%;
  font-family: inherit;
  font-size: 17px;
  padding: 8px 10px;
  border: 1px solid var(--rule);
  background: var(--paper);
  color: var(--ink);
  box-sizing: border-box;
  border-radius: 2px;
  transition: border-color 150ms, box-shadow 150ms;
}
.custom-tag-text-input:focus-visible {
  outline: none;
  border-color: var(--accent);
}
.custom-tag-text-input.invalid-flash {
  animation: invalid-flash 600ms ease-out;
}
@keyframes invalid-flash {
  0%, 100% { border-color: var(--rule); box-shadow: none; }
  50%      { border-color: #c0392b; box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.15); }
}

/* P20 — chargen-trait-picker accordion. */
.trait-cat-section { margin-bottom: 6px; }
.trait-cat-header {
  appearance: none;
  background: transparent;
  border: 1px solid var(--rule);
  color: var(--ink);
  font: inherit;
  font-size: 17px;
  padding: 10px 14px;
  width: 100%;
  text-align: left;
  cursor: pointer;
  border-radius: 4px;
  transition: background 150ms;
}
.trait-cat-header:hover,
.trait-cat-header:focus-visible {
  background: var(--paper-2);
  outline: none;
}
.trait-chip-grid { padding: 4px 0; }

/* Mobile pass — ≥44pt touch targets at <768px. */
@media (max-width: 768px) {
  .btn-icon.regen-btn { padding: 12px; }
  .custom-tag-remove { padding: 14px; }
  .custom-tag-remove svg { width: 16px; height: 16px; }
  .field-editor-input,
  .field-editor-textarea,
  .custom-tag-text-input {
    font-size: 18px;  /* iOS no-zoom rule per CONVENTIONS.md */
  }
  .trait-cat-header { padding: 14px 16px; font-size: 18px; }
}

/* ============================================================================
   P21 — overflow display + session-pool banner (UI-SPEC)
   ========================================================================== */

/* Overflow numbers — used in skill-row "L100 → 105/100" format and chip
   xp-preview overflow extension "Lore 105/100 +25". */
.overflow-num {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.overflow-num.overflow-base  { color: var(--overflow-base); }
.overflow-num.overflow-gear  { color: var(--overflow-gear); }
.overflow-num.overflow-feat  { color: var(--overflow-feat); }
.overflow-num.overflow-state { color: var(--overflow-state); }
.skill-row .overflow-arrow {
  color: var(--ink-faded);
  margin: 0 4px;
}

/* xp-preview badge overflow variant (chip render in play.jsx). */
.xp-preview-badge.overflow-active .overflow-num {
  margin-right: 4px;
}

/* Session-pool collapsible banner — shown when c.character.sessionPool
   has any entries. */
.session-pool-banner {
  margin-top: 12px;
  padding: 10px 12px;
  background: var(--paper-2);
  border-left: 3px solid var(--overflow-state);
  border-radius: 3px;
  font-size: 17px;
}
.session-pool-banner .header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  background: transparent;
  border: none;
  padding: 0;
  cursor: pointer;
  text-align: left;
  color: var(--ink);
  font: inherit;
}
.session-pool-banner .header:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.session-pool-banner .total-xp {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--overflow-state);
}
.session-pool-banner .breakdown {
  margin: 8px 0 0;
  padding: 8px 0 0;
  border-top: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.session-pool-banner .pool-row {
  display: flex;
  justify-content: space-between;
  font-variant-numeric: tabular-nums;
}
.session-pool-banner .pool-row dt {
  margin: 0;
  font-weight: normal;
}
.session-pool-banner .pool-row dd {
  margin: 0;
  color: var(--overflow-state);
  font-weight: 600;
}

/* ============================================================
 * v1.4.3 — TYPOGRAPHY PASS (global readability bump)
 * ------------------------------------------------------------
 * Player feedback (4x): "TEXT IS TOO SMALL. IT IS SO HARD TO
 * READ, particularly the [tags] and other supplementary items.
 * The menus, the wizard, the main game, ALL TOO SMALL."
 *
 * Strategy: append-only overrides at the END of styles.css so
 * the prior cascade is preserved + this pass is easy to back
 * out if it breaks layouts. Bumps are aggressive on supplementary
 * (tags, DC pills, XP badges, labels, stepper) where the player
 * specifically pointed; conservative on already-large elements.
 *
 * Changes:
 *   base body 20 → 22 (+10%)
 *   small supplementary (11-13 → 15-17, +30-50%)
 *   wizard cards + buttons + inputs +1-3px
 *   chip [tag] badges + DC pill + +N XP badge bumped most
 * ============================================================ */

html, body {
  font-size: 24px;          /* was 20px (v1.3) */
}

/* Section labels — used everywhere (wizard "Choose an archetype",
 * side-pane category headers, chip "Choices" label). Was 11px. */
.label {
  font-size: 18px;          /* was 11px — biggest jump on the loudest pain */
  letter-spacing: 0.18em;   /* tighten slightly so larger text doesn't sprawl */
}
.side-pane .label {
  font-size: 18px;          /* was 13px */
}

/* Small-caps eyebrow text — chapter headers, character sheet header */
.small-caps {
  font-size: 0.92em;        /* relative bump rides the body increase */
}

/* Wizard card grid — "Choose an archetype", origin, loadout pickers */
.choice .title {
  font-size: 18px;          /* was 13px */
  letter-spacing: 0.08em;
}
.choice .desc {
  font-size: 19px;          /* was 14px */
  line-height: 1.55;
}

/* Stepper crumb text (Archetype → Identity → Traits → ...) */
.stepper {
  font-size: 16px;          /* was 10px parent — was nearly invisible */
}
.stepper .step {
  font-weight: 500;
}

/* Buttons — primary action everywhere */
.btn {
  font-size: 18px;          /* was 13px */
  letter-spacing: 0.10em;
}

/* Inputs / textareas — wizard name + concept fields, etc. */
.input, .textarea {
  font-size: 21px;          /* was 17px */
}

/* Chip metadata — the user explicitly called out [tags]
 * being too small. These are the worst offenders. */
.trigger-badge {
  font-size: 18px;          /* was 12px */
  font-weight: 500;
}
.suggested-action-dc {
  font-size: 18px;          /* was 12px */
  font-weight: 500;
}
.suggested-action-dc-skill {
  font-size: 17px;          /* match dc bump */
}
.xp-preview-badge {
  font-size: 18px;          /* was 12px */
  font-weight: 500;
}
.suggested-action-num {
  font-size: 18px;          /* was ~12px — chip 1/2/3 number tile */
  min-width: 28px;
  padding: 4px 8px;
}

/* Trait chips on chargen Review */
.chip {
  font-size: 17px;          /* was inherited / smaller */
  padding: 6px 12px;
}

/* SidePane lore + item rows — the Pack/Book/Vows tab content */
.lore-entry .name {
  font-size: 19px;          /* lift from default body */
}
.lore-entry .desc {
  font-size: 18px;
  line-height: 1.55;
}
.item-row .name {
  font-size: 19px;
}
.item-row .qty {
  font-size: 17px;          /* was 12px */
}
.item-row .note {
  font-size: 17px;          /* was 12px */
}

/* Suggested-action chip body text on play screen — only bumped
 * on the chip-button-rule itself (mobile rule keeps its responsive size). */
.suggested-action {
  min-height: 56px;
}
.suggested-action-text {
  font-size: 21px;          /* readable chip body across viewports */
  line-height: 1.45;
}

/* REQ THINK-05 — .thinking-preview / -label / -text rules. v1.5 P24: italic + dimmed + max-height 240px with overflow-y auto (auto-scroll). This block reads var(--ink-shadow) + var(--ink-muted) with rgba fallbacks (audit at 024-05-PLAN.md::Task 3). NOTE: until P34 those two tokens were never DECLARED anywhere, so the fallbacks always won and this text rendered palette-blind grey in every palette — the previous claim here that "both parchment and observatory palettes are honored" was false. As of P34 (THEME-02) both tokens are declared in every palette block at the top of this file (--ink-muted: var(--ink-faded), --ink-shadow: var(--rule)), so the claim is true now. Editing this block requires re-running T4 (proxy/test-runner/t4-fullrun.js) which asserts computed-style on both palettes (Plan 07). */
/* v1.5 — extended-thinking preview block. Renders the model's literal
 * internal scratch-pad during the thinking phase (max-effort Opus / Sonnet
 * sit silent for 3-5min before any text otherwise). Dimmed + italic so
 * the player reads it as distinct from in-fiction narration; clearly
 * "GM is thinking" not "GM is narrating".
 *
 * Layout: indented block with a subtle left rule, label sits above the
 * scratch text in a smaller weight. The narration-cursor reuses the
 * same blinking ▍ used by streamingNarration so the visual continuity
 * carries when text generation eventually starts. */
.thinking-preview {
  margin: 12px 0 20px;
  padding: 12px 16px;
  border-left: 3px solid var(--ink-shadow, rgba(120, 120, 120, 0.35));
  background: rgba(120, 120, 120, 0.05);
  border-radius: 4px;
  opacity: 0.8;
}
.thinking-preview-label {
  font-size: 16px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted, rgba(120, 120, 120, 0.85));
  margin-bottom: 8px;
}
.thinking-preview-text {
  font-style: italic;
  font-size: 18px;
  line-height: 1.55;
  color: var(--ink-muted, rgba(120, 120, 120, 0.9));
  white-space: pre-wrap;
  /* Cap visible height so very long thinking traces don't blow out the
   * viewport. Internal scroll keeps recent thinking in view. */
  max-height: 240px;
  overflow-y: auto;
}

/* ============================================================
 * REQ OVERFLOW-V15-01 (P26) — Per-source attribution sub-chips
 * ------------------------------------------------------------
 * Renders inside .xp-preview-badge.overflow-active and inside
 * .skill-row .skill-level when effectiveSkill > 100 AND the helper
 * exposes non-zero per-source contributions via
 * SkillsLib.effectiveSkillBreakdown(...) (lib/skills.js:315).
 *
 * Each .overflow-source-chip is a small inline pill with a thin
 * outline; color is selected by [data-source] attribute via the
 * existing --overflow-{gear,feat,state} tokens (already defined in
 * both parchment and observatory palettes at the top of this file).
 * ============================================================ */
.overflow-source-cluster {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-left: 6px;
  vertical-align: baseline;
}
.overflow-source-chip {
  display: inline-block;
  font-size: 13px;
  padding: 1px 5px;
  border-radius: 3px;
  border: 1px solid currentColor;
  background: transparent;
  font-family: 'Cinzel', serif;
  letter-spacing: 0.04em;
  line-height: 1.4;
  white-space: nowrap;
}
.overflow-source-chip[data-source="gear"]   { color: var(--overflow-gear); }
.overflow-source-chip[data-source="feat"]   { color: var(--overflow-feat); }
.overflow-source-chip[data-source="rested"] { color: var(--overflow-state); }
/* Forward-compat fallback for unknown source labels (future buffs:
 * 'blessed', 'inspired', etc.) — render in the neutral base color so
 * the chip is still visible without a CSS edit blocking the launch. */
.overflow-source-chip:not([data-source="gear"]):not([data-source="feat"]):not([data-source="rested"]) {
  color: var(--overflow-base);
}

/* Narrow-viewport collapse — preserve the chip's single-line shape on
 * phones <= 360px wide. Per CONTEXT D-12 (wraps OR collapses to '+X
 * total'): we collapse via ::after using the data-overflow-total
 * attribute set in the JSX wrapper. The individual chips are hidden,
 * replaced with one summary literal so a Lore L100 with stacked bonuses
 * still reads as a single-line badge. SidePane skill-row uses the same
 * mechanism (its level-row text already includes the X/100 number, so
 * we just hide the cluster — no ::after needed there). */
@media (max-width: 360px) {
  .xp-preview-badge.overflow-active .overflow-source-cluster { display: none; }
  .xp-preview-badge.overflow-active::after {
    content: '+' attr(data-overflow-total) ' total';
    font-size: 13px;
    margin-left: 4px;
    color: var(--overflow-state);
    font-style: italic;
    font-family: 'Cinzel', serif;
    letter-spacing: 0.04em;
  }
  .skill-row .overflow-source-cluster { display: none; }
}

/* ============================================================
   REQ MIGR-V15-01 — SettingsModal Save section (P26 Plan 03)
   Most visual styling lives inline in the JSX (play.jsx
   SaveTabSection) to keep the change footprint contained.
   This block adds: hover affordance for backup rows, fade-in
   animation for the import error banner, and a --danger token
   fallback (existing call sites already use var(--danger, ...)
   with hex fallbacks; defining the token here lets the palette
   override take effect cleanly).
   ============================================================ */

/* --danger token — MOVED to the two palette blocks at the top of this file in
   P34 (THEME-02). It is still defined per palette (parchment #a83232,
   observatory #f08080, both hexes unchanged) so the inline
   var(--danger, #b00) fallbacks in SaveTabSection resolve to the
   palette-appropriate red, and the cache-pill call sites at styles.css:1622 +
   1650 still benefit. The two stray single-line `:root` rules that used to sit
   here were a parser hazard: a first-match `/:root\s*\{([^}]*)\}/` regex reads
   the top block and silently drops --danger, so the token contract test would
   report a phantom violation. Do not re-add a `:root` rule at the bottom of
   this file — put new tokens in the palette blocks at the top. */

.settings-section-header {
  /* Hook for future styling — see SaveTabSection inline `style` prop. */
}

.save-tab-section .save-import-error {
  animation: save-fade-in 200ms ease;
}

.save-tab-section .backup-row {
  transition: background 120ms ease;
}
.save-tab-section .backup-row:hover {
  background: var(--paper-2);
}
.save-tab-section .backup-row:last-child {
  border-bottom: none;
}

@keyframes save-fade-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ============================================================
   REQ WORLD-V15-01 + CHAR-V15-01 — the creation shell on a phone.
   Adapts the v1.2 ledger drawer full-screen pattern to creation: under
   768px the interview is a full-screen scroller with its primary action
   pinned to the bottom edge.

   v1.9 front door: ONLY the geometry T4 pins lives here (the shell is
   position:fixed, top 0, full height; .wizard-cta-sticky is position:fixed,
   bottom 0, >=44px). Everything visual — the ground showing through, the
   frosted header, the dock plate, the fade above it — is in the FRONT DOOR
   block at the end of this file. The old stepper / top-bar / frontispiece
   rules went with the wizards they served.

   CONTEXT D-14: <768px breakpoint matches the v1.2 drawer pattern.
   CONTEXT D-15: >=44pt touch target on the sticky CTA, above
                 safe-area-inset-bottom.
   ⚠ Nothing between .wizard-shell and .wizard-cta-sticky may carry a
   transform, filter or backdrop-filter (or an animation that sets one):
   that re-anchors the fixed CTA to the transformed box. See interview.jsx.
   ============================================================ */

@media (max-width: 767px) {
  .wizard-shell {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    height: 100dvh;
    width: 100vw;
    max-width: none;
    margin: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    z-index: 100;
  }

  .wizard-shell .wizard-cta-sticky {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 110;
    min-height: 44px;
    margin: 0;
  }
}

/* ============================================================
   v1.7 P36 — Prose Arrival + Staged Inline Dice
   (PROSE-01..05, DICE-01..02)

   Ported from sketch findings (003 winner B "paragraph ink-in",
   002 winner B "staged inline d20", composed in 005):
   .claude/skills/sketch-findings-TRPG/references/{prose-typography,
   dice-feedback}.md. Every colour resolves through palette tokens —
   zero per-palette forks (SC2). Every rule is scoped
   html[data-screen="play"] so it wins the cascade against the base
   .top-bar / mobile override blocks earlier in this file, and every
   animation here has a prefers-reduced-motion arm at the bottom of
   this section where content appears complete and instant (PROSE-05).
   Placement at END of file is deliberate: last-wins at equal
   specificity.
   ============================================================ */

/* ---- PROSE-02 — the quill line. From send until the first WHOLE
   paragraph lands: italic, muted, fleuron in gilt, CSS-animated
   trailing ellipsis. Removed by the render gate the moment real
   prose exists. ---- */
html[data-screen="play"] .quill-line {
  font-family: 'EB Garamond', serif;
  font-style: italic;
  font-size: 19px;
  color: var(--ink-faded);
  margin: 8px 0 4px;
}
html[data-screen="play"] .quill-line::before {
  content: '\2766\00a0';
  font-style: normal;
  color: var(--gold);
  opacity: 0.7;
}
html[data-screen="play"] .quill-line::after {
  content: '\2026';
  display: inline-block;
  margin-left: 2px;
  animation: quillDots 1.4s ease-in-out infinite;
}
@keyframes quillDots {
  0%, 100% { opacity: 0.2; }
  50%      { opacity: 1; }
}

/* ---- PROSE-01 — paragraph ink-in. A paragraph is either absent or
   whole; when it arrives it rises + unblurs as one block. The class
   is assigned once per paragraph identity (play.jsx ink ledger) and
   never re-applied, so settled prose never re-animates. fill-mode
   `both` holds the hidden start state through any stagger delay and
   the visible end state after — removing the class later is a visual
   no-op. transform/opacity/filter only: never moves layout, never
   changes scrollHeight, and the scroll policy ignores it by design. ---- */
html[data-screen="play"] .feed .prose-para-in {
  animation: proseInkIn 700ms ease both;
}
@keyframes proseInkIn {
  from { opacity: 0; transform: translateY(12px); filter: blur(2px); }
  to   { opacity: 1; transform: translateY(0);    filter: blur(0); }
}

/* ---- PROSE-03 — scene titles as QUIET small-caps rules. The ornate
   Cinzel-uppercase plate reads as chrome; the quiet rule reads as a
   chapter breath (prose-typography.md "Scene rules (quiet)"). Base
   .scene-title-block keeps its centring; this override drops the
   heavy top border, switches the title to small-caps EB Garamond and
   dims the fleuron. The gilt drop cap on the scene's first paragraph
   is the existing .dropcap mechanism, lifted from accent to gold. ---- */
html[data-screen="play"] .scene-title-block {
  border-top: 0;
  padding-top: 0;
  margin: 36px auto 12px;
}
html[data-screen="play"] .scene-title-text {
  font-family: 'EB Garamond', serif;
  font-variant: small-caps;
  text-transform: none;
  letter-spacing: 0.3em;
  font-size: 20px;
  color: var(--ink-2);
  margin-bottom: 6px;
}
html[data-screen="play"] .scene-title-block .ornament-glyph {
  color: var(--ink-faded);
  font-size: 13px;
}
html[data-screen="play"] .narration.dropcap > p:first-of-type::first-letter {
  color: var(--gold);
}

/* ---- DICE-01 — the strict stage grid. Die pinned left spanning the
   card's working rows; modifier chips and the result line stack in
   the right column so nothing drifts or bounces mid-ceremony (the
   sketch's loose-flex misalignment fix). Every staged element holds
   its layout slot from mount — the ceremony animates opacity and
   transform only, so the card's height is constant from the moment
   the ScrollPolicy snap fired at its appearance. ---- */
html[data-screen="play"] .roll-card-stage {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: 18px;
  align-items: center;
  margin-bottom: 14px;
}
html[data-screen="play"] .roll-card-dice {
  flex-direction: column;
  flex-wrap: nowrap;
  gap: 8px;
  margin-bottom: 0;
  align-self: center;
}
html[data-screen="play"] .roll-card-flow {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
}
html[data-screen="play"] .roll-card-mods {
  min-height: 34px;
}

/* ---- DICE-01 — ceremony staging classes. cx-hidden = not yet
   revealed (layout slot held, invisible); cx-in = fly-in on reveal;
   cx-stamp = the verdict stamp. Static cards carry none of these. ---- */
html[data-screen="play"] .roll-card .cx-hidden {
  opacity: 0;
}
html[data-screen="play"] .roll-card .cx-in {
  animation: rcFlyIn 350ms cubic-bezier(0.2, 1.6, 0.4, 1) both;
}
@keyframes rcFlyIn {
  from { opacity: 0; transform: translateY(14px) scale(0.8); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
html[data-screen="play"] .roll-card .roll-card-verdict.cx-stamp {
  animation: rcStamp 300ms cubic-bezier(0.3, 1.2, 0.5, 1) both;
}
@keyframes rcStamp {
  from { opacity: 0; transform: rotate(-4deg) scale(2.6); }
  to   { opacity: 1; transform: rotate(0deg) scale(1); }
}

/* ---- DICE-02 — natural-face treatment, token-driven. Gold glow on a
   natural 20 (--glow-gilt is the palette's own layered glow; arcane
   ships core+corona), red on a natural 1. Persistent on static cards
   too: same signal under reduced motion, in history, everywhere. ---- */
html[data-screen="play"] .d20-face.nat20 {
  border-color: var(--verdict-crit);
  box-shadow: var(--glow-gilt);
}
html[data-screen="play"] .d20-face.nat20 .d20-num {
  color: var(--verdict-crit);
}
html[data-screen="play"] .d20-face.nat1 {
  border-color: var(--verdict-fail);
  box-shadow: 0 0 14px color-mix(in srgb, var(--verdict-fail) 60%, transparent);
}
html[data-screen="play"] .d20-face.nat1 .d20-num {
  color: var(--verdict-fail);
}

/* ---- PROSE-05 + DICE + P37 DOCK reduced motion — simplify, never remove.
   Prose paragraphs appear complete the instant their text closes
   (arrival cadence is the stream itself, which is data, not motion).
   Dice: the JS timeline already collapses to one complete stage; the
   cx-hidden override below is the CSS-level backstop guaranteeing
   zero information loss even if a staged class ever renders under
   reduced motion. Same numbers, same verdicts, same scene signals.
   P37 extends the same contract to the overlay action dock: the panel
   appears and dismisses complete and instant (no slide — transition:
   none also collapses the visibility delay, so closed is hidden NOW),
   and the handle's new-actions pulse degrades to a static gold marker
   (the `fresh` colour stays; only the throb goes). Same count, same
   choices, zero information loss. ---- */
@media (prefers-reduced-motion: reduce) {
  html[data-screen="play"] .feed .prose-para-in { animation: none; }
  html[data-screen="play"] .quill-line::after { animation: none; }
  html[data-screen="play"] .narration-cursor { animation: none; }
  html[data-screen="play"] .roll-card .cx-in,
  html[data-screen="play"] .roll-card .roll-card-verdict.cx-stamp { animation: none; }
  html[data-screen="play"] .roll-card .cx-hidden { opacity: 1; }
  html[data-screen="play"] .d20-face.tumbling { animation: none; }
  html[data-screen="play"] .d20-face.tumbling .d20-num { animation: none; }
  html[data-screen="play"] .roll-card-tag.rolling-tag { animation: none; }
  html[data-screen="play"] .feed.revealing { cursor: default; }
  html[data-screen="play"] .feed.revealing::after { display: none; }
  html[data-screen="play"] .actions-panel { transition: none; transform: none; }
  /* The OPEN state must be restated at its own specificity: .actions-panel.open
     re-declares its transition at (0,3,1), which outranks the bare rule above
     at (0,2,1) — and transitions are read from the DESTINATION state, so
     without this the panel still FADED IN over 220ms under reduced motion
     (dismiss was already instant; the open edge was the gap). Same pattern as
     the .fresh:not(:disabled) restatement below. */
  html[data-screen="play"] .actions-panel.open { transition: none; }
  html[data-screen="play"] .actions-handle.fresh:not(:disabled) { animation: none; }
  html[data-screen="play"] .actions-handle { transition: none; }
}

/* ============================================================================
   AAA SHELL LAYER — the play screen as "a beautiful, serious book that plays".
   ----------------------------------------------------------------------------
   Owner: AAA SHELL workstream. Spec: .planning/aaa/design/DESIGN.md.
   This section is AUTHORITATIVE for the play layout at every viewport, the
   header meters, the prose surface, the inline dice + turn-summary cards, the
   choice sheet, the composer and the ledger SHELL (rail / drawer / tab bar).
   It sits after every earlier play-scoped block ON PURPOSE (last wins at equal
   specificity) and before the interview section, which belongs to FRONT DOOR.

   Rules of the layer:
     · every value is a DESIGN SCALE token (--space-*, --text-*, --radius-*,
       --dur-*) or a palette token — no raw hex; shadows via --elev-*.
     · `color:` names palette tokens directly (or color-mix of them) — the
       contrast suite gates every one.
     · NOTHING here puts transform / filter / backdrop-filter / will-change /
       contain on .play-shell or .shell-body (contract A4), no blanket
       `.play-shell > *` rule (A2), no new scroller outside the allow-list (A3),
       and every play-scoped rule whose SUBJECT is .side-pane is bracketed in
       @media (min-width: 769px) (A6).
   ============================================================================ */

/* ---- 1. GEOMETRY — the rail track widens with the screen. ------------------ */
html[data-screen="play"] .play-shell { --shell-rail-w: 320px; }
@media (min-width: 1280px) { html[data-screen="play"] .play-shell { --shell-rail-w: 360px; } }
@media (min-width: 1680px) { html[data-screen="play"] .play-shell { --shell-rail-w: 400px; } }

/* ---- 2. THE AMBIENT LAYER (THREE's mount point) + the candle. --------------
   Absolute, inset 0, z-index 0, inert: behind the header (z2), the body (z2)
   and the dock (z3), in front of the page ground. Its ::before is a warm pool
   of light over the reading column — the CSS fallback a canvas may cover. */
html[data-screen="play"] .shell-ambient {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}
html[data-screen="play"] .shell-ambient::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 42% 58% at 50% 30%, var(--shell-candle), transparent 72%),
    radial-gradient(ellipse 70% 40% at 50% 104%, var(--shell-hair-soft), transparent 70%);
}
@media (min-width: 769px) {
  html[data-screen="play"] .shell-ambient::before { right: var(--shell-rail-w); }
}

/* ---- 3. THE HEADER — meters left, running head centred over the prose,
   the one door (menu) at the right. A quiet plate: translucent page colour,
   one gilt hairline, no heavy gradient. Grid columns mirror the body so the
   running head sits over the reading column, not the viewport. -------------- */
html[data-screen="play"] .shell-hdr {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) var(--shell-rail-w);
  align-items: center;
  column-gap: var(--space-5);
  row-gap: 0;
  min-height: 64px;
  padding: var(--space-2) var(--space-5);
  border-bottom: 1px solid var(--shell-hair);
  background: color-mix(in srgb, var(--paper) 74%, transparent);
  box-shadow: var(--elev-1);
}
html[data-screen="play"] .shell-meters {
  grid-column: 1;
  order: 0;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-5);
  width: auto;
  min-width: 0;
}
html[data-screen="play"] .shell-meter {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--space-2);
  flex: 0 1 auto;
  width: auto;
  max-width: none;
  min-width: 0;
}
html[data-screen="play"] .shell-meter .m-icon {
  display: inline-flex;
  flex: 0 0 auto;
  color: var(--ink-faded);
}
html[data-screen="play"] .shell-meter.m-hp .m-icon   { color: var(--danger); }
html[data-screen="play"] .shell-meter.m-mana .m-icon { color: var(--trigger-skill); }
html[data-screen="play"] .shell-meter.m-stam .m-icon { color: var(--ink-2); }
html[data-screen="play"] .shell-meter .m-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 124px;
  min-width: 0;
}
/* tabular lining numerals keep the plate's box identical across turns (the
   old mono face did that job; T4 SHELL_CHROME_FIXED_ACROSS_TURNS). */
html[data-screen="play"] .shell-meter .m-label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  line-height: 1.1;
  letter-spacing: 0;
  font-variant-numeric: lining-nums tabular-nums;
  white-space: nowrap;
}
html[data-screen="play"] .shell-meter .m-key { color: var(--ink-faded); font-weight: 500; font-size: var(--text-md); }
html[data-screen="play"] .shell-meter .m-val { color: var(--ink); font-weight: 600; font-size: var(--text-lg); }
html[data-screen="play"] .shell-meter .m-max { color: var(--ink-faded); font-weight: 400; font-size: var(--text-sm); }
html[data-screen="play"] .shell-meter .m-bar {
  height: 4px;
  border-radius: var(--radius-round);
  background: var(--tint-sunken);
  box-shadow: inset 0 0 0 1px var(--border-soft);
  overflow: hidden;
}
html[data-screen="play"] .shell-meter .m-fill {
  height: 100%;
  border-radius: var(--radius-round);
  transition: width var(--dur-slow) var(--ease-out);
}
/* The running head — a printed book's title line, centred over the READING
   COLUMN (not the viewport): absolutely placed at the column's midpoint so the
   meters' width can never push it off-centre. Shown only where the column is
   wide enough that it cannot meet the meters (>= 1760px, see below: the
   meters end at ~522px, and the column midpoint only clears them by a safe
   margin once the viewport is ~1760 wide with the 400px rail). */
html[data-screen="play"] .shell-runhead {
  position: absolute;
  top: 50%;
  left: calc((100% - var(--shell-rail-w)) / 2);
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-width: 0;
  max-width: 34em;
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-variant-caps: all-small-caps;
  letter-spacing: 0.1em;
  color: var(--ink-faded);
  white-space: nowrap;
}
html[data-screen="play"] .shell-runhead > span:not(.shell-runhead-sep) { overflow: hidden; text-overflow: ellipsis; }
html[data-screen="play"] .shell-runhead-sep {
  flex: 0 0 auto;
  width: 5px;
  height: 5px;
  background: var(--gold);
  opacity: 0.7;
  transform: rotate(45deg);
}
/* The one door. 44x44, one icon, a hairline edge; gold on hover (agency). */
html[data-screen="play"] .shell-menu-btn {
  grid-column: 4;
  justify-self: end;
  width: var(--tap);
  height: var(--tap);
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-1);
  background: transparent;
  color: var(--ink-2);
  letter-spacing: 0;
  transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
html[data-screen="play"] .shell-menu-btn:hover {
  color: var(--gold);
  border-color: var(--shell-hair);
  background: var(--hover-wash);
}
html[data-screen="play"] .shell-menu-btn:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
@media (max-width: 1759px) {
  html[data-screen="play"] .shell-runhead { display: none; }
}
@media (max-width: 1023px) {
  html[data-screen="play"] .shell-hdr { grid-template-columns: minmax(0, 1fr) auto; }
  html[data-screen="play"] .shell-menu-btn { grid-column: 2; }
}

/* ---- 4. THE READING COLUMN. The feed spans the whole column (wheel + the
   scrollbar live at its edge); the MEASURE is padding: (100% - measure) / 2,
   floored at --space-5. --measure is in em, and the feed's font-size is the
   prose size, so the line length stays ~66-72 characters at every width. --- */
html[data-screen="play"] .play-shell .feed {
  font-size: var(--text-prose);
  max-width: none;
  width: 100%;
  margin: 0;
  padding: var(--space-7) max(var(--space-5), calc((100% - var(--measure)) / 2)) var(--space-8);
  /* THE RHYTHM HAS ONE SOURCE. .feed is a flex column, where margins never
     collapse — paragraph margin + the old 28px gap + each entry's own margin
     stacked into 80-100px holes. Now: one 24px gap between entries, the last
     paragraph carries no margin, and only the player's line asks for extra. */
  gap: var(--space-5);
}
html[data-screen="play"] .shell-main { padding: 0; }
/* The page's own margin rules: two faint gilt hairlines a hand's breadth
   outside the text block — a manuscript's ruled margins, fixed to the column
   (background-attachment: scroll on a scroller stays with the box), never
   touching a glyph. Hidden where the gutters are too narrow to hold them. */
@media (min-width: 1024px) {
  html[data-screen="play"] .play-shell .feed {
    background-image:
      linear-gradient(transparent, var(--shell-hair-soft) 18%, var(--shell-hair-soft) 82%, transparent),
      linear-gradient(transparent, var(--shell-hair-soft) 18%, var(--shell-hair-soft) 82%, transparent);
    background-size: 1px 100%, 1px 100%;
    background-repeat: no-repeat;
    background-position:
      calc(50% - var(--measure) / 2 - var(--space-7)) 0,
      calc(50% + var(--measure) / 2 + var(--space-7)) 0;
  }
}

/* Prose. Warm ink (a touch of gold in the white = candlelight on vellum),
   generous leading, old-style figures, no coloured dialogue. */
html[data-screen="play"] .feed .narration {
  font-family: var(--font-serif);
  font-size: var(--text-prose);
  line-height: var(--leading-prose);
  color: color-mix(in srgb, var(--ink) 90%, var(--gold));
  max-width: none;
  font-variant-numeric: oldstyle-nums proportional-nums;
  font-kerning: normal;
}
html[data-screen="play"] .feed .narration { margin: 0; }
html[data-screen="play"] .feed .narration p {
  margin: 0 0 0.9em;
  text-wrap: pretty;
}
html[data-screen="play"] .feed .narration p:last-child { margin-bottom: 0; }
/* Dialogue is set as the book sets it: in the text, a shade brighter, never
   a second accent colour. */
html[data-screen="play"] .feed .narration .quoted { color: var(--ink); font-weight: 500; }
html[data-screen="play"] .feed .narration .letter-fragment { color: var(--ink); font-weight: 500; letter-spacing: 0; }
html[data-screen="play"] .feed .narration strong { color: var(--ink); font-weight: 600; }
/* The drop cap: gilt Cinzel, two lines tall, optically seated. */
html[data-screen="play"] .feed .narration.dropcap > p:first-of-type::first-letter {
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--gold);
  float: left;
  font-size: 3.15em;
  line-height: 0.8;
  padding: 0.075em 0.09em 0 0;
  margin: 0;
}
/* The player's own line — a gamebook reply: "You" in gilt small caps
   (agency), the words in italic, one gilt hairline in the margin. */
html[data-screen="play"] .feed .player-action {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  max-width: none;
  margin: var(--space-3) 0 0;
  padding: var(--space-1) 0 var(--space-1) var(--space-4);
  border-left: 1px solid var(--shell-hair);
  background: none;
  font-family: var(--font-serif);
  font-size: calc(var(--text-prose) * 0.92);
  font-style: italic;
  font-weight: 400;
  line-height: 1.5;
  color: var(--ink-2);
}
html[data-screen="play"] .feed .player-action::before { content: none; }
html[data-screen="play"] .feed .player-action-tag {
  flex: 0 0 auto;
  margin: 0;
  font-family: var(--font-serif);
  font-style: normal;
  font-size: var(--text-lg);
  font-weight: 600;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.08em;
  text-transform: none;
  color: var(--gold);
  opacity: 1;
}
/* Scene breaks: a chapter breath — small-caps title over a gilt lozenge
   between two hairlines. */
html[data-screen="play"] .feed .scene-title-block {
  max-width: none;
  margin: var(--space-5) auto 0;
  padding: 0;
  border: 0;
  text-align: center;
}
html[data-screen="play"] .feed .scene-title-text {
  font-family: var(--font-serif);
  font-size: var(--text-title);
  font-variant-caps: all-small-caps;
  letter-spacing: 0.14em;
  text-transform: none;
  color: var(--ink-2);
  margin-bottom: var(--space-2);
}
html[data-screen="play"] .feed .scene-title-block .ornament { gap: var(--space-3); }
html[data-screen="play"] .feed .scene-title-block .ornament::before,
html[data-screen="play"] .feed .scene-title-block .ornament::after {
  max-width: 96px;
  background: var(--shell-hair);
}
html[data-screen="play"] .ornament-lozenge {
  display: block;
  width: 6px;
  height: 6px;
  background: var(--gold);
  opacity: 0.8;
  transform: rotate(45deg);
}
/* The ink blot at the end of a streaming paragraph (was a blinking block ▍
   in the rose accent): a gilt dot that breathes. The glyph stays in the DOM
   for the legacy arm; it is simply not painted. */
html[data-screen="play"] .narration-cursor {
  display: inline-block;
  width: 0.32em;
  height: 0.32em;
  margin-left: 0.28em;
  vertical-align: 0.1em;
  border-radius: var(--radius-round);
  background: var(--gold);
  box-shadow: var(--glow-gilt);
  color: transparent;
  overflow: hidden;
  line-height: 0;
  animation: shellInkBreath 1.6s var(--ease-in-out) infinite;
}
@keyframes shellInkBreath { 50% { opacity: 0.3; } }
/* "The chronicler writes…" — a drawn quill instead of a dingbat. */
html[data-screen="play"] .feed .quill-line {
  display: block;
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-style: italic;
  color: var(--ink-faded);
}
html[data-screen="play"] .feed .quill-line::before {
  content: '';
  display: inline-block;
  vertical-align: -3px;
  margin-right: var(--space-3);
  width: 20px;
  height: 20px;
  opacity: 0.85;
  background-color: var(--gold);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 20l6-6M14 6c4 0 6 2 6 6-2 0-4 0-6 2s-2 4-4 4c-4 0-4-4-2-6s6-6 6-6z'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 20l6-6M14 6c4 0 6 2 6 6-2 0-4 0-6 2s-2 4-4 4c-4 0-4-4-2-6s6-6 6-6z'/%3E%3C/svg%3E");
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}
/* Tap-to-reveal hint while a paragraph or the dice are still arriving: a
   quiet pill in sentence case, not a Cinzel caps wall. */
html[data-screen="play"] .feed.revealing::after {
  content: 'Click to show the rest  \00B7  Esc';
  bottom: var(--space-3);
  margin: var(--space-5) auto 0;
  padding: var(--space-1) var(--space-4);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-round);
  background: color-mix(in srgb, var(--paper) 90%, transparent);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-style: italic;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-faded);
  opacity: 1;
}
@media (pointer: coarse) {
  html[data-screen="play"] .feed.revealing::after { content: 'Tap to show the rest'; }
}

/* ---- 5. THE INLINE DICE CARD — a plate in the book, not a HUD. One hairline
   frame, a verdict-coloured top rule, the die as a drawn hexagon, modifiers
   as quiet tags, the total in the book face, the verdict as an inked stamp.
   (The 3D dice OVERLAY belongs to THREE; this is only the inline card.) --- */
html[data-screen="play"] .feed .roll-card {
  width: 100%;
  max-width: none;
  margin: 0;
  padding: var(--space-5) var(--space-5) var(--space-4);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-2);
  background: linear-gradient(180deg, color-mix(in srgb, var(--rc-accent) 8%, var(--paper-2)), var(--paper-2) 60%);
  box-shadow: var(--elev-2);
  overflow: visible;
}
html[data-screen="play"] .feed .roll-card::before {
  content: '';
  position: absolute;
  left: var(--space-5);
  right: var(--space-5);
  top: -1px;
  height: 2px;
  border-radius: var(--radius-round);
  background: var(--rc-accent);
  opacity: 0.9;
}
html[data-screen="play"] .feed .roll-card.rolling::before { background: var(--gold); }
html[data-screen="play"] .roll-card-header {
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
html[data-screen="play"] .roll-card-icon { display: inline-flex; color: var(--ink-faded); }
html[data-screen="play"] .roll-card-label {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 600;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.08em;
  text-transform: none;
  color: var(--ink);
}
html[data-screen="play"] .roll-card-tag,
html[data-screen="play"] .roll-card-tag.adv,
html[data-screen="play"] .roll-card-tag.dis,
html[data-screen="play"] .roll-card-tag.rerolled,
html[data-screen="play"] .roll-card-tag.rolling-tag {
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-weight: 600;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.06em;
  text-transform: none;
  padding: 0 var(--space-2);
  border: 1px solid currentColor;
  border-radius: var(--radius-1);
  background: none;
}
html[data-screen="play"] .roll-card-tag.rolling-tag { color: var(--gold); margin-left: auto; }
html[data-screen="play"] .roll-card-stage { column-gap: var(--space-5); margin-bottom: var(--space-4); }
/* The die: a drawn hexagon (d20 silhouette) in the verdict colour, the kept
   face in the book numerals. Mask + background-color: one asset, any palette. */
html[data-screen="play"] .d20-face {
  width: 76px;
  min-width: 76px;
  height: 84px;
  min-height: 84px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  justify-content: center;
  gap: 0;
}
html[data-screen="play"] .d20-face::before {
  content: '';
  position: absolute;
  inset: 0;
  background-color: var(--rc-accent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 76 84'%3E%3Cpath d='M38 3l33 19v40L38 81 5 62V22z' fill='black' fill-opacity='.12' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 76 84'%3E%3Cpath d='M38 3l33 19v40L38 81 5 62V22z' fill='black' fill-opacity='.12' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}
html[data-screen="play"] .roll-card.rolling .d20-face::before {
  background-color: var(--gold);
  filter: drop-shadow(0 0 6px var(--gold));
}
/* The tumble keeps its spin but loses the rose box-shadow pulse: a glow drawn
   around a square box read as a dark tile behind the hexagon. The glow now
   lives on the hexagon (above). */
html[data-screen="play"] .d20-face.tumbling {
  animation: diceTumble 1100ms cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
}
html[data-screen="play"] .d20-face.tumbling .d20-num { color: var(--ink); }
html[data-screen="play"] .d20-face.secondary { width: 56px; min-width: 56px; height: 62px; min-height: 62px; opacity: 0.55; border-style: none; }
html[data-screen="play"] .d20-face .d20-num {
  position: relative;
  font-family: var(--font-serif);
  font-size: 34px;
  font-weight: 600;
  font-variant-numeric: lining-nums;
  line-height: 1;
  color: var(--ink);
}
html[data-screen="play"] .d20-face.secondary .d20-num { font-size: 24px; }
html[data-screen="play"] .d20-face .d20-tag {
  position: relative;
  margin-top: 2px;
  font-family: var(--font-serif);
  font-size: var(--text-xs);
  font-variant-caps: all-small-caps;
  letter-spacing: 0.08em;
  text-transform: none;
  color: var(--ink-faded);
}
html[data-screen="play"] .d20-face.nat20,
html[data-screen="play"] .d20-face.nat1 { box-shadow: none; }
html[data-screen="play"] .d20-face.nat20::before {
  background-color: var(--verdict-crit);
  filter: drop-shadow(0 0 6px var(--verdict-crit));
}
html[data-screen="play"] .d20-face.nat1::before { background-color: var(--verdict-fail); }
/* Modifiers: quiet tags, not raised chips. */
html[data-screen="play"] .roll-card-mods { gap: var(--space-2); min-height: 0; }
html[data-screen="play"] .mod-chip,
html[data-screen="play"] .mod-chip.negative {
  flex-direction: row;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-1);
  background: var(--tint-sunken);
}
html[data-screen="play"] .mod-chip-value {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 600;
  font-variant-numeric: lining-nums;
  color: var(--ink);
}
html[data-screen="play"] .mod-chip.negative .mod-chip-value { color: var(--danger); }
html[data-screen="play"] .mod-chip-label {
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-2);
}
/* Result line: the equation duplicated the tags above it — cut. What stays:
   the total in the book face, "vs DC", and the verdict stamp. */
html[data-screen="play"] .roll-card-result {
  align-items: center;
  gap: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border-soft);
}
html[data-screen="play"] .roll-card-equation { display: none; }
html[data-screen="play"] .total-num {
  font-family: var(--font-serif);
  font-size: 40px;
  font-weight: 600;
  font-variant-numeric: lining-nums;
  color: var(--ink);
}
html[data-screen="play"] .total-vs {
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-variant-caps: all-small-caps;
  letter-spacing: 0.08em;
  text-transform: none;
  color: var(--ink-faded);
}
html[data-screen="play"] .roll-card-verdict,
html[data-screen="play"] .roll-card-verdict.crit-success,
html[data-screen="play"] .roll-card-verdict.crit-fail {
  margin-left: auto;
  padding: var(--space-1) var(--space-3);
  border: 2px solid currentColor;
  border-radius: var(--radius-1);
  background: color-mix(in srgb, var(--rc-accent) 12%, transparent);
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 700;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.12em;
  text-transform: none;
  color: var(--rc-accent);
}
html[data-screen="play"] .roll-card-actions {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border-soft);
}
html[data-screen="play"] .btn-reroll {
  min-height: var(--tap);
  padding: 0 var(--space-4);
  gap: var(--space-2);
  border: 1px solid var(--shell-hair);
  border-radius: var(--radius-1);
  background: none;
  color: var(--gold);
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: none;
}
html[data-screen="play"] .btn-reroll:hover { background: var(--hover-wash); transform: none; }
html[data-screen="play"] .btn-reroll-icon { display: inline-flex; color: var(--gold); }

/* ---- 6. THE TURN SUMMARY — a ledger slip: one hairline plate, a
   small-caps head, label | value rows. ------------------------------------- */
html[data-screen="play"] .feed .turn-summary-card {
  width: 100%;
  max-width: none;
  margin: var(--space-2) 0 0;
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-2);
  background: color-mix(in srgb, var(--paper-2) 92%, transparent);
  box-shadow: var(--elev-1);
  font-family: var(--font-serif);
  font-size: var(--text-md);
  line-height: var(--leading-ui);
  color: var(--ink);
}
html[data-screen="play"] .turn-summary-card .ts-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
  font-size: var(--text-lg);
  font-weight: 600;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.1em;
  color: var(--ink-2);
}
html[data-screen="play"] .turn-summary-card .ts-head::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: var(--shell-hair);
}
html[data-screen="play"] .turn-summary-card .ts-list {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  column-gap: var(--space-5);
  row-gap: var(--space-2);
  margin: 0;
}
html[data-screen="play"] .turn-summary-card .ts-row { display: contents; }
html[data-screen="play"] .turn-summary-card dt { margin: 0; font-size: var(--text-sm); color: var(--ink-faded); line-height: 1.6; }
html[data-screen="play"] .turn-summary-card dd { margin: 0; color: var(--ink); }
html[data-screen="play"] .turn-summary-card .ts-lost dd { color: var(--ink-2); }

/* ---- 7. THE DOCK (at rest) — the handle alone, on a hairline. On desktop the
   plate covers only the reading column; the rail's own surface continues to
   the foot of the screen (::after), so the ledger reads as one full-height
   column instead of a panel sitting on a footer. -------------------------- */
html[data-screen="play"] .shell-dock {
  padding: var(--space-2) var(--space-5) calc(var(--space-2) + env(safe-area-inset-bottom));
  border-top: 1px solid var(--shell-hair);
  background: color-mix(in srgb, var(--paper) 80%, transparent);
  box-shadow: none;
}
@media (min-width: 769px) {
  html[data-screen="play"] .shell-dock {
    padding-right: calc(var(--space-5) + var(--shell-rail-w));
    border-top: 0;
    background: none;
  }
  html[data-screen="play"] .shell-dock::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    right: var(--shell-rail-w);
    z-index: -1;
    border-top: 1px solid var(--shell-hair);
    background: color-mix(in srgb, var(--paper) 80%, transparent);
  }
  html[data-screen="play"] .shell-dock::after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: var(--shell-rail-w);
    z-index: -1;
    border-left: 1px solid var(--shell-hair-soft);
    background: var(--paper-2);
  }
}
html[data-screen="play"] .actions-handle {
  gap: var(--space-3);
  max-width: none;
  min-height: 52px;
  padding: 0 var(--space-4);
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 500;
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--ink);
  border-radius: var(--radius-1);
  transition: color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
html[data-screen="play"] .actions-handle:hover:not(:disabled) { color: var(--ink); background: var(--hover-wash); }
html[data-screen="play"] .actions-handle-glyph {
  display: inline-flex;
  color: var(--gold);
  font-size: inherit;
  transition: transform var(--dur-base) var(--ease-out);
}
html[data-screen="play"] .actions-handle:hover:not(:disabled) .actions-handle-glyph { transform: translateY(-2px); }
html[data-screen="play"] .actions-handle-count {
  font-size: var(--text-sm);
  font-style: italic;
  color: var(--ink-faded);
}
html[data-screen="play"] .actions-handle-count::before {
  content: '';
  display: inline-block;
  width: 4px;
  height: 4px;
  margin: 0 var(--space-3) 0 0;
  vertical-align: 0.25em;
  background: var(--ink-faded);
  border-radius: var(--radius-round);
}
html[data-screen="play"] .actions-handle.fresh { color: var(--ink); }
html[data-screen="play"] .actions-handle.fresh .actions-handle-label { color: var(--gold); }
html[data-screen="play"] .actions-handle:disabled { opacity: 0.5; }

/* ---- 8. THE CHOICE SHEET — slides up over the prose. Solid page colour (it
   overlays text), a gilt hairline, a soft upward shadow, rounded top corners.
   Desktop: it covers only the reading column, never the ledger. ----------- */
html[data-screen="play"] .actions-panel {
  padding: var(--space-4) var(--space-5) 0;
  border-top: 1px solid var(--shell-hair);
  border-radius: var(--radius-2) var(--radius-2) 0 0;
  background: var(--paper);
  box-shadow: 0 -18px 40px -24px color-mix(in srgb, var(--paper) 30%, black), var(--elev-2);
  max-height: clamp(160px, calc(100vh - 100% - 200px), 64vh);
  max-height: clamp(160px, calc(100dvh - 100% - 200px), 64dvh);
  transform: translateY(12px);
  transition: transform var(--dur-slow) var(--ease-out), opacity var(--dur-base) var(--ease-out), visibility 0s linear var(--dur-slow);
}
html[data-screen="play"] .actions-panel.open {
  transform: translateY(0);
  transition: transform var(--dur-slow) var(--ease-out), opacity var(--dur-base) var(--ease-out), visibility 0s;
}
@media (min-width: 769px) {
  html[data-screen="play"] .actions-panel { right: var(--shell-rail-w); padding-right: var(--space-5); }
}
/* The sheet's content spans the prose measure plus the two margin-rule
   gutters — i.e. it sits exactly between the page's ruled margins. The inner
   takes the prose font-size only so that em-based --measure resolves the same
   as in the feed; every child below sets its own size. */
html[data-screen="play"] .actions-panel-inner {
  width: 100%;
  max-width: calc(var(--measure) + 2 * var(--space-7));
  margin: 0 auto;
  font-size: var(--text-prose);
}
html[data-screen="play"] .actions-panel .play-composer { max-width: none; }
html[data-screen="play"] .actions-panel .suggested-actions-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  max-width: none;
  margin: var(--space-1) 0 var(--space-3);
}
html[data-screen="play"] .choice-prompt {
  font-family: var(--font-serif);
  font-size: var(--text-title);
  font-style: italic;
  font-weight: 500;
  line-height: var(--leading-tight);
  color: var(--ink);
}
html[data-screen="play"] .actions-panel .suggested-actions-hint {
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-style: italic;
  color: var(--ink-faded);
}
html[data-screen="play"] .actions-panel .suggested-actions-hint kbd {
  min-width: 22px;
  padding: 0 var(--space-1);
  margin: 0 2px;
  border-radius: var(--radius-1);
  font-family: var(--font-serif);
  font-size: var(--text-xs);
  font-style: normal;
  font-weight: 600;
}
/* The list: one column, hairline separators, no boxes. */
html[data-screen="play"] .actions-panel .suggested-actions.choice-list {
  display: flex;
  flex-direction: column;
  gap: 0;
  width: 100%;
  max-width: none;
  margin: 0 0 var(--space-3);
  border-top: 1px solid var(--border-soft);
}
html[data-screen="play"] .actions-panel .suggested-action {
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr);
  column-gap: var(--space-4);
  align-items: start;
  min-height: 56px;
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-2);
  border: 0;
  border-bottom: 1px solid var(--border-soft);
  border-radius: 0;
  background: none;
  box-shadow: none;
  color: var(--ink);
  font-family: var(--font-serif);
  line-height: 1.35;
  text-align: left;
  transform: none;
  transition: background var(--dur-base) var(--ease-out);
}
html[data-screen="play"] .actions-panel .suggested-action:hover,
html[data-screen="play"] .actions-panel .suggested-action:focus-visible {
  background: linear-gradient(90deg, color-mix(in srgb, var(--gold) 10%, transparent), transparent 75%);
  border-color: var(--border-soft);
  box-shadow: none;
  transform: none;
}
html[data-screen="play"] .actions-panel .suggested-action:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }
html[data-screen="play"] .actions-panel .suggested-action:active { transform: none; background: color-mix(in srgb, var(--gold) 14%, transparent); }
/* The numeral: a gilt hairline ring — agency. Fills on hover. */
html[data-screen="play"] .actions-panel .suggested-action-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  min-width: 0;
  margin-top: -2px;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--gold) 55%, transparent);
  border-radius: var(--radius-round);
  background: none;
  color: var(--gold);
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 600;
  font-variant-numeric: lining-nums;
  letter-spacing: 0;
  transition: background var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
html[data-screen="play"] .actions-panel .suggested-action:hover .suggested-action-num {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--paper);
}
html[data-screen="play"] .actions-panel .suggested-action-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}
html[data-screen="play"] .actions-panel .suggested-action-text {
  font-size: 20px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--ink);
}
/* The meta line: one quiet line, items joined by a centred dot. */
html[data-screen="play"] .actions-panel .suggested-action-row.meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 0;
  row-gap: 2px;
  padding-left: 0;
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  letter-spacing: 0;
  line-height: 1.4;
  color: var(--ink-faded);
}
html[data-screen="play"] .actions-panel .suggested-action-row.meta > * + *::before {
  content: '\00B7';
  margin: 0 var(--space-2);
  color: var(--ink-faded);
}
html[data-screen="play"] .actions-panel .risk-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  border: 0;
  border-radius: 0;
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 600;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.06em;
  white-space: nowrap;
}
html[data-screen="play"] .actions-panel .risk-badge::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: var(--radius-round);
  background: currentColor;
}
/* Risk speaks the colour roles: safe green, a plain roll in neutral ink,
   a hard roll in danger red. Gold is left for agency (the numerals). */
html[data-screen="play"] .risk-badge.rb-safe { color: var(--verdict-success); }
html[data-screen="play"] .risk-badge.rb-mid  { color: var(--ink-2); }
html[data-screen="play"] .risk-badge.rb-high { color: var(--danger); }
html[data-screen="play"] .actions-panel .meta-skill { color: var(--ink-2); }
html[data-screen="play"] .actions-panel .trigger-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-weight: 400;
  letter-spacing: 0;
  color: var(--ink-faded);
}
html[data-screen="play"] .actions-panel .trigger-badge svg { color: var(--ink-faded); }
html[data-screen="play"] .actions-panel .xp-preview-badge {
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-weight: 400;
  font-style: italic;
  letter-spacing: 0;
  color: var(--ink-faded);
}
/* Inspiration: a quiet toggle row, gilt when armed. */
html[data-screen="play"] .actions-panel .inspire-bar { justify-content: flex-start; margin: 0 0 var(--space-3); }
html[data-screen="play"] .actions-panel .btn-inspire {
  min-height: var(--tap);
  padding: 0 var(--space-4);
  gap: var(--space-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-1);
  background: none;
  color: var(--ink-2);
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  box-shadow: none;
  transition: border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
html[data-screen="play"] .actions-panel .btn-inspire:hover { border-color: var(--shell-hair); background: var(--hover-wash); color: var(--ink); }
html[data-screen="play"] .actions-panel .btn-inspire.active {
  border-color: var(--cta-edge);
  background: var(--cta-fill);
  color: var(--cta-ink);
  font-weight: 600;
  box-shadow: var(--glow-gilt);
}
html[data-screen="play"] .actions-panel .btn-inspire-icon { display: inline-flex; color: var(--gold); font-size: inherit; }
html[data-screen="play"] .actions-panel .btn-inspire-count {
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-style: italic;
  font-weight: 400;
  color: var(--ink-faded);
  letter-spacing: 0;
}

/* ---- 9. THE COMPOSER — one field, its send button inside, sticky to the
   sheet's foot. The hint appears while the field has focus; the count only
   near the limit (play.jsx). -------------------------------------------- */
html[data-screen="play"] .actions-panel .play-composer {
  display: block;
  position: sticky;
  bottom: 0;
  z-index: 1;
  margin: 0;
  padding: var(--space-3) 0 var(--space-3);
  background: var(--paper);
  border-top: 0;
}
html[data-screen="play"] .composer-field {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  padding: var(--space-1);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-1);
  background: var(--tint-sunken);
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
html[data-screen="play"] .composer-field:focus-within {
  border-color: color-mix(in srgb, var(--gold) 70%, transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 16%, transparent);
}
html[data-screen="play"] .shell-dock .play-composer .composer-input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 44px;
  max-height: 176px;
  padding: 10px var(--space-3);
  border: 0;
  background: none;
  resize: none;
  field-sizing: content;
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  line-height: var(--leading-ui);
  color: var(--ink);
}
html[data-screen="play"] .shell-dock .play-composer .composer-input:focus-visible { outline: none; }
html[data-screen="play"] .composer-input::placeholder { color: var(--ink-faded); font-style: italic; opacity: 1; }
html[data-screen="play"] .shell-dock .play-composer .composer-send {
  flex: 0 0 auto;
  align-self: flex-end;
  height: var(--tap);
  min-height: var(--tap);
  min-width: var(--tap);
  padding: 0 var(--space-4);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  border-radius: var(--radius-1);
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: none;
}
/* Disabled reads as intentionally resting — an outline, never a grey slab. */
html[data-screen="play"] .shell-dock .play-composer .composer-send:disabled {
  opacity: 1;
  background: transparent;
  border-color: var(--border-soft);
  color: var(--ink-faded);
  box-shadow: none;
}
html[data-screen="play"] .composer-foot {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: 20px;
  margin-top: var(--space-1);
  padding: 0 var(--space-1);
  font-family: var(--font-serif);
  font-size: var(--text-xs);
  font-style: italic;
  color: var(--ink-faded);
}
html[data-screen="play"] .composer-foot .composer-key-hint {
  font-size: var(--text-xs);
  letter-spacing: 0;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
html[data-screen="play"] .play-composer:focus-within .composer-foot .composer-key-hint { opacity: 1; }
html[data-screen="play"] .composer-count { margin-left: auto; font-variant-numeric: lining-nums tabular-nums; }

/* ---- 10. THE LEDGER SHELL — a full-height rail on desktop: an opaque page
   tier (it is read at length), a hairline edge, a sticky tab bar and a fade
   at the foot so a clipped list reads as "more below", not as a bug. The TAB
   CONTENTS belong to SURFACES. -------------------------------------------- */
@media (min-width: 769px) {
  html[data-screen="play"] .side-pane {
    padding: 0 var(--space-5) var(--space-7);
    border-left: 1px solid var(--shell-hair-soft);
    background: var(--paper-2);
    -webkit-mask-image: linear-gradient(to bottom, black calc(100% - var(--space-6)), transparent);
            mask-image: linear-gradient(to bottom, black calc(100% - var(--space-6)), transparent);
  }
}
html[data-screen="play"] .side-pane .ledger-head {
  display: flex;
  align-items: center;
  min-height: 56px;
  padding-top: var(--space-2);
}
html[data-screen="play"] .side-pane .ledger-title {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-variant-caps: all-small-caps;
  letter-spacing: 0.12em;
  color: var(--ink-faded);
}
html[data-screen="play"] .side-pane .tab-row {
  position: sticky;
  top: 0;
  z-index: 3;
  display: flex;
  flex-wrap: nowrap;
  justify-content: space-between;
  gap: 0;
  row-gap: 0;
  margin: 0 calc(-1 * var(--space-5)) var(--space-5);
  padding: 0 var(--space-5);
  border-bottom: 1px solid var(--border-soft);
  background: var(--paper-2);
}
html[data-screen="play"] .side-pane .tab-row .tab {
  flex: 1 1 0;
  min-width: 0;
  min-height: 48px;
  padding: 0 var(--space-1);
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 500;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.08em;
  text-transform: none;
  text-align: center;
  color: var(--ink-faded);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
html[data-screen="play"] .side-pane .tab-row .tab:hover { color: var(--ink-2); }
html[data-screen="play"] .side-pane .tab-row .tab.active { color: var(--ink); border-bottom-color: var(--gold); }
html[data-screen="play"] .side-pane .tab-row .tab:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }

/* ---- 11. REDUCED MOTION for this layer — simplify, never remove. ---------- */
@media (prefers-reduced-motion: reduce) {
  html[data-screen="play"] .narration-cursor { animation: none; }
  html[data-screen="play"] .d20-face.tumbling { animation: none; }
  html[data-screen="play"] .actions-handle-glyph { transition: none; }
  html[data-screen="play"] .actions-handle:hover:not(:disabled) .actions-handle-glyph { transform: none; }
}

/* ---- 12. PHONE (<= 768px) — the same book, one column. ------------------- */
@media (max-width: 768px) {
  html[data-screen="play"] .shell-hdr {
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: var(--space-4);
    min-height: 60px;
    padding: calc(var(--space-2) + env(safe-area-inset-top)) var(--space-4) var(--space-2);
  }
  html[data-screen="play"] .shell-meters { gap: var(--space-4); width: 100%; }
  html[data-screen="play"] .shell-meter { flex: 1 1 0; gap: 6px; }
  html[data-screen="play"] .shell-meter .m-key { display: none; }
  html[data-screen="play"] .shell-meter .m-body { width: auto; flex: 1 1 auto; }
  html[data-screen="play"] .shell-meter .m-label { justify-content: flex-start; }
  html[data-screen="play"] .shell-menu-btn { grid-column: 2; }

  html[data-screen="play"] .play-shell .feed {
    padding: var(--space-6) var(--space-5) var(--space-7);
  }
  html[data-screen="play"] .shell-main { padding-left: 0; padding-right: 0; }
  html[data-screen="play"] .feed .player-action { margin: var(--space-2) 0 0; }
  html[data-screen="play"] .feed .roll-card { padding: var(--space-4); }
  html[data-screen="play"] .feed .roll-card::before { left: var(--space-4); right: var(--space-4); }
  html[data-screen="play"] .roll-card-stage { column-gap: var(--space-4); }
  html[data-screen="play"] .total-num { font-size: 34px; }
  html[data-screen="play"] .feed .turn-summary-card { padding: var(--space-4); }

  html[data-screen="play"] .shell-dock {
    padding: var(--space-1) var(--space-4) calc(var(--space-1) + env(safe-area-inset-bottom));
  }
  /* Taller on phone so four two-line choices + the composer fit without an
     inner scroll (a clipped last choice read as a bug). Measured at 390x844:
     ~566px of sheet, leaving ~160px of prose above it — the reading band. */
  html[data-screen="play"] .actions-panel {
    padding: var(--space-4) var(--space-4) 0;
    max-height: clamp(160px, calc(100vh - 100% - 176px), 72vh);
    max-height: clamp(160px, calc(100dvh - 100% - 176px), 72dvh);
  }
  html[data-screen="play"] .actions-panel .suggested-actions-hint { display: none; }
  html[data-screen="play"] .choice-prompt { font-size: 23px; }
  html[data-screen="play"] .actions-panel .suggested-action {
    grid-template-columns: 30px minmax(0, 1fr);
    column-gap: var(--space-3);
    padding: var(--space-3) var(--space-2) var(--space-3) 0;
  }
  html[data-screen="play"] .actions-panel .suggested-action-num { width: 30px; height: 30px; }
  html[data-screen="play"] .actions-panel .suggested-action-text { font-size: var(--text-lg); }
  html[data-screen="play"] .actions-panel .suggested-action-row.meta { padding-left: 0; }
  html[data-screen="play"] .actions-panel .btn-inspire-label-full { display: none; }
  html[data-screen="play"] .actions-panel .btn-inspire-label-short { display: inline; }
  html[data-screen="play"] .actions-panel .btn-inspire-count { display: none; }
  /* The sheet as a grid on phone: the inspiration toggle moves UP into the
     prompt row (right side, where desktop shows the keyboard hint) so it is on
     screen without scrolling past four choices; the narrative-token cluster
     (a textarea, not a toggle) keeps its full-width row after the list. */
  html[data-screen="play"] .actions-panel-inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "head insp" "list list" "rest rest" "comp comp";
    column-gap: var(--space-3);
    align-items: center;
  }
  html[data-screen="play"] .actions-panel .suggested-actions-header { grid-area: head; margin: 0 0 var(--space-3); }
  html[data-screen="play"] .actions-panel .suggested-actions.choice-list { grid-area: list; }
  html[data-screen="play"] .actions-panel .inspire-bar { grid-area: rest; }
  html[data-screen="play"] .actions-panel .inspire-bar:has(.btn-inspire) { grid-area: insp; align-self: start; margin: 0 0 var(--space-3); }
  html[data-screen="play"] .actions-panel .play-composer { grid-area: comp; }
  html[data-screen="play"] .actions-panel .play-composer { padding-bottom: var(--space-3); }
  html[data-screen="play"] .shell-dock .play-composer .composer-send { padding: 0; width: var(--tap); }
  html[data-screen="play"] .composer-send-label { display: none; }
  html[data-screen="play"] .composer-foot { display: none; }

  /* The ledger drawer's shell (the drawer itself is the <=768 .side-pane
     block earlier in this file; these are its chrome). */
  html[data-screen="play"] .side-pane .tab-row {
    margin: 0 calc(-1 * var(--space-4)) var(--space-4);
    padding: 0 var(--space-2);
    background: var(--paper);
  }
  html[data-screen="play"] .side-pane .drawer-handle::before { background: var(--border-strong); }
  html[data-screen="play"] .drawer-back {
    justify-content: center;
    min-height: 48px;
    margin-bottom: var(--space-3);
    border: 1px solid var(--shell-hair);
    border-radius: var(--radius-1);
    font-family: var(--font-serif);
    font-size: var(--text-lg);
    font-weight: 500;
    letter-spacing: 0.01em;
    text-transform: none;
    color: var(--gold);
  }
  /* The drawer's side inset joins the 4/8 grid (was 18px) so the tab bar's
     bleed and the close bar's bleed land exactly on its edges; its bottom
     padding goes to the close bar itself. A sticky element stops at the
     scroller's PADDING edge, so the old 14px bottom padding floated the bar
     14px up and let list rows show underneath it. */
  .play-shell .side-pane.drawer-open { padding: var(--space-3) var(--space-4) 0; }
  html[data-screen="play"] .drawer-close-bar {
    margin: var(--space-3) calc(-1 * var(--space-4)) 0;
    padding: var(--space-2) var(--space-4) calc(var(--space-2) + env(safe-area-inset-bottom));
    border-top-color: var(--border-soft);
    box-shadow: 0 -12px 24px -16px black;
  }
  html[data-screen="play"] .drawer-close-bar button {
    min-width: 160px;
    border-color: var(--border-strong);
    border-radius: var(--radius-1);
    font-family: var(--font-serif);
    font-size: var(--text-md);
    font-weight: 500;
    letter-spacing: 0.02em;
    text-transform: none;
  }
}

/* ============================================================================
   AAA SHELL LAYER — ROUND 2 (critic-driven). Overrides the round-1 rules above
   on source order; same selectors, same scoping, same prohibitions (A2/A3/A4/
   A6). Themes, in the brief's order:
     A material + light   B colour roles   C desktop layout   D header seam
     E type               F controls       G one state at a time   H ledger shell
   ============================================================================ */

/* ---- C. GEOMETRY. Rail 400px from 1280 (five tabs never wrap), 420px from
   1600; the page sheet is ~760px wide with gutters clamp(48px, 8vw, 160px).
   Both page widths are raw token lists: `100%` resolves where they are USED
   (the main column for the page, the dock for the sheet). */
html[data-screen="play"] .play-shell {
  --shell-page-max: 760px;
  --shell-gutter: clamp(48px, 8vw, 160px);
}
@media (min-width: 1280px) { html[data-screen="play"] .play-shell { --shell-rail-w: 400px; } }
@media (min-width: 1600px) { html[data-screen="play"] .play-shell { --shell-rail-w: 420px; --shell-page-max: 820px; } }

/* ---- A + C. THE PAGE. One warm vellum sheet under the reading column — the
   book lying on a dark desk (the desk is the ground + ambient layer). It does
   not scroll: the text scrolls on it. Grain, a candle pool at its head, an
   inner edge highlight and a deep soft shadow onto the desk. Not a card around
   prose — the page itself. */
html[data-screen="play"] .shell-main { position: relative; }
html[data-screen="play"] .shell-main::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: min(calc(100% - 2 * var(--shell-gutter)), var(--shell-page-max));
  transform: translateX(-50%);
  z-index: 0;
  pointer-events: none;
  background:
    var(--tex-grain),
    var(--candle),
    linear-gradient(180deg, var(--page), var(--page-deep));
  box-shadow: var(--shadow-page), inset 1px 0 0 rgba(255, 255, 255, 0.04), inset -1px 0 0 rgba(255, 255, 255, 0.04);
}
html[data-screen="play"] .play-shell .feed {
  position: relative;
  z-index: 1;
  background: none;
  /* D. THE HEADER SEAM. A 56px fade at the head (a line is either fading out
     or fully present — never sliced by the hairline) and a 48px fade at the
     foot (never cut hard against the dock). Replaces .scroll-fade's 24px. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 56px, black calc(100% - 48px), transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, black 56px, black calc(100% - 48px), transparent 100%);
  scroll-padding-top: 56px;
  scroll-padding-bottom: 48px;
  padding-top: 64px;
}
/* The desk around the page: a warm vignette on the ambient layer (a canvas
   mounted there by THREE paints over it). */
html[data-screen="play"] .shell-ambient::before {
  background: var(--vignette-warm);
}

/* ---- C + H. THE RAIL IS ONE FULL-HEIGHT COLUMN. The DOM keeps the rail in
   the body row (contract A2), so its surface is continued through the header
   (::after) and the dock (::after) with the SAME solid tier, edge and shadow.
   No bottom fade: a solid column, not a panel dissolving into a footer. */
@media (min-width: 769px) {
  html[data-screen="play"] .side-pane {
    padding: 0 var(--space-5) var(--space-7);
    border-left: 1px solid var(--shell-hair-soft);
    background: var(--tex-grain), var(--paper-2);
    box-shadow: var(--shadow-rail);
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
    -webkit-mask-image: none;
            mask-image: none;
  }
}

/* ---- C. THE HEADER — running head at the left of the reading plate, the
   meters at its right, the rail's own segment (title + the one door) over the
   rail. The main plate's bottom edge is a gold-leaf hairline; the rail segment
   is the rail's surface. */
html[data-screen="play"] .shell-hdr {
  grid-template-columns: minmax(0, 1fr) auto var(--shell-rail-w);
  column-gap: var(--space-5);
  min-height: 68px;
  padding: var(--space-2) 0 var(--space-2) var(--space-5);
  border-bottom: 0;
  background: var(--tex-grain), color-mix(in srgb, var(--paper) 82%, transparent);
  box-shadow: none;
}
html[data-screen="play"] .shell-hdr > * { position: relative; z-index: 1; }
html[data-screen="play"] .shell-hdr::before {
  content: '';
  position: absolute;
  left: 0;
  right: var(--shell-rail-w);
  bottom: 0;
  height: 1px;
  background: var(--leaf);
  opacity: 0.7;
}
html[data-screen="play"] .shell-hdr::after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: var(--shell-rail-w);
  border-left: 1px solid var(--shell-hair-soft);
  background: var(--tex-grain), var(--paper-2);
  box-shadow: var(--shadow-rail);
}
html[data-screen="play"] .shell-runhead {
  position: relative;
  top: auto;
  left: auto;
  transform: none;
  grid-column: 1;
  justify-self: start;
  letter-spacing: 0.14em;
  font-variant-caps: normal;
  text-transform: uppercase;
  font-size: 14px;
  color: var(--ink-2);
}
html[data-screen="play"] .shell-meters { grid-column: 2; justify-self: end; gap: var(--space-5); }
html[data-screen="play"] .shell-hdr-ledger {
  grid-column: 3;
  grid-row: 1;
  justify-self: start;
  margin-left: var(--space-5);
  font-family: var(--font-serif);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-2);
}
html[data-screen="play"] .shell-menu-btn {
  grid-column: 3;
  grid-row: 1;
  justify-self: end;
  margin-right: var(--space-5);
  width: 46px;
  height: 46px;
  border-color: color-mix(in srgb, var(--gold) 38%, transparent);
  color: var(--ink);
}
@media (max-width: 1759px) { html[data-screen="play"] .shell-runhead { display: flex; } }
@media (max-width: 1279px) {
  html[data-screen="play"] .shell-runhead { display: none; }
  html[data-screen="play"] .shell-hdr { grid-template-columns: minmax(0, 1fr) var(--shell-rail-w); }
  html[data-screen="play"] .shell-meters { grid-column: 1; justify-self: start; }
  html[data-screen="play"] .shell-hdr-ledger,
  html[data-screen="play"] .shell-menu-btn { grid-column: 2; }
}

/* ---- B. METERS — one brass fill, identity in the icon, red only when low;
   labels at the 16/17px floor, values larger. */
html[data-screen="play"] .shell-meter .m-icon { color: var(--ink-2); }
/* icon colours are written as mixes of GATED text tokens (a colour alias here
   would enrol in the contrast suite's alias set; --paper-2 must never be text) */
html[data-screen="play"] .shell-meter.m-hp .m-icon { color: color-mix(in srgb, var(--danger) 72%, var(--ink-faded)); }
html[data-screen="play"] .shell-meter.m-mana .m-icon,
html[data-screen="play"] .shell-meter.m-stam .m-icon { color: color-mix(in srgb, var(--gold) 82%, var(--ink-faded)); }
html[data-screen="play"] .shell-meter .m-bar { height: 5px; }
html[data-screen="play"] .shell-meter.is-low .m-fill { background: var(--danger); }
html[data-screen="play"] .shell-meter.is-low .m-icon { color: var(--danger); }

/* ---- A. THE DOCK — the main plate (grain, gold-leaf top edge) and the rail's
   continuation, both drawn by pseudo-elements behind the handle. */
@media (min-width: 769px) {
  html[data-screen="play"] .shell-dock::before {
    border-top: 0;
    background:
      var(--leaf) top / 100% 1px no-repeat,
      var(--tex-grain),
      color-mix(in srgb, var(--paper) 86%, transparent);
  }
  html[data-screen="play"] .shell-dock::after {
    border-left: 1px solid var(--shell-hair-soft);
    background: var(--tex-grain), var(--paper-2);
    box-shadow: var(--shadow-rail);
  }
}

/* ---- E. PROSE TYPE — a touch larger on desktop, the measure unchanged
   (~70 characters); chapter heads brighter than the body they head; the drop
   cap locked to the baseline with initial-letter (float fallback kept). */
html {
  /* tokens:shell (r2 retune) */
  --text-prose: clamp(20px, 0.35vw + 18px, 24px);
}
html[data-screen="play"] .feed .scene-title-block { margin: var(--space-7) auto var(--space-2); }
html[data-screen="play"] .feed .scene-title-text {
  font-size: 16px;
  font-weight: 600;
  font-variant-caps: normal;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--ink);
  margin-bottom: var(--space-3);
}
html[data-screen="play"] .feed .scene-title-block .ornament::before,
html[data-screen="play"] .feed .scene-title-block .ornament::after {
  height: 2px;
  max-width: 116px;
  background: color-mix(in srgb, var(--gold) 40%, transparent);
}
html[data-screen="play"] .ornament-lozenge { width: 9px; height: 9px; opacity: 1; }
@supports (initial-letter: 2) or (-webkit-initial-letter: 2) {
  html[data-screen="play"] .feed .narration.dropcap > p:first-of-type::first-letter {
    float: none;
    font-size: inherit;
    line-height: inherit;
    padding: 0;
    margin: 0 0.14em 0 0;
    -webkit-initial-letter: 2;
            initial-letter: 2;
  }
}
/* The player's line: "YOU" set inline on the italic's baseline, 2px gold rule. */
html[data-screen="play"] .feed .player-action {
  display: block;
  padding: 2px 0 2px var(--space-3);
  border-left: 2px solid color-mix(in srgb, var(--gold) 55%, transparent);
}
html[data-screen="play"] .feed .player-action-tag {
  display: inline;
  margin-right: var(--space-3);
  font-size: 14px;
  font-variant-caps: normal;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  line-height: 1;
}
/* The streaming caret: a thin gold bar that blinks once a second, not a dot. */
html[data-screen="play"] .narration-cursor {
  width: 2px;
  height: 1.05em;
  margin-left: 0.18em;
  vertical-align: text-bottom;
  border-radius: 1px;
  box-shadow: none;
  animation: shellCaretBlink 1s steps(1, end) infinite;
}
@keyframes shellCaretBlink { 50% { opacity: 0; } }

/* ---- G. ONE STATE AT A TIME. The feed shows content only: the "click to
   skip" pill is gone as a visible element (it could only settle animations,
   never reveal unwritten text); tap / click / Esc still skip. The status line
   lives in the dock handle (play.jsx). */
html[data-screen="play"] .feed.revealing::after { display: none; }
html[data-screen="play"] .actions-handle { min-height: 60px; }
html[data-screen="play"] .actions-handle:disabled { opacity: 1; cursor: default; }
html[data-screen="play"] .actions-handle-label {
  font-size: 22px;
  font-style: italic;
  font-weight: 500;
  color: var(--ink);
}
html[data-screen="play"] .actions-handle-count {
  font-size: 16px;
  font-style: normal;
  color: var(--ink-2);
}
html[data-screen="play"] .actions-handle-count::before { background: var(--gold); opacity: 0.7; }
html[data-screen="play"] .actions-handle .quill-line {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-style: italic;
  color: var(--ink-2);
}
html[data-screen="play"] .actions-handle .quill-line::before {
  content: '';
  width: 20px;
  height: 20px;
  background-color: var(--gold);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 20l6-6M14 6c4 0 6 2 6 6-2 0-4 0-6 2s-2 4-4 4c-4 0-4-4-2-6s6-6 6-6z'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 20l6-6M14 6c4 0 6 2 6 6-2 0-4 0-6 2s-2 4-4 4c-4 0-4-4-2-6s6-6 6-6z'/%3E%3C/svg%3E");
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}
html[data-screen="play"] .actions-handle .quill-line::after {
  content: '\2026';
  margin-left: calc(2px - var(--space-3));
  animation: quillDots 1.4s ease-in-out infinite;
}

/* ---- A + C. THE CHOICE SHEET — a slip of the same vellum, exactly the page's
   width, rising over it (desktop: never over the ledger). Grain, gold-leaf top
   edge, inner highlight, a soft upward shadow. Centred with left/width, NOT a
   transform (the reduced-motion arm zeroes the panel's transform). */
@media (min-width: 769px) {
  html[data-screen="play"] .actions-panel {
    width: min(calc(100% - var(--shell-rail-w) - 2 * var(--shell-gutter)), var(--shell-page-max));
    left: calc((100% - var(--shell-rail-w) - min(calc(100% - var(--shell-rail-w) - 2 * var(--shell-gutter)), var(--shell-page-max))) / 2);
    right: auto;
    padding: var(--space-5) var(--space-6) 0;
  }
}
html[data-screen="play"] .actions-panel {
  border-top: 0;
  background:
    var(--leaf) top / 100% 1px no-repeat,
    var(--tex-grain),
    var(--candle),
    var(--page);
  box-shadow: var(--shadow-sheet), var(--edge-light);
}
html[data-screen="play"] .actions-panel .play-composer { background: var(--tex-grain), var(--page); }
/* The sheet's grid at every width: prompt + Inspiration share the head row. */
html[data-screen="play"] .actions-panel-inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "head insp" "list list" "rest rest" "comp comp";
  column-gap: var(--space-4);
  align-items: center;
  max-width: none;
}
html[data-screen="play"] .actions-panel .suggested-actions-header { grid-area: head; margin: 0 0 var(--space-3); }
html[data-screen="play"] .actions-panel .suggested-actions.choice-list { grid-area: list; }
html[data-screen="play"] .actions-panel .inspire-bar { grid-area: rest; }
html[data-screen="play"] .actions-panel .inspire-bar:has(.btn-inspire) { grid-area: insp; align-self: start; margin: 0 0 var(--space-3); }
html[data-screen="play"] .actions-panel .play-composer { grid-area: comp; }
/* F. Inspiration: a visible gold token, 46px. */
html[data-screen="play"] .actions-panel .btn-inspire {
  min-height: 46px;
  border-color: color-mix(in srgb, var(--gold) 38%, transparent);
  background: color-mix(in srgb, var(--gold) 7%, transparent);
  color: var(--ink);
}
html[data-screen="play"] .actions-panel .btn-inspire-label-full { display: none; }
html[data-screen="play"] .actions-panel .btn-inspire-label-short { display: inline; }
/* I. Risk in words + a colour ramp: No roll (green) · Easy (cool ink) ·
   Moderate (brass-amber) · Hard (ember) · Very hard (danger). */
html[data-screen="play"] .actions-panel .suggested-action-row.meta { font-size: var(--text-md); }
html[data-screen="play"] .actions-panel .trigger-badge,
html[data-screen="play"] .actions-panel .xp-preview-badge { font-size: var(--text-md); }
html[data-screen="play"] .actions-panel .risk-badge { font-variant-caps: normal; letter-spacing: 0.01em; }
html[data-screen="play"] .risk-badge[data-tier="safe"]      { color: var(--verdict-success); }
html[data-screen="play"] .risk-badge[data-tier="easy"]      { color: var(--ink-2); }
html[data-screen="play"] .risk-badge[data-tier="moderate"]  { color: color-mix(in srgb, var(--gold) 72%, var(--danger)); }
html[data-screen="play"] .risk-badge[data-tier="hard"]      { color: color-mix(in srgb, var(--danger) 62%, var(--gold)); }
html[data-screen="play"] .risk-badge[data-tier="very-hard"] { color: var(--danger); }
html[data-screen="play"] .actions-panel .risk-dc {
  margin-left: 6px;
  font-weight: 500;
  font-variant-numeric: lining-nums;
  opacity: 0.85;
}
/* F. The composer: a ruled writing line + a 46px gold-filled Send beside it
   (no box inside a box). The key hints sit under it on desktop. */
html[data-screen="play"] .composer-field {
  align-items: flex-end;
  gap: var(--space-3);
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}
html[data-screen="play"] .composer-field:focus-within { border: 0; box-shadow: none; }
html[data-screen="play"] .shell-dock .play-composer .composer-input {
  padding: 10px 2px;
  border-bottom: 1px solid var(--border-strong);
  border-radius: 0;
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
html[data-screen="play"] .shell-dock .play-composer .composer-input:focus {
  border-bottom-color: var(--gold);
  box-shadow: 0 1px 0 var(--gold);
}
html[data-screen="play"] .shell-dock .play-composer .composer-send,
html[data-screen="play"] .shell-dock .play-composer .composer-send:hover {
  height: 46px;
  min-height: 46px;
  min-width: 46px;
  background: var(--gold);
  border-color: var(--gold);
  color: var(--paper);
}
html[data-screen="play"] .shell-dock .play-composer .composer-send:hover { box-shadow: var(--glow-gilt), var(--elev-2); }
html[data-screen="play"] .shell-dock .play-composer .composer-send:disabled {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--paper);
  opacity: 0.5;
}
html[data-screen="play"] .composer-foot .composer-key-hint { opacity: 1; font-size: 14px; }

/* ---- H. THE TAB BAR — sentence case at 17px (small caps read as 11px), one
   row always, the open tab in gold with a 2px gold rule, the rest at ~62%. */
html[data-screen="play"] .side-pane .tab-row { background: var(--tex-grain), var(--paper-2); }
html[data-screen="play"] .side-pane .tab-row .tab {
  min-height: 48px;
  white-space: nowrap;
  font-size: var(--text-md);
  font-variant-caps: normal;
  letter-spacing: 0.01em;
  color: color-mix(in srgb, var(--ink) 62%, transparent);
}
html[data-screen="play"] .side-pane .tab-row .tab.active,
html[data-screen="play"] .side-pane .tab-row .tab[aria-selected="true"] {
  color: var(--gold);
  font-weight: 600;
  border-bottom-color: var(--gold);
}
/* (integration) Parchment's gold is a dark-ground metal: as 17px text on the
   light rail it reads 2.99:1. There the open tab is ink over the gold rule
   (DESIGN §4 Tabs); the dark palettes keep the gold word. */
html[data-palette="parchment"][data-screen="play"] .side-pane .tab-row .tab.active,
html[data-palette="parchment"][data-screen="play"] .side-pane .tab-row .tab[aria-selected="true"] {
  color: var(--ink);
}

/* ---- G + B. The turn summary lists resource changes; a health loss in the
   danger role. */
html[data-screen="play"] .turn-summary-card .ts-now { color: var(--ink-2); }
html[data-screen="play"] .turn-summary-card .ts-loss dd { color: var(--danger); }
html[data-screen="play"] .feed .turn-summary-card {
  background: var(--tex-grain), var(--page-deep);
  box-shadow: var(--elev-1), var(--edge-light);
}

/* ---- Reduced motion for round 2. */
@media (prefers-reduced-motion: reduce) {
  html[data-screen="play"] .narration-cursor { animation: none; }
  html[data-screen="play"] .actions-handle .quill-line::after { animation: none; }
}

/* ---- PHONE (<= 768px): the page is the whole column; one exit in the drawer. */
@media (max-width: 768px) {
  html[data-screen="play"] .shell-hdr {
    grid-template-columns: minmax(0, 1fr) auto;
    padding: calc(var(--space-2) + env(safe-area-inset-top)) var(--space-4) var(--space-2);
    min-height: 62px;
  }
  html[data-screen="play"] .shell-hdr::before { right: 0; }
  html[data-screen="play"] .shell-hdr::after,
  html[data-screen="play"] .shell-hdr-ledger { display: none; }
  html[data-screen="play"] .shell-meters { grid-column: 1; justify-self: stretch; gap: var(--space-4); }
  html[data-screen="play"] .shell-menu-btn { grid-column: 2; margin-right: 0; }
  html[data-screen="play"] .shell-main::before { left: 0; width: 100%; transform: none; box-shadow: none; }
  html[data-screen="play"] .play-shell .feed { padding-top: 56px; }
  html[data-screen="play"] .shell-dock {
    background:
      var(--leaf) top / 100% 1px no-repeat,
      var(--tex-grain),
      color-mix(in srgb, var(--paper) 90%, transparent);
    border-top: 0;
  }
  html[data-screen="play"] .actions-handle-label { font-size: 21px; }
  html[data-screen="play"] .actions-panel { padding: var(--space-4) var(--space-4) 0; }
  html[data-screen="play"] .choice-prompt { font-size: 22px; }
  html[data-screen="play"] .shell-dock .play-composer .composer-send { width: 46px; }
  /* one meta line per choice on a phone: 15px, and the XP preview (the
     noisiest item) stays on desktop only — it is still in the aria-label */
  html[data-screen="play"] .actions-panel .suggested-action-row.meta,
  html[data-screen="play"] .actions-panel .trigger-badge,
  html[data-screen="play"] .actions-panel .risk-badge { font-size: var(--text-sm); }
  html[data-screen="play"] .actions-panel .xp-preview-badge { display: none; }

  /* H. The drawer: tabs at the top (no grabber, no top exit), ONE gold exit
     in the thumb zone with a 48px fade above it. */
  .play-shell .side-pane.drawer-open {
    padding-top: env(safe-area-inset-top);
    background: var(--tex-grain), var(--paper);
  }
  html[data-screen="play"] .side-pane .tab-row { background: var(--tex-grain), var(--paper); }
  html[data-screen="play"] .drawer-close-bar {
    padding: var(--space-3) var(--space-4) calc(var(--space-3) + env(safe-area-inset-bottom));
    border-top: 0;
    background: var(--tex-grain), var(--paper);
    box-shadow: none;
  }
  html[data-screen="play"] .drawer-close-bar::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: 48px;
    pointer-events: none;
    background: linear-gradient(to bottom, transparent, var(--paper));
  }
  html[data-screen="play"] .drawer-close-bar button,
  html[data-screen="play"] .drawer-close-bar button:hover {
    width: 100%;
    min-height: 48px;
    justify-content: center;
    gap: var(--space-2);
    border: 1px solid var(--gold);
    background: var(--cta-fill);
    color: var(--gold);
    font-size: var(--text-lg);
    font-weight: 600;
  }
  html[data-screen="play"] .drawer-close-bar button svg { transform: rotate(180deg); }
}

/* ============================================================
   v1.9 — THE FRONT DOOR
   Every screen before play: the key screen, the menu (title page, contents,
   world picker, delete dialog content, burning row), the settings sheet, the
   creation interview (seed → asking → building → summary, both books) and the
   preset character choice. Markup: frontdoor.jsx, auth.jsx, menu.jsx,
   interview.jsx, app.jsx (PresetCharacterChoice, TweaksPanel).

   Direction — a serious book that plays: the menu is a title page and a table
   of contents; the interview is a quiet chronicler asking one question at a
   time; the summaries are title pages and a dramatis personae.

   BUILT TO .planning/aaa/design/DESIGN.md and ON SHELL's DESIGN SCALE tokens
   (--text-*, --space-*, --radius-*, --dur-*, --ease-*, --font-*, --tap):
   · Two families. EB Garamond (--font-serif) for prose AND UI; Cinzel
     (--font-display) only for the wordmark, a title page's one title, the
     brand and drop caps. Labels are small caps at >= 17px, never uppercase
     walls.
   · Gold means "you / press this / chosen / focus"; decoration is limited to
     the gilt hairline system and the SVG rules at section breaks.
   · Radii --radius-1 (controls) / --radius-2 (surfaces). No viewport frame.
   · MATERIAL (ground, grain, vignette, light) is SHELL's, at the token source:
     these screens are transparent over it and never paint their own ground,
     except the solid --paper plates DESIGN specifies (dock, sheet, bars).
   · No new custom properties; colour only through palette tokens and
     color-mix() of them.
   · Screen roots never transform, filter, mask or backdrop-filter: they hold
     position:fixed children (ambient slot, dialog, the phone CTA). Entrance
     motion lives on leaf blocks and runs on --dur-* (which collapse under
     prefers-reduced-motion); a reduced-motion arm at the end removes it.
   ============================================================ */

/* ---------------------------------------------------------------- *
 * 1. Screens and the ambient slot
 * ---------------------------------------------------------------- */

/* Every front-door screen is a fixed, full-viewport scroller — the play
   shell's own contract (only the page scrolls, never the document), so the
   ground stays put behind a scrolling page instead of ending in a seam one
   viewport down. */
.fd-screen {
  position: fixed;
  inset: 0;
  overflow-x: hidden;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}

.fd-folio {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--space-8) var(--space-7) var(--space-7);
  animation: fdFade var(--dur-reveal) var(--ease-out) backwards;
}

/* The column centres itself vertically while it fits and falls back to the
   top once it does not (margin:auto collapses to 0 on overflow). */
.fd-folio-col {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 760px;
  margin: auto;
}

/* THE AMBIENT SLOT — reserved for the WebGL ambient layer, which mounts its
   canvas inside it. Full-bleed, behind content, never interactive, empty. */
.fd-ambient {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* Settings, on the front door, is a word in the page's own voice — the same
   gutter as the running head's rule, so wordmark, rule and control register.
   (The play screen hides this control and owns its corner.) */
.tweaks-fab .tweaks-fab-label { display: none; }
html:not([data-screen="play"]) .tweaks-fab {
  top: 20px;
  right: var(--space-7);
  width: auto;
  height: var(--tap);
  min-width: var(--tap);
  padding: 0 var(--space-4);
  gap: var(--space-2);
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-1);
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
}
html:not([data-screen="play"]) .tweaks-fab svg { display: none; }
html:not([data-screen="play"]) .tweaks-fab .tweaks-fab-label { display: inline; }
html:not([data-screen="play"]) .tweaks-fab:hover { color: var(--ink); background: var(--hover-wash); border-color: var(--border-strong); }
html:not([data-screen="play"]) .tweaks-fab:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
/* The creation screens are a full-screen layer on a phone (z-index 100): the
   control and its sheet stay above them, or they would be visible but dead. */
html[data-screen="world"] .tweaks-fab,
html[data-screen="character"] .tweaks-fab { z-index: 120; }
html[data-screen="world"] .tweaks-panel,
html[data-screen="character"] .tweaks-panel { z-index: 130; }
html[data-screen="world"] .tw-scrim,
html[data-screen="character"] .tw-scrim { z-index: 125; }

/* ---------------------------------------------------------------- *
 * 2. The title page head (key screen + menu)
 * ---------------------------------------------------------------- */

.fd-title {
  position: relative;
  text-align: center;
  /* the wordmark is wider than the reading column; the head is not bound by it */
  margin: 0 -160px;
}

/* The candle: a warm pool of light behind the wordmark (the same light the
   play shell sets over its reading column). */
.fd-title::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 46%;
  width: 1100px;
  height: 480px;
  transform: translate(-50%, -50%);
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--gold) 10%, transparent),
    color-mix(in srgb, var(--gold) 4%, transparent) 55%,
    transparent);
  pointer-events: none;
  z-index: -1;
}

/* The wordmark: full capitals, set heavy and cut clean — a hard 1px shadow
   and a hairline of gilt, never a blur. */
.fd-wordmark {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 108px;
  line-height: 1;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* trailing tracking would pull the optical centre left */
  margin: var(--space-5) 0 var(--space-5) 0.06em;
  color: color-mix(in srgb, var(--ink) 92%, var(--gold));
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.65), 0 0 1px color-mix(in srgb, var(--gold) 35%, transparent);
  animation: fdInk 1100ms var(--ease-out) backwards;
}

.fd-subtitle {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 24px;
  line-height: 1.4;
  color: var(--ink-2);
  max-width: 30em;
  margin: var(--space-5) auto 0;
  text-wrap: balance;
  animation: fdFade var(--dur-reveal) 200ms var(--ease-out) backwards;
}

/* The printer's rule — gilt hairlines fading outward from a lozenge. */
.fd-rule {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  color: color-mix(in srgb, var(--gold) 75%, transparent);
}
.fd-rule::before,
.fd-rule::after {
  content: '';
  height: 1px;
  width: 128px;
}
.fd-rule::before { background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--gold) 35%, transparent)); }
.fd-rule::after  { background: linear-gradient(270deg, transparent, color-mix(in srgb, var(--gold) 35%, transparent)); }
.fd-rule svg { display: block; flex: none; }

/* The engraved rule-set of the title page: a thick-and-thin pair each side,
   320px across. */
.fd-rule--engraved::before,
.fd-rule--engraved::after {
  width: 150px;
  height: 4px;
  background: none;
  border-top: 1px solid color-mix(in srgb, var(--gold) 55%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 22%, transparent);
}
.fd-rule--engraved svg { width: 16px; height: 16px; }

/* ---------------------------------------------------------------- *
 * 3. Type primitives
 * ---------------------------------------------------------------- */

/* Labels: serif small caps (DESIGN §3), large enough that the small capitals
   read at 14px. */
.fd-section-label,
.fd-label,
.iv-eyebrow,
.iv-sec-title,
.iv-note-label,
.iv-recap-label,
.iv-trait-label,
.iv-context-label,
.iv-or,
.fd-world-kicker,
.iv-path-kicker,
.iv-fact dt,
.iv-stat-name,
.tw-label {
  font-family: var(--font-serif);
  font-size: 20px;
  font-weight: 600;
  font-variant-caps: all-small-caps;
  letter-spacing: var(--track-caps);
  line-height: 1.3;
  text-transform: none;
  color: var(--ink-faded);
}

/* Section labels carry a soft hairline out to the edge of their column. */
.fd-section-label,
.iv-sec-title,
.iv-or {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin: 0 0 var(--space-3);
}
.fd-section-label::after,
.iv-sec-title::after,
.iv-or::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--border-soft), transparent);
}

.fd-label { display: block; margin: 0 0 var(--space-2); }

.fd-mono {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 0.84em;
  font-style: normal;
  letter-spacing: 0;
  color: var(--ink);
}

.key-suffix {
  font-family: inherit;
  font-style: normal;
  font-weight: 600;
  font-variant-caps: all-small-caps;
  letter-spacing: var(--track-caps);
}

.fd-visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* No paragraph on these screens ends on a lonely word. */
.fd-subtitle, .fd-plate-text, .fd-fine, .fd-burn-text, .fd-colophon p,
.fd-world-blurb, .iv-lede, .iv-hint, .iv-note-idea,
.iv .iv-option .desc, .iv-entry-desc, .iv-fact dd, .iv-path-blurb,
.iv-recap li, .iv-trail-a, .iv-build-note, .iv-context-line { text-wrap: pretty; }

/* ---------------------------------------------------------------- *
 * 4. Buttons, text actions, icon buttons, links (DESIGN §4 Button)
 * ---------------------------------------------------------------- */

.fd-btn {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: 0 var(--space-5);
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 500;
  letter-spacing: 0;
  line-height: var(--leading-tight);
  text-transform: none;
  text-decoration: none;
  white-space: nowrap;
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-1);
  box-shadow: none;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out), box-shadow var(--dur-base) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.fd-btn svg { flex: none; }
.fd-btn:hover { border-color: var(--border-strong); background: var(--hover-wash); color: var(--ink); }
.fd-btn:active { transform: translateY(1px); }
.fd-btn:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* Primary: the CTA triad — gold, and only gold means "press this". */
.fd-btn--primary {
  color: var(--cta-ink);
  background: var(--cta-fill);
  border-color: var(--cta-edge);
}
.fd-btn--primary:hover {
  color: var(--cta-ink);
  background: var(--cta-fill);
  border-color: var(--cta-edge);
  box-shadow: var(--glow-gilt);
}
.fd-btn--primary:active { box-shadow: none; }

.fd-btn--lg {
  min-height: 52px;
  padding: 0 var(--space-6);
  font-size: var(--text-lg);
}

.fd-btn--ghost {
  border-color: transparent;
  color: var(--ink-2);
  padding: 0 var(--space-3);
}
.fd-btn--ghost:hover { border-color: transparent; background: var(--hover-wash); color: var(--ink); }

/* A text action: a word, underlined on hover — "Export", "Delete". */
.fd-btn--text {
  min-width: var(--tap);
  padding: 0 var(--space-2);
  border-color: transparent;
  color: var(--ink-2);
  font-size: 16px;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 4px;
}
.fd-btn--text:hover {
  background: transparent;
  border-color: transparent;
  color: var(--ink);
  text-decoration-color: color-mix(in srgb, var(--gold) 55%, transparent);
}
.fd-btn--text-danger:hover { color: var(--danger); text-decoration-color: var(--danger); }

.fd-btn--danger {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 55%, transparent);
}
.fd-btn--danger:hover {
  color: var(--danger);
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}

/* Disabled: a neutral outline, uniformly faded — gold is kept for "ready". */
.fd-btn:disabled,
.fd-btn:disabled:hover {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}
.fd-btn--primary:disabled,
.fd-btn--primary:disabled:hover {
  color: var(--ink-2);
  background: transparent;
  border-color: var(--border-strong);
}

.fd-icon-btn {
  appearance: none;
  -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--tap);
  height: var(--tap);
  padding: 0;
  margin: 0;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-1);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.fd-icon-btn:hover { color: var(--ink); background: var(--hover-wash); border-color: var(--border-soft); }
.fd-icon-btn:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

.fd-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: var(--tap);
  padding: 0 var(--space-1);
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-style: normal;
  color: var(--ink-2);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: color-mix(in srgb, var(--gold) 45%, transparent);
  text-underline-offset: 4px;
  transition: color var(--dur-fast) var(--ease-out), text-decoration-color var(--dur-fast) var(--ease-out);
}
.fd-link:hover { color: var(--ink); text-decoration-color: var(--gold); }
.fd-link:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* ---------------------------------------------------------------- *
 * 5. Fields (DESIGN §4 Input / textarea)
 * ---------------------------------------------------------------- */

/* A field's edge must be findable: ink-faded at 55% (~3:1), not a hairline. */
.fd-input {
  display: block;
  width: 100%;
  height: 52px;
  padding: 0 var(--space-4);
  margin: 0;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: var(--text-md);
  letter-spacing: 0.02em;
  color: var(--ink);
  caret-color: var(--gold);
  background: var(--tint-sunken);
  border: 1px solid color-mix(in srgb, var(--ink-faded) 55%, transparent);
  border-radius: var(--radius-1);
  outline: none;
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
/* The hint reads as a hint — italic serif, quieter than anything typed. */
.fd-input::placeholder {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: var(--text-lg);
  letter-spacing: 0;
  color: color-mix(in srgb, var(--ink-faded) 80%, transparent);
  opacity: 1;
}
.fd-input:focus {
  border-color: var(--gold);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 18%, transparent);
}
.fd-input:disabled { opacity: 0.7; }

/* The writing surface: text on faint ruled lines, with a margin band down the
   left like a copybook page. Layer 1 — the lines — is laid in the CONTENT
   box on the 32px line pitch, and the box is sized so the bottom border IS
   the last rule (no orphan line). Layer 2 (bottom-most, so the sunken fill
   follows its clip) is the margin band and its gilt margin rule. The field
   grows as it is written in (interview.jsx). */
.fd-writing {
  display: block;
  width: 100%;
  min-height: calc(4 * 32px - 1px + 12px + 2px);
  margin: 0;
  padding: 12px var(--space-4) 0 52px;
  resize: none;
  overflow-y: auto;
  font-family: var(--font-serif);
  font-size: 20px;
  line-height: 32px;
  color: var(--ink);
  caret-color: var(--gold);
  background-color: var(--tint-sunken);
  background-image:
    repeating-linear-gradient(180deg, transparent 0 31px,
                    color-mix(in srgb, var(--ink) 9%, transparent) 31px 32px),
    linear-gradient(90deg, color-mix(in srgb, var(--ink) 3%, transparent) 0 36px,
                    color-mix(in srgb, var(--gold) 30%, transparent) 36px 37px, transparent 37px);
  background-attachment: local, scroll;
  background-origin: content-box, padding-box;
  background-clip: content-box, padding-box;
  background-position: 0 0, 0 0;
  background-repeat: repeat, no-repeat;
  border: 1px solid color-mix(in srgb, var(--ink-faded) 55%, transparent);
  border-radius: var(--radius-1);
  outline: none;
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.fd-writing::placeholder { color: color-mix(in srgb, var(--ink-faded) 85%, transparent); font-style: italic; opacity: 1; }
.fd-writing:focus {
  border-color: var(--gold);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 18%, transparent);
}
.fd-writing:disabled { opacity: 0.7; }
.fd-writing--compose { min-height: calc(2 * 32px - 1px + 12px + 2px); }

/* The revision box: surface above, its button beneath (never inside it). */
.fd-composer { display: block; }
.fd-composer .fd-btn { margin-top: var(--space-3); }

/* Inline alert — banner anatomy: 3px edge in its role colour. */
.fd-alert {
  margin: var(--space-4) 0 0;
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-md);
  line-height: 1.45;
  color: var(--ink-2);
  background: color-mix(in srgb, var(--danger) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent);
  border-left: 3px solid var(--danger);
  border-radius: var(--radius-2);
}
.fd-alert strong { color: var(--danger); font-weight: 600; }
.fd-alert--row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-top: var(--space-5); text-align: left; }

/* ---------------------------------------------------------------- *
 * 6. The key screen
 * ---------------------------------------------------------------- */

.auth-wrap {
  display: flex;
  justify-content: center;
  margin-top: var(--space-7);
}

/* A plate — card anatomy (surface + backdrop, hairline, 8px radius). */
.fd-plate {
  position: relative;
  width: 100%;
  max-width: 560px;
  padding: var(--space-6) var(--space-6) var(--space-5);
  text-align: left;
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop, none);
          backdrop-filter: var(--panel-backdrop, none);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-2);
  box-shadow: var(--elev-2);
  animation: fdRise var(--dur-reveal) 160ms var(--ease-out) backwards;
}

.fd-plate-title {
  font-family: var(--font-serif);
  font-size: var(--text-title);
  font-weight: 500;
  letter-spacing: 0;
  line-height: var(--leading-tight);
  color: var(--ink);
  margin: 0 0 var(--space-2);
}

.fd-plate-text {
  font-size: var(--text-lg);
  line-height: 1.5;
  color: var(--ink-2);
  margin: 0 0 var(--space-5);
}

.fd-fine {
  font-size: 16px;
  line-height: 1.5;
  font-style: italic;
  color: var(--ink-faded);
  margin: var(--space-2) 0 0;
}
.fd-fine--center { text-align: center; margin-top: var(--space-1); }

/* One decisive action, full width, with the way to get a key beneath it. */
.auth-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-1);
  margin-top: var(--space-5);
}
.auth-actions .fd-link { align-self: center; }

/* ---------------------------------------------------------------- *
 * 7. The menu — contents, actions, picker, colophon, dialog, burning row
 * ---------------------------------------------------------------- */

.fd-contents {
  margin-top: var(--space-7);
  text-align: left;
}
/* One rule at the head of the list, not two: the list's own top edge. */
.fd-contents .fd-section-label::after { display: none; }

.fd-contents-list { border-top: 1px solid color-mix(in srgb, var(--gold) 30%, transparent); }

/* One chronicle = one line of the contents page: hairline separators, no
   boxes. Hover and focus light the row the play screen's way — a 3px gilt
   edge and a faint gold fill; the most recent chronicle keeps a quieter
   version of that light (it is the one Resume continues). The row is the
   target (role=button); its inner buttons stop propagation. */
.save-row.fd-entry {
  position: relative;
  display: grid;
  grid-template-columns: 48px minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--space-4);
  padding: var(--space-4) var(--space-2) var(--space-4) 0;
  margin: 0;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--border-soft);
  border-radius: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  cursor: pointer;
  outline: none;
  transform: none;
  transition: background-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
  animation: fdRise var(--dur-reveal) var(--ease-out) backwards;
}
.save-row.fd-entry:nth-child(1) { animation-delay: 160ms; }
.save-row.fd-entry:nth-child(2) { animation-delay: 220ms; }
.save-row.fd-entry:nth-child(3) { animation-delay: 280ms; }
.save-row.fd-entry:nth-child(4) { animation-delay: 340ms; }
.save-row.fd-entry:nth-child(5) { animation-delay: 400ms; }
.save-row.fd-entry:nth-child(n+6) { animation-delay: 460ms; }
.save-row.fd-entry.is-latest {
  background: linear-gradient(90deg, color-mix(in srgb, var(--gold) 5%, transparent), transparent 70%);
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--gold) 55%, transparent);
}
.save-row.fd-entry:hover,
.save-row.fd-entry:focus-visible {
  background: linear-gradient(90deg, color-mix(in srgb, var(--gold) 7%, transparent), transparent 75%);
  box-shadow: inset 3px 0 0 var(--gold);
  transform: none;
  border-color: var(--border-soft);
}
.save-row.fd-entry:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }

.fd-entry-num {
  align-self: center;
  text-align: center;
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 500;
  font-variant-numeric: lining-nums;
  letter-spacing: 0.04em;
  color: var(--ink-faded);
  transition: color var(--dur-base) var(--ease-out);
}
.save-row.fd-entry.is-latest .fd-entry-num,
.save-row.fd-entry:hover .fd-entry-num,
.save-row.fd-entry:focus-visible .fd-entry-num { color: var(--gold); }

.fd-entry-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.fd-entry-name {
  font-family: var(--font-serif);
  font-size: var(--text-title);
  font-weight: 500;
  line-height: var(--leading-tight);
  color: var(--ink);
}

.fd-entry-meta {
  display: flex;
  flex-wrap: wrap;
  font-size: 16px;
  line-height: 1.45;
  color: var(--ink-faded);
}
/* the separator rides at the END of the fact before it, so a wrapped line
   never begins with a dot */
.fd-entry-meta > span:not(:last-child)::after {
  content: '·';
  display: inline-block;
  margin: 0 0.5em;
}
.fd-entry-cost { font-variant-numeric: lining-nums tabular-nums; }

.fd-entry-actions {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}
.fd-entry-resume { margin-left: var(--space-2); padding: 0 var(--space-4) 0 var(--space-5); }
.save-row.fd-entry:hover .fd-entry-resume.fd-btn--quiet,
.save-row.fd-entry:focus-visible .fd-entry-resume.fd-btn--quiet {
  color: var(--cta-ink);
  background: var(--cta-fill);
  border-color: var(--cta-edge);
}

/* The burning row — the undo notice in the chronicle's own place. It is not
   live: dimmed, struck through, its actions gone, and a draining hairline.
   It carries .soft-delete-toast for the harness, so every property the
   shared fixed slab sets is reset here. */
.soft-delete-toast.fd-burning {
  position: relative;
  left: auto;
  right: auto;
  bottom: auto;
  transform: none;
  z-index: auto;
  width: auto;
  padding: var(--space-4) var(--space-2) var(--space-4) 0;
  font-size: inherit;
  color: inherit;
  background: color-mix(in srgb, var(--danger) 5%, transparent);
  border: 0;
  border-bottom: 1px solid var(--border-soft);
  border-radius: 0;
  box-shadow: inset 3px 0 0 color-mix(in srgb, var(--danger) 70%, transparent);
  cursor: default;
  overflow: hidden;
  animation: fdFade var(--dur-base) var(--ease-out) backwards;
}
.soft-delete-toast.fd-burning:hover { background: color-mix(in srgb, var(--danger) 5%, transparent); box-shadow: inset 3px 0 0 color-mix(in srgb, var(--danger) 70%, transparent); }
.fd-burning .fd-entry-num { opacity: 0.45; }
.fd-burning .fd-entry-name {
  color: var(--ink-faded);
  text-decoration: line-through;
  text-decoration-color: color-mix(in srgb, var(--danger) 70%, transparent);
  text-decoration-thickness: 1px;
}
.fd-burning .fd-burning-note { font-style: italic; color: var(--ink-2); }
/* (integration) the row clips its hairline (overflow: hidden) and Undo sits flush
   right, so its focus ring is drawn inside the button rather than cut off. */
.fd-burning .fd-entry-resume:focus-visible { outline-offset: -4px; }
.fd-burning-bar {
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 2px;
  background: color-mix(in srgb, var(--danger) 75%, transparent);
}

.fd-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-4);
  margin-top: var(--space-7);
  animation: fdFade var(--dur-reveal) 320ms var(--ease-out) backwards;
}
.fd-folio.has-list .fd-actions { margin-top: var(--space-6); }

.fd-picker {
  margin-top: var(--space-7);
  text-align: left;
}
.fd-picker-head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-bottom: var(--space-3);
}
.fd-picker-head .fd-section-label { flex: 1; margin: 0; }
.fd-picker-cancel { margin-right: calc(-1 * var(--space-3)); }

.fd-picker-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: var(--space-4);
}

/* A world is a card (DESIGN §4 Card): kicker, title, blurb, and its action
   as a full-width bordered plate pinned to the card's foot. */
.world-card.fd-world {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-2);
  width: 100%;
  margin: 0;
  padding: var(--space-5);
  text-align: left;
  font: inherit;
  text-transform: none;
  letter-spacing: normal;
  color: var(--ink);
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop, none);
          backdrop-filter: var(--panel-backdrop, none);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-2);
  box-shadow: var(--elev-1);
  cursor: pointer;
  transition: transform var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out), background-color var(--dur-base) var(--ease-out);
  animation: fdRise var(--dur-reveal) var(--ease-out) backwards;
}
.world-card.fd-world:nth-child(2) { animation-delay: 80ms; }
.world-card.fd-world:nth-child(3) { animation-delay: 160ms; }
.world-card.fd-world:hover {
  transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--gold) 55%, transparent);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--gold) 6%, transparent), transparent 60%),
    var(--panel-bg);
  box-shadow: var(--elev-2);
}
.world-card.fd-world:active { transform: translateY(0); }
.world-card.fd-world:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

.world-card.fd-world .fd-world-name {
  font-family: var(--font-serif);
  font-size: var(--text-title);
  font-weight: 500;
  letter-spacing: 0;
  line-height: var(--leading-tight);
  color: var(--ink);
  margin: 0;
}

.world-card.fd-world .fd-world-blurb {
  font-family: var(--font-serif);
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--ink-2);
  white-space: normal;
}

.fd-world-go {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap);
  margin-top: auto;
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--gold);
  border: 1px solid color-mix(in srgb, var(--gold) 55%, transparent);
  border-radius: var(--radius-1);
  transition: background-color var(--dur-base) var(--ease-out);
}
.world-card.fd-world .fd-world-blurb { margin-bottom: var(--space-4); }
.world-card.fd-world:hover .fd-world-go { background: color-mix(in srgb, var(--gold) 10%, transparent); }

.fd-colophon {
  margin-top: var(--space-8);
  text-align: center;
  font-size: 16px;
  font-style: italic;
  line-height: 1.5;
  color: var(--ink-faded);
  animation: fdFade var(--dur-reveal) 440ms var(--ease-out) backwards;
}
.fd-colophon p { margin: 0; }

/* The delete dialog's own content (the dialog chrome is SURFACES' .modal).
   One order everywhere: the destructive action first, the safe one last —
   rightmost on a desk, nearest the thumb on a phone. The extra padding keeps
   the actions clear of the dialog's corner marks. */
.modal:has(> .fd-burn) { max-width: 560px; padding: 40px 48px 48px; }
.modal:has(> .fd-burn) .btn-ghost:focus-visible { outline-color: var(--gold); }
.fd-burn-text {
  font-size: var(--text-lg);
  line-height: 1.5;
  color: var(--ink-2);
  margin: 0 0 var(--space-5);
}
.fd-burn-text strong { color: var(--ink); font-weight: 600; }
.fd-burn-actions {
  display: flex;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* ---------------------------------------------------------------- *
 * 8. The settings sheet (Settings → .tweaks-panel) — DESIGN §4 sheet
 * ---------------------------------------------------------------- */

.tw-scrim {
  position: fixed;
  inset: 0;
  z-index: 89;
  background: color-mix(in srgb, var(--scrim) 70%, transparent);
  -webkit-backdrop-filter: blur(2px);
          backdrop-filter: blur(2px);
  animation: fdFade var(--dur-base) var(--ease-out) backwards;
}

.tweaks-panel {
  top: 76px;
  right: var(--space-7);
  width: 380px;
  max-height: calc(100vh - 96px);
  max-height: calc(100dvh - 96px);
  overflow-y: auto;
  padding: var(--space-3) var(--space-5) var(--space-5);
  /* a sheet is solid (DESIGN §4): the glass tier over an opaque ground, so the
     page under it never shows through its text */
  background: var(--sheet-fill);   /* (integration) the one opaque sheet fill */
  border: 1px solid color-mix(in srgb, var(--gold) 30%, var(--border-soft));
  border-radius: var(--radius-2);
  box-shadow: var(--elev-3);
  animation: fdSheetIn var(--dur-base) var(--ease-out) backwards;
}
html[data-screen="play"] .tweaks-panel { top: 60px; right: 14px; }

.tw-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  font-family: var(--font-serif);
  font-size: var(--text-title);
  font-weight: 500;
  color: var(--ink);
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
}
/* (integration) The same round 44px close as every other sheet (.modal-close). */
.tw-close {
  margin-right: calc(-1 * var(--space-1));
  border-radius: var(--radius-round);
  border-color: var(--border-soft);
}
.tw-close:hover { border-color: color-mix(in srgb, var(--gold) 60%, transparent); }

.tw-row + .tw-row { margin-top: var(--space-5); }
.tw-label { margin-bottom: var(--space-2); }

.tw-options { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.tw-options--grid { display: grid; grid-template-columns: 1fr 1fr; }

.tw-opt {
  appearance: none;
  -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: 0 var(--space-3);
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--text-md);
  line-height: var(--leading-tight);
  text-align: left;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-1);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.tw-opt:hover { color: var(--ink); border-color: var(--border-strong); background: var(--hover-wash); }
/* (integration) One chosen state for every settings chip: the CTA triad, the
   same as the chronicle settings sheet (.settings-modal .chip.active). */
.tw-opt.is-on {
  color: var(--cta-ink);
  border-color: var(--cta-edge);
  background: var(--cta-fill);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--gold) 30%, transparent);
}
.tw-opt:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* A dot of each scheme's own ground and metal. Literal colours on purpose:
   a preview of a palette cannot be drawn from the active one. */
.tw-swatch {
  flex: none;
  width: 16px;
  height: 16px;
  border-radius: var(--radius-round);
  box-shadow: 0 0 0 1px rgba(128, 128, 128, 0.45);
}
.tw-swatch--parchment   { background: radial-gradient(circle at 66% 34%, #a07820 0 30%, #f1e8d4 33%); }
.tw-swatch--observatory { background: radial-gradient(circle at 66% 34%, #e6c97a 0 30%, #1b2233 33%); }
.tw-swatch--dark-glass  { background: radial-gradient(circle at 66% 34%, #e8c06a 0 30%, #161c26 33%); }
.tw-swatch--arcane      { background: radial-gradient(circle at 66% 34%, #b78cff 0 30%, #0c0815 33%); }

.tw-note {
  margin-top: var(--space-2);
  font-size: 16px;
  font-style: italic;
  line-height: 1.45;
  color: var(--ink-faded);
}
.tw-warn {
  margin-top: var(--space-2);
  font-size: 16px;
  line-height: 1.45;
  color: var(--danger);
}
.tw-account {
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-soft);
}
.tw-account-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: var(--text-md);
  color: var(--ink-2);
}

/* ---------------------------------------------------------------- *
 * 9. Creation — shared page furniture (interview + preset choice)
 * ---------------------------------------------------------------- */

/* A column: running head, the page (which grows), the action row. */
.fd-screen.iv {
  display: flex;
  flex-direction: column;
  padding: 20px var(--space-7) var(--space-8);
}

/* The running head: brand on the page's left gutter, the way out before the
   Settings control, and the gilt hairline (DESIGN §4) running gutter to
   gutter — wordmark, rule and Settings share one edge. */
.iv-top {
  position: relative;
  z-index: 3;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: var(--tap);
  padding: 0 132px var(--space-4) 0;
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
}
.iv-top .brand { font-size: var(--text-sm); letter-spacing: 0.28em; color: var(--ink-2); }
.iv-top .brand-mark { width: 9px; height: 9px; }
.iv-top-exit { font-size: var(--text-md); }
.iv-top-exit svg { stroke-width: 2; }

.iv-page {
  position: relative;
  z-index: 1;
  flex: 1 0 auto;
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
  padding-top: var(--space-8);
}
.iv--asking .iv-page { max-width: none; display: flex; flex-direction: column; padding-top: var(--space-6); }
.iv--building .iv-page { max-width: 600px; }
.iv--summary .iv-page,
.iv-page--wide { max-width: 1000px; }

.iv-eyebrow {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0 var(--space-3);
  margin: 0 0 var(--space-3);
  animation: fdFade var(--dur-reveal) var(--ease-out) backwards;
}
.iv-eyebrow-count { display: inline-flex; align-items: center; gap: var(--space-3); }
.iv-eyebrow-count::before { content: '·'; }

.iv-error { margin: 0 0 var(--space-5); }

.iv-hint {
  font-size: var(--text-md);
  font-style: italic;
  line-height: 1.5;
  color: var(--ink-faded);
  margin: var(--space-3) 0 0;
}

/* The chronicler at work: italic, and three motes of candlelight. */
.iv .thinking {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) 0;
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-style: italic;
  color: var(--ink-2);
}
.iv .thinking > em { margin-right: 6px; }
.iv .thinking-dot {
  width: 6px;
  height: 6px;
  margin: 0 2px;
  border-radius: var(--radius-round);
  background: var(--gold);
}
.iv-wait { display: flex; margin-top: var(--space-3); }
.iv-wait--block { justify-content: center; padding: var(--space-8) 0; }

/* The action row. Opacity-only motion: it holds the phone's fixed CTA. */
.iv-actions {
  flex: none;
  display: flex;
  justify-content: flex-end;
  width: 100%;
  max-width: 720px;
  margin: var(--space-5) auto 0;
  animation: fdFade var(--dur-reveal) 240ms var(--ease-out) backwards;
}

/* On a desk the seed and the wait are centred in the page, not hung from the
   top with a hole beneath. */
@media (min-width: 768px) {
  .iv--seed .iv-page,
  .iv--building .iv-page { flex: none; margin-top: auto; padding-top: var(--space-6); }
  .iv--seed .iv-actions { margin-bottom: auto; }
  .iv--building .iv-page { margin-bottom: auto; }
}

/* ---------------------------------------------------------------- *
 * 10. Creation — seed
 * ---------------------------------------------------------------- */

.iv .iv-title {
  font-family: var(--font-serif);
  font-size: 52px;
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: 0;
  color: color-mix(in srgb, var(--ink) 90%, var(--gold));
  margin: 0 0 var(--space-3);
  text-wrap: balance;
  animation: fdRise var(--dur-reveal) var(--ease-out) backwards;
}

.iv-lede {
  font-size: 22px;
  font-style: italic;
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 34em;
  margin: 0 0 var(--space-6);
  animation: fdRise var(--dur-reveal) 80ms var(--ease-out) backwards;
}

.iv-context {
  margin: 0 0 var(--space-6);
  padding: var(--space-1) 0 var(--space-1) var(--space-4);
  border-left: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
  animation: fdRise var(--dur-reveal) 120ms var(--ease-out) backwards;
}
.iv-context-name {
  font-size: 22px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
}
.iv-context-line { font-size: var(--text-md); font-style: italic; line-height: 1.5; color: var(--ink-2); margin-top: 2px; }

.iv-seed .fd-field { animation: fdRise var(--dur-reveal) 160ms var(--ease-out) backwards; }

/* ---------------------------------------------------------------- *
 * 11. Creation — asking
 * ---------------------------------------------------------------- */

/* Wide screens: the page is a gloss and a text column, set as a pair and
   centred together; the gloss faces the text (right-aligned). The question
   block fills the page's height and anchors the answer field to its foot, so
   a three-option question does not leave a hole under the field. */
.iv-ask {
  flex: 1 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 720px);
  justify-content: center;
  column-gap: var(--space-7);
}
.iv-ask.has-margin { grid-template-columns: 248px minmax(0, 720px); }
.iv-ask-main { display: flex; flex-direction: column; min-width: 0; }
.iv-q-block { flex: 1 0 auto; display: flex; flex-direction: column; }

.iv-margin {
  padding-top: 2px;
  text-align: right;
  animation: fdFade var(--dur-reveal) 320ms var(--ease-out) backwards;
}
.iv-note + .iv-note { margin-top: var(--space-6); }
.iv-note-label { margin-bottom: var(--space-2); }
.iv-note-idea {
  margin: 0;
  font-size: var(--text-md);
  font-style: italic;
  line-height: 1.5;
  color: var(--ink-2);
}
.iv-note-idea::before { content: '“'; }
.iv-note-idea::after { content: '”'; }

.iv-trail {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: iv-trail;
}
.iv-trail-item {
  position: relative;
  font-size: var(--text-md);
  line-height: 1.4;
  counter-increment: iv-trail;
}
.iv-trail-item + .iv-trail-item { margin-top: var(--space-3); }
.iv-trail-q {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: var(--text-sm);
  font-style: italic;
  color: var(--ink-faded);
}
.iv-trail-a {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  margin-top: 2px;
  color: var(--ink);
}

/* Narrow: the same transcript, folded shut above the question. */
.iv-log {
  margin: 0 0 var(--space-6);
  background: var(--panel-bg);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-2);
}
.iv-log > summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap);
  padding: 0 var(--space-4);
  cursor: pointer;
  font-size: 20px;
  font-weight: 600;
  font-variant-caps: all-small-caps;
  letter-spacing: var(--track-caps);
  color: var(--ink-2);
}
.iv-log > summary::-webkit-details-marker { display: none; }
.iv-log > summary::after {
  content: '';
  width: 7px;
  height: 7px;
  margin: 0 4px 4px 0;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  transition: transform var(--dur-base) var(--ease-out);
}
.iv-log[open] > summary::after { transform: rotate(-135deg); margin-bottom: -2px; }
.iv-log[open] > summary { border-bottom: 1px solid var(--border-soft); }
.iv-log > summary:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; border-radius: var(--radius-2); }
.iv-log-count {
  margin-left: auto;
  font-size: var(--text-md);
  font-variant-caps: normal;
  font-variant-numeric: lining-nums;
  color: var(--ink-faded);
}
.iv-log .iv-trail { padding: var(--space-4); }
.iv-log .iv-trail-item { padding-left: 28px; }
.iv-log .iv-trail-item::before {
  content: counter(iv-trail);
  position: absolute;
  left: 0;
  top: 1px;
  font-size: var(--text-sm);
  font-variant-numeric: lining-nums;
  color: var(--ink-faded);
}
.iv-log .iv-trail-item--idea { counter-increment: none; }
.iv-log .iv-trail-item--idea::before { content: '–'; }

@media (min-width: 1200px) {
  .iv-log { display: none; }
}
@media (max-width: 1199px) {
  .iv-ask,
  .iv-ask.has-margin { grid-template-columns: minmax(0, 720px); }
  .iv-margin { display: none; }
}

/* The question — the chronicler's line, set as the page's heading. */
.iv .interview-q {
  font-family: var(--font-serif);
  font-size: var(--text-display);
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: 0;
  color: color-mix(in srgb, var(--ink) 90%, var(--gold));
  max-width: 21em;
  margin: 0 0 var(--space-5);
  text-wrap: balance;
  animation: fdRise var(--dur-reveal) var(--ease-out) backwards;
}

/* The answers the chronicler offers — the play screen's choice-row anatomy
   (DESIGN §4): numeral in a gold hairline ring, serif text, hairline
   separators. Hover, keyboard focus and the chosen answer all light the row
   the same way — a 3px gilt edge and a faint gold fill — with the chosen one
   also filling its ring; a quiet chevron says the whole row is the target.
   (.choice stays on the element for the harnesses.) */
.iv-options {
  display: flex;
  flex-direction: column;
  margin: 0 0 var(--space-6);
  border-top: 1px solid var(--border-soft);
}

.iv .iv-option {
  position: relative;
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr);
  column-gap: var(--space-4);
  align-items: start;
  width: 100%;
  margin: 0;
  padding: 14px var(--space-7) 14px var(--space-3);
  text-align: left;
  font: inherit;
  color: var(--ink);
  background: transparent;
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  border: 0;
  border-bottom: 1px solid var(--border-soft);
  border-radius: 0;
  box-shadow: none;
  cursor: pointer;
  transform: none;
  transition: background-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out),
              opacity var(--dur-slow) var(--ease-out);
  animation: fdRise var(--dur-reveal) var(--ease-out) backwards;
}
.iv .iv-option:nth-child(1) { animation-delay: 120ms; }
.iv .iv-option:nth-child(2) { animation-delay: 180ms; }
.iv .iv-option:nth-child(3) { animation-delay: 240ms; }
.iv .iv-option:nth-child(4) { animation-delay: 300ms; }
.iv .iv-option:nth-child(5) { animation-delay: 360ms; }
.iv .iv-option:nth-child(n+6) { animation-delay: 420ms; }

/* the row's chevron */
.iv .iv-option::after {
  content: '';
  position: absolute;
  right: var(--space-5);
  top: 50%;
  width: 8px;
  height: 8px;
  margin-top: -4px;
  border-top: 1.5px solid currentColor;
  border-right: 1.5px solid currentColor;
  transform: rotate(45deg);
  color: var(--ink-faded);
  opacity: 0.55;
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}

.iv .iv-option:hover,
.iv .iv-option:focus-visible,
.iv .iv-option.selected {
  background: color-mix(in srgb, var(--gold) 6%, transparent);
  box-shadow: inset 3px 0 0 var(--gold);
  transform: none;
  border-color: var(--border-soft);
}
.iv .iv-option:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }
.iv .iv-option:active,
.iv .iv-option.selected { background: color-mix(in srgb, var(--gold) 11%, transparent); }
.iv .iv-option:hover::after,
.iv .iv-option:focus-visible::after,
.iv .iv-option.selected::after { opacity: 1; color: var(--gold); transform: translateX(3px) rotate(45deg); }
.iv .iv-option:disabled { cursor: default; }

.iv-option-mark {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  margin-top: -2px;
  font-size: var(--text-md);
  font-weight: 500;
  font-variant-numeric: lining-nums;
  color: var(--gold);
  border: 1px solid color-mix(in srgb, var(--gold) 55%, transparent);
  border-radius: var(--radius-round);
  transition: background-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}
.iv .iv-option:hover .iv-option-mark,
.iv .iv-option:focus-visible .iv-option-mark { border-color: var(--gold); }
.iv .iv-option.selected .iv-option-mark {
  color: var(--paper);
  background: var(--gold);
  border-color: var(--gold);
}

.iv .iv-option .title {
  display: block;
  margin: 0 0 2px;
  font-family: var(--font-serif);
  font-size: 20px;
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: 0;
  text-transform: none;
  text-wrap: balance;
  color: var(--ink);
}
.iv .iv-option .desc {
  display: block;
  max-width: 62ch;
  font-family: var(--font-serif);
  font-size: var(--text-md);
  line-height: 1.55;
  color: var(--ink-2);
}

/* While the next question is being written the page stays put: the chosen
   answer stays lit, the rest recede, nothing reacts under the cursor. */
.iv .interview-waiting .iv-option:not(.selected) { opacity: 0.4; }
.iv .interview-waiting .iv-option:not(.selected):hover { background: transparent; box-shadow: none; }
.iv .interview-waiting .iv-option:not(.selected):hover::after { opacity: 0.55; color: var(--ink-faded); transform: rotate(45deg); }
.iv .interview-waiting .iv-option:not(.selected):hover .iv-option-mark { border-color: color-mix(in srgb, var(--gold) 55%, transparent); }

/* The free answer: the label as a divider, the writing surface, and beneath
   it (never inside it) the row of actions — Previous on the left, Answer on
   the right. It sits at the foot of the page. */
.iv-or { margin-top: auto; margin-bottom: var(--space-3); cursor: text; }
.interview-answer-row { display: block; }
.iv-answer-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-3);
}
.iv-answer-actions .iv-prev { margin-left: calc(-1 * var(--space-3)); margin-right: auto; }
.iv-answer-actions .iv-prev svg { stroke-width: 2; }
.iv-enter-hint { font-size: var(--text-sm); font-style: italic; color: var(--ink-faded); }
.iv .iv-answer-btn { min-width: 120px; }

.iv-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  margin-top: var(--space-5);
}
.iv-foot:empty { display: none; }

/* ---------------------------------------------------------------- *
 * 12. Creation — building
 * ---------------------------------------------------------------- */

.iv-build {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.iv-build .iv-eyebrow { justify-content: center; }
.iv-build .iv-wait { justify-content: center; }
/* One progress language on this page: the dial is the working light; the
   line beneath says what is happening, in words. */
.iv-build .thinking-dot { display: none; }

.fd-dial {
  position: relative;
  width: 208px;
  height: 208px;
  margin: 0 auto var(--space-6);
  animation: fdFade var(--dur-reveal) var(--ease-out) backwards;
}
.fd-dial svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
/* Every mark on the dial clears 3:1 against the ground. */
.fd-dial-face .ring { fill: none; stroke: color-mix(in srgb, var(--ink-faded) 70%, transparent); stroke-width: 1; }
.fd-dial-face .ring.outer { stroke: color-mix(in srgb, var(--gold) 55%, transparent); stroke-width: 1.5; }
.fd-dial-face .ring.faint { stroke: color-mix(in srgb, var(--ink-faded) 55%, transparent); stroke-dasharray: 2 5; }
.fd-dial-face .tick { stroke: color-mix(in srgb, var(--ink-faded) 75%, transparent); stroke-width: 1; }
.fd-dial-face .tick.major { stroke: var(--ink-2); stroke-width: 2; }
.fd-dial-sweep {
  transform-origin: 50% 50%;
  animation: fdSpin 5.2s linear infinite;
}
.fd-dial-sweep .sweep {
  fill: none;
  stroke: var(--gold);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-dasharray: 40 557;
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--gold) 70%, transparent));
}
.fd-dial-numeral {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding-top: 4px;
  font-family: var(--font-display);
  font-size: 52px;
  font-weight: 600;
  color: color-mix(in srgb, var(--ink) 90%, var(--gold));
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6);
  animation: fdBreathe 3.6s ease-in-out infinite;
}

.iv-build-title {
  font-family: var(--font-serif);
  font-size: 46px;
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: 0;
  color: color-mix(in srgb, var(--ink) 90%, var(--gold));
  margin: 0 0 var(--space-3);
  animation: fdRise var(--dur-reveal) 80ms var(--ease-out) backwards;
}
.iv-build-note {
  font-size: var(--text-md);
  font-style: italic;
  line-height: 1.5;
  color: var(--ink-faded);
  margin: var(--space-1) 0 0;
}

.iv-recap {
  width: 100%;
  max-width: 560px;
  margin-top: var(--space-7);
  padding-top: var(--space-5);
  text-align: left;
  border-top: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
  animation: fdFade var(--dur-reveal) 400ms var(--ease-out) backwards;
}
.iv-recap-label { text-align: center; margin-bottom: var(--space-3); }
.iv-recap ul { list-style: none; margin: 0; padding: 0; }
.iv-recap li {
  position: relative;
  padding-left: 24px;
  font-size: 18px;
  line-height: 1.55;
  color: var(--ink-2);
}
.iv-recap li + li { margin-top: var(--space-2); }
.iv-recap li::before {
  content: '–';
  position: absolute;
  left: 2px;
  top: 0;
  color: var(--ink-faded);
}

/* ---------------------------------------------------------------- *
 * 13. Creation — the summaries (title page + dramatis personae)
 * Narrow: one 600px column in reading order. Wide (>= 1200px): the prose
 * column and a rail beside it that holds the at-a-glance facts (the world's
 * age and rules; the character's figures and traits) above the fold, while
 * the cast and the rest follow the prose. The page's primary action is a
 * sticky bar at the foot of the viewport, so it is always in reach.
 * ---------------------------------------------------------------- */

.iv-titlepage {
  max-width: 840px;
  margin: 0 auto var(--space-6);
  text-align: center;
}
.iv-titlepage .iv-eyebrow { justify-content: center; }

.iv .iv-name {
  font-family: var(--font-display);
  font-size: 60px;
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: 0.05em;
  color: color-mix(in srgb, var(--ink) 92%, var(--gold));
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.65), 0 0 1px color-mix(in srgb, var(--gold) 35%, transparent);
  margin: 0 0 var(--space-3) 0.05em;
  text-wrap: balance;
  animation: fdInk 1000ms var(--ease-out) backwards;
}

.iv-tagline {
  font-size: 22px;
  font-style: italic;
  line-height: 1.45;
  color: var(--ink-2);
  max-width: 30em;
  margin: 0 auto var(--space-5);
  text-wrap: balance;
  animation: fdFade var(--dur-reveal) 200ms var(--ease-out) backwards;
}

.iv-spread {
  display: grid;
  grid-template-columns: minmax(0, 600px);
  grid-template-areas: "prose" "rail" "rest";
  justify-content: center;
  column-gap: var(--space-7);
}
.iv-spread > .iv-prose { grid-area: prose; }
.iv-spread > .iv-rail { grid-area: rail; }
.iv-spread > .iv-rest { grid-area: rest; }
@media (min-width: 1200px) {
  .iv-spread {
    grid-template-columns: minmax(0, 600px) 300px;
    grid-template-areas: "prose rail" "rest rail";
    grid-template-rows: auto 1fr;
  }
  .iv-rail {
    align-self: start;
    position: sticky;
    top: var(--space-5);
  }
  .iv-rail > :first-child { margin-top: 0; }
}

.iv-prose {
  animation: fdRise var(--dur-reveal) 240ms var(--ease-out) backwards;
}
.iv .summary-prose {
  max-width: none;
  margin: 0 0 1em;
  font-family: var(--font-serif);
  font-size: 22px;
  line-height: var(--leading-prose);
  color: color-mix(in srgb, var(--ink) 90%, var(--gold));
  text-wrap: pretty;
}
.iv .summary-prose:last-child { margin-bottom: 0; }
/* The drop cap sits ON the second line's baseline (initial-letter where the
   engine has it, a tuned float where it does not). */
.iv .iv-dropcap::first-letter {
  float: left;
  margin: 0.07em 0.1em 0 0;
  font-family: var(--font-display);
  font-size: 3.28em;
  font-weight: 600;
  line-height: 0.78;
  color: var(--gold);
}
@supports (initial-letter: 2) or (-webkit-initial-letter: 2) {
  .iv .iv-dropcap::first-letter {
    float: none;
    -webkit-initial-letter: 2;
            initial-letter: 2;
    margin: 0 0.12em 0 0;
    font-size: inherit;
    line-height: inherit;
  }
}

.iv-rail { min-width: 0; }

/* Facts: stacked label / value pairs between gilt hairlines. */
.iv-facts {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  margin: var(--space-7) 0 0;
  border-top: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
}
.iv-fact { padding: var(--space-3) 0 var(--space-4); border-bottom: 1px solid var(--border-soft); }
.iv-fact dt { margin-bottom: 2px; }
.iv-fact dd { margin: 0; font-size: var(--text-md); line-height: 1.5; color: var(--ink-2); }

.iv-rest { min-width: 0; }

.iv-columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-7) var(--space-6);
  margin: var(--space-7) 0 0;
}

.iv-sec { min-width: 0; }
.iv-sec-title { margin-bottom: var(--space-4); }

.iv-entries { display: flex; flex-direction: column; gap: var(--space-4); }
.iv .iv-entry { margin: 0; }
.iv-entry-name {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
}
.iv-entry-desc {
  margin-top: 2px;
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--ink-2);
}
.iv-entry-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-left: var(--space-3);
  font-size: var(--text-sm);
  font-weight: 500;
  font-style: italic;
  color: var(--gold);
}
.iv-entry-tag::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: var(--radius-round);
  background: currentColor;
}

.iv-troubles { list-style: none; margin: 0; padding: 0; counter-reset: iv-trouble; }
.iv-troubles > li {
  position: relative;
  padding-left: 28px;
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--ink);
  counter-increment: iv-trouble;
}
.iv-troubles > li + li { margin-top: var(--space-3); }
.iv-troubles > li::before {
  content: counter(iv-trouble) '.';
  position: absolute;
  left: 0;
  top: 0;
  font-variant-numeric: lining-nums;
  color: var(--ink-faded);
}

/* "How it will play" — the table's rules, on a card. */
.iv-sec--plate {
  margin: var(--space-6) 0 0;
  padding: var(--space-5);
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop, none);
          backdrop-filter: var(--panel-backdrop, none);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-2);
  box-shadow: var(--elev-1);
}
.iv-rules { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.iv-rules > li {
  position: relative;
  padding-left: 22px;
  font-size: var(--text-lg);
  line-height: 1.45;
  color: var(--ink);
}
.iv-rules > li::before {
  content: '–';
  position: absolute;
  left: 2px;
  top: 0;
  color: var(--ink-faded);
}
.iv-rules-label { font-style: italic; color: var(--ink-faded); margin-right: var(--space-1); }

/* What you are made of — cells behind one hairline; the strongest figure is
   the one gold number on the page (it is "you" at your best). */
.iv-sec--stats { margin: var(--space-7) 0 0; }
.iv-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  padding: 1px;
  background: var(--border-soft);
  border-radius: var(--radius-2);
  overflow: hidden;
}
.iv-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-4) var(--space-1) var(--space-3);
  background: var(--paper);
}
.iv-stat.is-top {
  background: linear-gradient(180deg, color-mix(in srgb, var(--gold) 9%, transparent), transparent 80%), var(--paper);
}
.iv-stat-val {
  font-family: var(--font-serif);
  font-size: var(--text-display);
  font-weight: 500;
  line-height: 1.1;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink);
}
.iv-stat.is-top .iv-stat-val { color: var(--gold); }

/* Traits are not controls, so they are not boxed: a dot in the role colour
   (success for, danger against) and the word. */
.iv-traits {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
  margin: var(--space-6) 0 0;
}
.iv .iv-trait-col { margin: 0; }
.iv-trait-label { margin-bottom: var(--space-2); }
.iv-trait-list { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4); }
.iv-trait {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--ink);
}
.iv-trait::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: var(--radius-round);
  background: var(--verdict-success);
}
.iv-trait-col--against .iv-trait::before { background: var(--danger); }

.iv-sec--opening { margin: var(--space-7) 0 0; }
.iv .iv-opening {
  padding-left: var(--space-4);
  font-size: 20px;
  font-style: italic;
  color: var(--ink-2);
  border-left: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
}

/* The revision box — at the end of the page; the bar's "Change something"
   brings the reader here. */
.iv-revise {
  max-width: 600px;
  margin: var(--space-8) auto 0;
  padding-top: var(--space-6);
  border-top: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
  scroll-margin-bottom: 160px;
}
@media (min-width: 1200px) {
  .iv-revise { max-width: 948px; }
  .iv-revise .fd-writing { max-width: 600px; }
}
.iv-revise-title {
  font-family: var(--font-serif);
  font-size: var(--text-title);
  font-weight: 500;
  line-height: var(--leading-tight);
  letter-spacing: 0;
  color: var(--ink);
  margin: 0 0 var(--space-1);
}
.iv-revise .iv-hint { margin: 0 0 var(--space-4); }
.iv-revise .iv-wait { margin-top: var(--space-3); }

/* The sticky action bar (desk): a solid plate on the viewport's foot with the
   gilt hairline on top and a fade above it, so the page slides under it and
   nothing is ever cut mid-line at the fold. */
@media (min-width: 768px) {
  .iv-actions--summary {
    position: sticky;
    /* sticky insets are measured inside the scroller's padding: step past it
       so the bar sits on the viewport's foot */
    bottom: calc(-1 * var(--space-8));
    z-index: 4;
    max-width: none;
    width: auto;
    margin: var(--space-7) calc(-1 * var(--space-7)) calc(-1 * var(--space-8));
    padding: var(--space-4) var(--space-7) var(--space-5);
    justify-content: center;
    gap: var(--space-4);
    background: var(--paper);
    border-top: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
  }
  .iv-actions--summary::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: 96px;
    pointer-events: none;
    background: linear-gradient(to top, var(--paper), transparent);
  }
  .iv-actions--summary .fd-btn--primary { min-width: 280px; }
}

/* ---------------------------------------------------------------- *
 * 14. The preset character choice — two cards side by side
 * ---------------------------------------------------------------- */

.iv-paths {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
  max-width: 880px;
  margin: 0 auto;
}

.iv-path {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--space-5);
  background: var(--panel-bg);
  -webkit-backdrop-filter: var(--panel-backdrop, none);
          backdrop-filter: var(--panel-backdrop, none);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-2);
  box-shadow: var(--elev-1);
  animation: fdRise var(--dur-reveal) 200ms var(--ease-out) backwards;
}
.iv-path + .iv-path { animation-delay: 280ms; }
.iv-path--written { border-color: color-mix(in srgb, var(--gold) 30%, transparent); }

.iv-path-name {
  margin: var(--space-2) 0 var(--space-2);
  font-family: var(--font-serif);
  font-size: var(--text-title);
  font-weight: 500;
  letter-spacing: 0;
  line-height: var(--leading-tight);
  color: var(--ink);
}
.iv-path-blurb {
  margin: 0 0 var(--space-5);
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--ink-2);
}
.iv-path-foot { margin-top: auto; }
.iv-path-foot .fd-btn { width: 100%; }
.iv--preset .fd-alert { max-width: 880px; margin: var(--space-5) auto 0; }

/* ---------------------------------------------------------------- *
 * 15. Phone (< 768px)
 * ---------------------------------------------------------------- */

@media (max-width: 767px) {
  html:not([data-screen="play"]) .tweaks-fab {
    top: calc(10px + env(safe-area-inset-top));
    right: 20px;
    padding: 0 var(--space-3);
  }

  /* The settings sheet rises from the bottom over a scrim, with its own close. */
  .tweaks-panel {
    top: auto;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    max-height: 85vh;
    max-height: 85dvh;
    padding: var(--space-2) 20px calc(var(--space-6) + env(safe-area-inset-bottom));
    border-radius: var(--radius-2) var(--radius-2) 0 0;
    border-bottom: 0;
  }
  html[data-screen="play"] .tweaks-panel { top: auto; right: 0; bottom: 0; }

  /* -- title page -- */
  .fd-folio { padding: calc(76px + env(safe-area-inset-top)) 20px calc(32px + env(safe-area-inset-bottom)); }
  .fd-title { margin: 0; }
  .fd-title::before { width: 560px; height: 340px; }
  .fd-wordmark { font-size: 38px; letter-spacing: 0.05em; margin: var(--space-4) 0 var(--space-4) 0.05em; }
  .fd-subtitle { font-size: 20px; margin-top: var(--space-4); }
  .fd-rule--engraved::before, .fd-rule--engraved::after { width: 96px; }
  /* With chronicles on the page the masthead gives way to the list. */
  .fd-folio.has-list { padding-top: calc(68px + env(safe-area-inset-top)); }
  .fd-folio.has-list .fd-wordmark { font-size: 34px; margin: var(--space-3) 0 var(--space-3) 0.05em; }
  .fd-folio.has-list .fd-subtitle { font-size: 18px; margin-top: var(--space-3); }

  .auth-wrap { margin-top: var(--space-6); }
  .fd-plate { padding: var(--space-5) 20px var(--space-4); }
  .fd-plate-title { font-size: 24px; }
  .fd-plate-text { font-size: 18px; }

  .fd-contents { margin-top: var(--space-6); }
  .save-row.fd-entry {
    grid-template-columns: 32px minmax(0, 1fr);
    column-gap: var(--space-2);
    padding: var(--space-3) 0 var(--space-2);
  }
  .fd-entry-num { align-self: start; padding-top: 4px; }
  .fd-entry-name { font-size: 24px; }
  .fd-entry-actions { grid-column: 2; margin: var(--space-1) 0 0 calc(-1 * var(--space-2)); }
  .fd-entry-resume { margin-left: auto; }
  .soft-delete-toast.fd-burning { padding: var(--space-3) 0 var(--space-2); }

  .fd-actions { flex-direction: column; align-items: stretch; gap: var(--space-2); margin-top: var(--space-6); }
  .fd-actions .fd-btn { width: 100%; }
  .fd-folio.has-list .fd-actions { margin-top: var(--space-5); }

  .fd-picker { margin-top: var(--space-6); }
  .fd-picker-grid { grid-template-columns: minmax(0, 1fr); gap: var(--space-3); }
  .world-card.fd-world { padding: 20px; }
  .world-card.fd-world .fd-world-name { font-size: 24px; }

  .fd-colophon { margin-top: var(--space-7); }

  .modal:has(> .fd-burn) { padding: 40px 28px 40px; }
  .modal:has(> .fd-burn) .fd-burn-actions { flex-direction: column; align-items: stretch; }
  .fd-burn-text { font-size: 18px; }

  /* -- creation: the full-screen scroller. The ground shows through (the
     shell is transparent), the running head is a solid plate pinned to the
     top, the CTA is a solid dock pinned to the bottom. Geometry lives in the
     WORLD-V15-01 block. -- */
  .fd-screen.iv { padding: 0 20px var(--space-7); }
  .fd-screen.iv.iv--seed,
  .fd-screen.iv.iv--summary { padding-bottom: calc(120px + env(safe-area-inset-bottom)); }

  .iv-top {
    position: sticky;
    top: 0;
    z-index: 105;
    margin: 0 -20px;
    min-height: calc(64px + env(safe-area-inset-top));
    padding: calc(10px + env(safe-area-inset-top)) 124px 10px 20px;
    background: var(--paper);
    border-bottom: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
  }
  .iv-top .brand { font-size: 14px; letter-spacing: 0.14em; gap: 8px; }
  .iv-eyebrow { flex-direction: column; align-items: flex-start; gap: 2px; }
  .iv-titlepage .iv-eyebrow, .iv-build .iv-eyebrow { align-items: center; }
  .iv-eyebrow-count::before { display: none; }
  .iv-top-exit { padding: 0 var(--space-2); }

  .iv-page { padding-top: var(--space-6); }
  .iv--building .iv-page { padding-top: var(--space-7); }

  .iv .iv-title { font-size: 38px; }
  .iv-lede { font-size: 19px; margin-bottom: var(--space-5); }
  .iv-context-name { font-size: 20px; }

  .fd-writing { font-size: 19px; }

  .iv .interview-q { font-size: 28px; margin-bottom: var(--space-5); }
  .iv-options { margin-bottom: var(--space-6); }
  .iv .iv-option { grid-template-columns: 32px minmax(0, 1fr); column-gap: var(--space-3); padding: var(--space-4) var(--space-6) var(--space-4) var(--space-2); }
  .iv .iv-option::after { right: var(--space-3); }
  .iv .iv-option .title { font-size: 19px; }

  /* REQ CHAR-V15-01 (T4): the answer row stacks and its button is full-width
     and >= 44pt; Previous sits beneath it. */
  .iv-answer-actions { flex-direction: column-reverse; align-items: stretch; gap: var(--space-2); }
  .iv-answer-actions .iv-prev { align-self: flex-start; margin: 0 0 0 calc(-1 * var(--space-3)); }
  .iv-enter-hint { display: none; }
  .iv .iv-answer-btn { width: 100%; min-height: 48px; }
  .fd-composer .fd-btn { width: 100%; }

  .iv-foot { justify-content: stretch; }
  .iv-foot .iv-enough { width: 100%; }

  .fd-dial { width: 164px; height: 164px; margin-bottom: var(--space-5); }
  .fd-dial-numeral { font-size: 42px; }
  .iv-build-title { font-size: 36px; }
  .iv-recap { margin-top: var(--space-6); }

  .iv-titlepage { margin-bottom: var(--space-6); }
  .iv .iv-name { font-size: 36px; letter-spacing: 0.04em; margin-left: 0.04em; }
  .iv-tagline { font-size: 19px; margin-bottom: 20px; }
  .iv .summary-prose { font-size: 20px; }
  .iv-columns { grid-template-columns: minmax(0, 1fr); gap: var(--space-6); margin-top: var(--space-6); }
  .iv-facts { margin-top: var(--space-6); }
  .iv-sec--plate { padding: 20px; }
  .iv-rules > li { font-size: 18px; }
  .iv-sec--stats { margin-top: var(--space-6); }
  .iv-stat-val { font-size: 30px; }
  .iv-entry-tag { margin-left: var(--space-2); }
  .iv-sec--opening { margin-top: var(--space-6); }
  .iv .iv-opening { font-size: 19px; }
  .iv-revise { margin-top: var(--space-7); padding-top: var(--space-5); }
  .iv-revise-title { font-size: 24px; }

  .iv-paths { grid-template-columns: minmax(0, 1fr); gap: var(--space-3); }
  .iv-path { padding: 20px; }
  .iv-path-name { font-size: 24px; }

  /* The dock: the pinned CTA itself (T4 pins its geometry). The button box is
     the solid dock plate with the gilt hairline on its top edge; the gold face
     is drawn inside it. On the summary the dock is split: "Change something"
     takes the left share (its own fixed button, above the dock's box), the
     primary takes the right. A 64px fade above lets the page slide under. */
  .iv .iv-actions { margin: 0; }
  .iv .iv-actions::before {
    content: '';
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(76px + env(safe-area-inset-bottom));
    height: 64px;
    z-index: 106;
    pointer-events: none;
    background: linear-gradient(to top, var(--paper), transparent);
  }
  .iv .wizard-cta-sticky,
  .iv .wizard-cta-sticky:hover {
    width: 100%;
    min-height: calc(76px + env(safe-area-inset-bottom));
    padding: 0 var(--space-5) env(safe-area-inset-bottom);
    font-size: var(--text-lg);
    color: var(--cta-ink);
    background: var(--paper);
    border: 0;
    border-top: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
    border-radius: 0;
    box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.3);
  }
  .iv .wizard-cta-sticky::before {
    content: '';
    position: absolute;
    left: var(--space-4);
    right: var(--space-4);
    top: 12px;
    bottom: calc(12px + env(safe-area-inset-bottom));
    z-index: -1;
    background: var(--cta-fill);
    border: 1px solid var(--cta-edge);
    border-radius: var(--radius-1);
    transition: box-shadow var(--dur-base) var(--ease-out);
  }
  .iv .wizard-cta-sticky:hover::before { box-shadow: var(--glow-gilt); }
  .iv .wizard-cta-sticky:disabled,
  .iv .wizard-cta-sticky:disabled:hover { opacity: 1; color: color-mix(in srgb, var(--ink-2) 55%, transparent); }
  .iv .wizard-cta-sticky:disabled::before { background: transparent; border-color: var(--border-strong); }

  .iv-actions--summary .wizard-cta-sticky,
  .iv-actions--summary .wizard-cta-sticky:hover { padding-left: 180px; padding-right: var(--space-4); }
  .iv-actions--summary .wizard-cta-sticky::before { left: 180px; }
  .iv-actions--summary .iv-change-btn {
    position: fixed;
    left: var(--space-4);
    bottom: calc(12px + env(safe-area-inset-bottom));
    z-index: 111;
    width: 156px;
    min-height: 52px;
    padding: 0 var(--space-2);
    font-size: var(--text-md);
    background: var(--paper);
  }
}

/* ---------------------------------------------------------------- *
 * 16. Motion
 * ---------------------------------------------------------------- */

@keyframes fdFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes fdRise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
@keyframes fdInk {
  from { opacity: 0; letter-spacing: 0.12em; }
  to   { opacity: 1; }
}
@keyframes fdSheetIn {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes fdSpin { to { transform: rotate(360deg); } }
@keyframes fdBreathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.72; } }

/* Reduced motion: everything arrives complete and still; the dial keeps its
   finished drawing without the travelling light. */
@media (prefers-reduced-motion: reduce) {
  .fd-folio,
  .fd-wordmark,
  .fd-subtitle,
  .fd-plate,
  .save-row.fd-entry,
  .soft-delete-toast.fd-burning,
  .fd-actions,
  .world-card.fd-world,
  .fd-colophon,
  .tweaks-panel,
  .tw-scrim,
  .iv-eyebrow,
  .iv .iv-title,
  .iv-lede,
  .iv-context,
  .iv-seed .fd-field,
  .iv-margin,
  .iv .interview-q,
  .iv .iv-option,
  .iv-actions,
  .fd-dial,
  .fd-dial-sweep,
  .fd-dial-numeral,
  .iv-build-title,
  .iv-recap,
  .iv .iv-name,
  .iv-tagline,
  .iv-prose,
  .iv-path { animation: none; }
  .fd-dial-sweep { display: none; }
  .world-card.fd-world:hover { transform: none; }
}

/* ============================================================================
   AAA INTEGRATION i2 — the chrome belongs to the book.
   One final fix round over the merged preview (critic pass i1). Everything
   here is a late override of the workstream layers above; tokens come from the
   palette blocks and SHELL's DESIGN SCALE block (no new colour tokens).
   ============================================================================ */

/* ---- 1. PLAY LAYOUT (desktop): the rail is a real full-height column. The
   body row dissolves (display: contents) so the prose column, the rail and the
   dock are items of ONE grid: header over both, the rail spanning the body and
   dock rows, the dock under the prose only. No dead strip under the rail, and
   the rail's own scroller reaches the foot of the screen. (.shell-body keeps
   its rules for the phone, where the rail is the fixed drawer.) */
@media (min-width: 769px) {
  html[data-screen="play"] .play-shell { grid-template-columns: minmax(0, 1fr) var(--shell-rail-w); }
  html[data-screen="play"] .shell-hdr { grid-column: 1 / -1; grid-row: 1; }
  html[data-screen="play"] .shell-body { display: contents; }
  html[data-screen="play"] .shell-main { grid-column: 1; grid-row: 2; position: relative; z-index: 2; }
  html[data-screen="play"] .side-pane {
    grid-column: 2;
    grid-row: 2 / 4;
    position: relative;
    z-index: 2;
    min-height: 0;
    padding-bottom: var(--space-7);
  }
  html[data-screen="play"] .shell-dock { grid-column: 1; grid-row: 3; padding-right: var(--space-5); }
  html[data-screen="play"] .shell-dock::before { right: 0; }
  html[data-screen="play"] .shell-dock::after { display: none; }
  /* the dock is now the prose column's width, so the page-width sheet is
     centred in 100% (no rail term) */
  html[data-screen="play"] .actions-panel {
    width: min(calc(100% - 2 * var(--shell-gutter)), var(--shell-page-max));
    left: calc((100% - min(calc(100% - 2 * var(--shell-gutter)), var(--shell-page-max))) / 2);
  }
  /* ONE way into the menu on a desk: the ledger's Menu tab. The ☰ stays the
     phone's door to the drawer. */
  /* (i3: the ☰ is back on a desk — the Menu is no longer a ledger tab.) */
  /* Vitals ONCE: the header meters are always on screen beside the rail, so
     the Self tab drops its copy of them (inspiration, which the header does
     not show, stays) and the skills rise above the fold. */
  html[data-screen="play"] .side-pane .self-vitals > .vital:not(.inspiration-block) { display: none; }
}

/* ---- 2. THE SHEET OF CHOICES. It closes from its own top edge; the bar
   under it ("Back to the page") is gone while it is open, so the story keeps
   that row. Capped so the story always keeps real room above it. */
html[data-screen="play"] .shell-dock:has(> .actions-panel.open) { padding-top: 0; padding-bottom: 0; border-top: 0; }
html[data-screen="play"] .shell-dock:has(> .actions-panel.open) > .actions-handle { display: none; }
html[data-screen="play"] .actions-sheet-close {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: var(--tap);
  margin: 0;
  padding: 0;
  background: var(--paper);
  border: 0;
  cursor: pointer;
}
html[data-screen="play"] .actions-sheet-grip {
  width: 44px;
  height: 5px;
  border-radius: var(--radius-round);
  background: color-mix(in srgb, var(--gold) 45%, var(--ink-faded));
  transition: background-color var(--dur-fast) var(--ease-out);
}
html[data-screen="play"] .actions-sheet-close:hover .actions-sheet-grip { background: var(--gold); }
html[data-screen="play"] .actions-sheet-close:focus-visible { outline: none; }
html[data-screen="play"] .actions-sheet-close:focus-visible .actions-sheet-grip {
  background: var(--gold);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 35%, transparent);
}
html[data-screen="play"] .actions-panel-inner {
  grid-template-areas: "grip grip" "head insp" "list list" "rest rest" "comp comp";
}
html[data-screen="play"] .actions-sheet-close { grid-area: grip; background: none; }
html[data-screen="play"] .actions-panel.actions-panel { padding-top: 0; }
html[data-screen="play"] .actions-panel {
  border-radius: var(--radius-2) var(--radius-2) 0 0;
  box-shadow: 0 -14px 32px rgba(0, 0, 0, 0.45), var(--edge-light);
  max-height: min(58vh, 600px);
  max-height: min(58dvh, 600px);
}
@media (max-width: 768px) {
  html[data-screen="play"] .actions-panel {
    max-height: 55vh;
    max-height: 55dvh;
  }
  /* compact rows: the risk + skill line sits tight under the choice */
  html[data-screen="play"] .actions-panel .suggested-action { padding-top: 10px; padding-bottom: 10px; }
  html[data-screen="play"] .actions-panel .suggested-action-row.meta { margin-top: 2px; font-size: 15px; line-height: 1.3; }
  html[data-screen="play"] .choice-prompt { font-size: 21px; }
  html[data-screen="play"] .actions-panel .suggested-actions-header { margin-bottom: var(--space-2); }
}

/* ---- 3. RISK IS A WORD AND A DOT. The dot carries the tier colour (the
   badge's currentColor); the word and the DC read in the neutral meta inks. */
html[data-screen="play"] .actions-panel .risk-badge .risk-word { color: var(--ink-2); font-weight: 600; }
html[data-screen="play"] .actions-panel .risk-badge .risk-dc { color: var(--ink-faded); opacity: 1; font-weight: 500; }

/* ---- 4. METER ICONS MATCH THEIR BARS (header and ledger alike). */
.side-pane .vital-hp .vital-icon { color: color-mix(in srgb, var(--danger) 72%, var(--ink-faded)); }
.side-pane .vital-mana .vital-icon,
.side-pane .vital-stam .vital-icon { color: color-mix(in srgb, var(--gold) 82%, var(--ink-faded)); }

/* ---- 5. THE READING EDGES. The fades live inside the scroller's padding —
   the first line is never half-dissolved under the header rule. */
html[data-screen="play"] .play-shell .feed {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 24px, black calc(100% - 24px), transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, black 24px, black calc(100% - 24px), transparent 100%);
  scroll-padding-top: 32px;
  padding-top: 40px;
}
@media (max-width: 768px) {
  html[data-screen="play"] .play-shell .feed { padding-top: 32px; }
}

/* ---- 6. THE ROLL, AT REST. A centred ceremony: the die large, the readout on
   one serif line, the total as the headline, the outcome word in its colour.
   Slots are reserved from mount, so the card keeps one height through the
   whole ceremony (the scroll snap stays truthful). THREE's pacing is untouched. */
html[data-screen="play"] .play-shell .feed .roll-card {
  --rc-die: 148px;
  max-width: 520px;
  margin-left: auto;
  margin-right: auto;
  padding: var(--space-4) var(--space-5) var(--space-5);
  background: var(--tex-grain), linear-gradient(180deg, color-mix(in srgb, var(--rc-verdict) 7%, var(--page-deep)), var(--page-deep) 55%);
  border: 1px solid color-mix(in srgb, var(--gold) 26%, transparent);
  border-top: 2px solid var(--rc-verdict);
  box-shadow: var(--elev-2), var(--edge-light);
}
html[data-screen="play"] .play-shell .feed .roll-card::before { display: none; }
html[data-screen="play"] .play-shell .feed .roll-card.rc-pair { --rc-die: 120px; }
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-stage {
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  row-gap: var(--space-3);
  text-align: center;
}
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-flow,
html[data-screen="play"] .play-shell .feed .roll-card.rc-pair .roll-card-flow {
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-2);
  width: 100%;
}
html[data-screen="play"] .play-shell .feed .roll-card .rc-sentence {
  font-size: 20px;
  white-space: normal;
  min-height: 0;
  color: var(--ink-2);
  text-wrap: balance;
}
html[data-screen="play"] .play-shell .feed .roll-card .rc-outcome {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
}
html[data-screen="play"] .play-shell .feed .roll-card .rc-big {
  display: block;
  min-height: 1em;
  font-family: var(--font-serif);
  font-size: 56px;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink);
}
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict,
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.success,
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.fail,
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.partial {
  min-height: 1.25em;
  padding: 0;
  border: 0;
  font-size: 24px;
  letter-spacing: 0.08em;
}
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.crit-success,
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.crit-fail { font-size: 22px; letter-spacing: 0.06em; }
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-header { justify-content: center; margin-bottom: var(--space-2); }
@media (max-width: 600px) {
  html[data-screen="play"] .play-shell .feed .roll-card { --rc-die: 128px; padding: var(--space-4); }
  html[data-screen="play"] .play-shell .feed .roll-card.rc-pair { --rc-die: 112px; }
  html[data-screen="play"] .play-shell .feed .roll-card .rc-sentence { font-size: 19px; min-height: 0; }
  html[data-screen="play"] .play-shell .feed .roll-card .rc-big { font-size: 48px; }
  html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict,
  html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.success,
  html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.fail,
  html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.partial { font-size: 22px; }
}
@media (prefers-reduced-motion: no-preference) {
  html[data-screen="play"] .play-shell .feed .roll-card .rc-big.rc-known { animation: rt3Speak 220ms var(--ease-out) both; }
}

/* ---- 7. PLAY MICROTYPE — nothing under 14px; helpers at 15px. */
html[data-screen="play"] .composer-foot .composer-key-hint { font-size: 15px; }
html[data-screen="play"] .actions-handle-count { font-size: 15px; }
.side-pane .ledger-eyebrow { font-size: 14px; font-variant-caps: normal; text-transform: uppercase; letter-spacing: 0.1em; }
.side-pane .vital-max,
.side-pane .self-level .label-text { font-size: 14px; }

/* ---- 8. ONE BUTTON SYSTEM. Primary = filled gold, dark ink (the CTA triad,
   now solid in the default palette). Secondary = transparent, a gold-tinted
   hairline, ink. Selected (a chip, a row, a toggle) is NEVER the primary's
   fill: gold edge, a light gold tint, gold word. Disabled is a quiet dashed
   outline that still reads (>= 3.5:1), with a reason printed beside it. */
.fd-btn--quiet,
.settings-modal .btn:not(.btn-primary):not(.btn-ghost):not(.modal-close):not(.chip),
.confirm-modal .btn:not(.btn-primary):not(.btn-danger):not(.modal-close) {
  border-color: color-mix(in srgb, var(--gold) 38%, transparent);
  color: var(--ink);
}
.btn-primary,
.fd-btn--primary,
.settings-modal .settings-done.btn { font-weight: 600; }
.settings-modal .settings-done.btn {
  color: var(--cta-ink);
  background: var(--cta-fill);
  border-color: var(--cta-edge);
}
.chip.active,
.settings-modal .chip.active,
.tw-opt.is-on,
html[data-screen="play"] .actions-panel .btn-inspire.active {
  color: var(--gold);
  background: color-mix(in srgb, var(--gold) 10%, transparent);
  border-color: var(--gold);
  box-shadow: none;
}
.btn-primary:disabled,
.fd-btn--primary:disabled,
.iv .iv-answer-btn:disabled,
.levelup-step .levelup-confirm.btn:disabled,
html[data-screen="play"] .shell-dock .play-composer .composer-send:disabled {
  opacity: 1;
  color: var(--ink-faded);
  background: transparent;
  border: 1px dashed color-mix(in srgb, var(--gold) 40%, transparent);
  box-shadow: none;
  cursor: not-allowed;
}
/* The phone ledger's exit is a way back, not a call to act: secondary. */
html[data-screen="play"] .drawer-close-bar button,
html[data-screen="play"] .drawer-close-bar button:hover {
  color: var(--ink);
  background: var(--tex-grain), var(--paper-2);
  border-color: color-mix(in srgb, var(--gold) 45%, transparent);
}
.save-row.fd-entry:hover .fd-entry-resume.fd-btn--quiet,
.save-row.fd-entry:focus-visible .fd-entry-resume.fd-btn--quiet {
  color: var(--gold);
  background: color-mix(in srgb, var(--gold) 10%, transparent);
  border-color: var(--gold);
}

/* ---- 9. SHEETS NEVER SLICE TEXT. Opaque footers with a shadow instead of a
   long fade; the fade (where kept) is <= 24px and sits over padding; every
   sheet fits the viewport with a margin. */
.modal-bg.sheet-backdrop > .modal { max-height: calc(100vh - 64px); max-height: calc(100dvh - 64px); }
.settings-foot { box-shadow: 0 -12px 24px -8px rgba(0, 0, 0, 0.5); }
.settings-foot::before { height: var(--space-4); opacity: 0.7; }
.settings-modal .modal-head { box-shadow: 0 12px 24px -12px rgba(0, 0, 0, 0.45); }
.settings-modal .modal-head::after { display: none; }
.settings-modal .setting-label {
  font-size: 15px;
  font-variant-caps: normal;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-2);
}
.settings-modal .settings-group-title,
.settings-modal .ledger-eyebrow { font-size: 15px; font-variant-caps: normal; text-transform: uppercase; letter-spacing: 0.1em; }
@media (max-width: 480px) {
  .modal-bg.sheet-backdrop > .modal { max-height: 100%; }
}

/* ---- 10. LEVEL UP. One compact header row (seal · the line · the steps) so
   three whole feats show; the current step is lit; the bar says in words what
   is missing; Confirm is solid gold once it can be pressed. */
.levelup-scroll {
  padding-top: var(--space-5);
  padding-bottom: calc(var(--space-7));
  -webkit-mask-image: linear-gradient(to bottom, black calc(100% - var(--space-5)), transparent);
          mask-image: linear-gradient(to bottom, black calc(100% - var(--space-5)), transparent);
}
.levelup-hero {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  column-gap: var(--space-5);
  text-align: left;
  padding: 0 var(--space-7) var(--space-4) 0;
}
.levelup-seal-row { margin: 0; max-width: none; }
.levelup-seal { width: 72px; height: 72px; }
.levelup-seal-num { font-size: 34px; }
.levelup-hero-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.levelup-eyebrow { font-size: 15px; font-variant-caps: normal; text-transform: uppercase; letter-spacing: 0.12em; padding-left: 0; }
.levelup-step .level-header { font-size: 22px; }
.levelup-steps { justify-content: flex-start; margin-top: var(--space-2); }
.levelup-steps-item.is-current .levelup-steps-btn {
  color: var(--gold);
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 12%, transparent);
}
.levelup-steps-item.is-current .levelup-step-mark { border-color: var(--gold); color: var(--gold); }
.levelup-step .levelup-section-title { margin-top: var(--space-5); }
.levelup-step .auto-rewards .levelup-section-title { margin-top: 0; }
.levelup-step .reward-line { padding-top: var(--space-2); padding-bottom: var(--space-2); }
.levelup-step .feat-group-name {
  font-size: 15px;
  font-style: normal;
  font-variant-caps: normal;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-2);
  margin-bottom: var(--space-2);
}
.levelup-step .feat-card { padding-top: var(--space-3); padding-bottom: var(--space-3); }
.levelup-step .feat-card[disabled] { opacity: 0.55; }
.levelup-step .levelup-actions {
  background: var(--tex-grain), var(--sheet-fill);
  box-shadow: 0 -12px 24px -8px rgba(0, 0, 0, 0.5);
}
.levelup-status-text {
  font-family: var(--font-serif);
  font-size: 15px;
  font-style: italic;
  color: var(--ink-2);
}
.levelup-step .levelup-actions.is-ready .levelup-status-text { color: var(--gold); font-style: normal; }
.levelup-step .levelup-actions.is-ready .levelup-confirm.btn,
.levelup-step .levelup-actions.is-ready .levelup-confirm.btn:hover {
  color: var(--cta-ink);
  background: var(--cta-fill);
  border-color: var(--cta-edge);
}
@media (max-width: 480px) {
  .levelup-hero { padding-right: var(--space-7); column-gap: var(--space-4); }
  .levelup-seal { width: 60px; height: 60px; }
  .levelup-seal-num { font-size: 28px; }
  .levelup-step .level-header { font-size: 19px; }
  .levelup-step .levelup-actions { flex-wrap: wrap; row-gap: var(--space-1); }
  .levelup-step .levelup-status { flex: 1 0 100%; }
  .levelup-step .levelup-buttons { flex: 1 0 100%; justify-content: space-between; }
  .levelup-step .levelup-confirm.btn { flex: 1; min-width: 0; }
}

/* ---- 11. CREATION PAGES. One calm 56px header row; the answer field stays
   on screen (sticky at the page foot, opaque, shadowed); a disabled Answer
   says why; the summary's action bar is opaque and its fade short. */
.iv-top {
  display: grid;
  grid-template-columns: var(--tap) minmax(0, 1fr) var(--tap);
  align-items: center;
  min-height: 56px;
  padding: 0 0 var(--space-2);
}
.iv-top .iv-top-exit { grid-column: 1; justify-self: start; color: var(--ink-2); }
.iv-top .brand { grid-column: 2; justify-self: center; font-size: 15px; letter-spacing: 0.24em; }
html[data-screen="world"] .tweaks-fab,
html[data-screen="character"] .tweaks-fab,
html[data-screen="interview"] .tweaks-fab {
  width: var(--tap);
  min-width: var(--tap);
  padding: 0;
  justify-content: center;
}
html[data-screen="world"] .tweaks-fab .tweaks-fab-label,
html[data-screen="character"] .tweaks-fab .tweaks-fab-label,
html[data-screen="interview"] .tweaks-fab .tweaks-fab-label { display: none; }
.iv-eyebrow { font-size: 15px; }
.iv .interview-answer-row {
  position: sticky;
  bottom: 0;
  z-index: 3;
  margin: 0 calc(-1 * var(--space-4));
  padding: var(--space-3) var(--space-4) calc(var(--space-3) + env(safe-area-inset-bottom));
  background: var(--tex-grain), var(--paper);
  border-top: 1px solid color-mix(in srgb, var(--gold) 22%, transparent);
  box-shadow: 0 -12px 24px -8px rgba(0, 0, 0, 0.5);
}
.iv .iv-options { margin-bottom: var(--space-4); }
.iv .iv-option { padding-top: 12px; padding-bottom: 12px; }
.iv .iv-option .desc { max-width: none; }
.iv-answer-why { font-size: 15px; font-style: italic; color: var(--ink-faded); }
.iv-answer-actions { margin-top: var(--space-2); }
/* an opaque bar with a shadow: nothing dissolves above it */
.iv-summary .iv-actions--summary::before,
.iv-actions--summary::before,
.iv .iv-actions::before { display: none; }
.iv-actions--summary { box-shadow: 0 -12px 24px -8px rgba(0, 0, 0, 0.5); }

/* ---- 12. THE FRONT DOOR LIST. The first action is on the first screen; the
   whole row resumes; the rest of a row's actions sit behind one ⋯. */
.fd-folio .fd-actions { margin-top: var(--space-5); margin-bottom: var(--space-2); }
.fd-folio.has-list .fd-actions { margin-top: var(--space-5); }
.fd-folio.has-list .fd-contents { margin-top: var(--space-6); }
.fd-entry-actions { gap: var(--space-2); }
.fd-entry-go { display: inline-flex; width: var(--tap); justify-content: center; color: var(--ink-faded); }
@media (min-width: 601px) { .save-row.fd-entry.is-latest .fd-entry-go { display: none; } }
.save-row.fd-entry:hover .fd-entry-go,
.save-row.fd-entry:focus-visible .fd-entry-go { color: var(--gold); }
.fd-entry-more-wrap { position: relative; display: inline-flex; }
.fd-entry-more { color: var(--ink-2); border-radius: var(--radius-round); }
.fd-entry-more[aria-expanded="true"] { color: var(--gold); border-color: color-mix(in srgb, var(--gold) 45%, transparent); }
.fd-entry-menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-1));
  z-index: 20;
  display: flex;
  flex-direction: column;
  min-width: 220px;
  padding: var(--space-2);
  background: var(--tex-grain), var(--sheet-fill);
  border: 1px solid color-mix(in srgb, var(--gold) 30%, var(--border-soft));
  border-radius: var(--radius-2);
  box-shadow: var(--elev-3);
  cursor: default;
}
.fd-entry-menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap);
  padding: 0 var(--space-3);
  font-family: var(--font-serif);
  font-size: var(--text-md);
  color: var(--ink);
  background: none;
  border: 0;
  border-radius: var(--radius-1);
  text-align: left;
  cursor: pointer;
}
.fd-entry-menu-item:hover,
.fd-entry-menu-item:focus-visible { background: var(--hover-wash); outline: none; }
.fd-entry-menu-item.is-danger { color: var(--danger); }
.fd-entry-menu-note {
  padding: var(--space-2) var(--space-3) var(--space-1);
  margin-top: var(--space-1);
  border-top: 1px solid var(--border-soft);
  font-size: 15px;
  color: var(--ink-faded);
}
@media (max-width: 600px) {
  .save-row.fd-entry { grid-template-columns: 32px minmax(0, 1fr) auto; align-items: center; }
  .fd-entry-actions { grid-column: 3; margin: 0; }
  .fd-entry-resume { display: none; }
  .save-row.fd-entry.is-latest .fd-entry-actions::before { content: none; }
  .fd-entry-name { font-size: 22px; }
  .fd-entry-meta { font-size: 15px; }
}

/* ---- 13. REVIEW FIXES (i2 shots). */
/* the gear on the creation pages is an icon, so its icon must show */
html[data-screen="world"] .tweaks-fab svg,
html[data-screen="character"] .tweaks-fab svg { display: inline-block; }
/* one border on a compose field: no ruled lines, no margin column */
.fd-writing--compose {
  min-height: 52px;
  padding: 12px var(--space-4);
  line-height: 1.45;
  background-image: none;
  background-clip: padding-box;
  border-color: color-mix(in srgb, var(--gold) 30%, transparent);
}
/* the sheet footers: an opaque edge and a shadow, no dissolving row above */
.settings-foot::before { display: none; }
.settings-modal .setting-hint,
.settings-modal .setting-help { font-size: 15px; }
.levelup-step .levelup-section-title,
.levelup-section-text {
  font-size: 15px;
  font-variant-caps: normal;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
@media (max-width: 768px) {
  /* phone: the answer field and its two buttons in ~130px, not half the screen */
  .iv .interview-answer-row { padding-top: var(--space-2); padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom)); }
  .iv .interview-answer-row .fd-writing--compose { min-height: 48px; font-size: 18px; }
  .iv .iv-answer-actions { flex-direction: row; align-items: center; gap: var(--space-2); }
  .iv-answer-actions .iv-answer-why,
  .iv-answer-actions .iv-enter-hint { display: none; }
  .iv .iv-answer-actions .iv-prev { align-self: center; margin: 0 auto 0 calc(-1 * var(--space-2)); }
  /* the label is still the field's name, just not a second line of chrome */
  .iv .iv-or { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; margin: 0; }
  /* the sticky field sits on the real foot of the page */
  .iv.iv--asking.wizard-interview { padding-bottom: 0; }
  .iv .iv-answer-actions .iv-answer-btn { width: auto; min-width: 132px; flex: 0 0 auto; }
}
/* the date takes its own line on a phone, so no separator is left hanging */
@media (max-width: 600px) {
  .fd-entry-meta > span:has(+ .fd-entry-when)::after { content: none; }
  .fd-entry-meta > .fd-entry-when { flex-basis: 100%; }
}
/* the ledger: skill groups in plain sentence case, the drawer's exit an
   opaque bar with a shadow (no 48px dissolve over the last rows), and the
   sheet a little tighter so the skills come up the page */
.skills-block .skill-group-header {
  font-size: 17px;
  font-weight: 600;
  font-variant-caps: normal;
  letter-spacing: 0.01em;
  color: var(--ink);
}
.side-pane .attr-cell { min-height: 68px; padding: var(--space-2) var(--space-1); }
.side-pane .self-section + .self-section { margin-top: var(--space-5); }
@media (max-width: 768px) {
  html[data-screen="play"] .drawer-close-bar::before { display: none; }
  html[data-screen="play"] .drawer-close-bar { box-shadow: 0 -12px 24px -8px rgba(0, 0, 0, 0.5); }
}
/* type floor: the last small-caps labels read ~11px — set them as true
   capitals at 15px with modest tracking */
.iv-eyebrow,
.iv-eyebrow > span,
.side-pane .treasured-eyebrow,
.side-pane .ledger-section-title,
.settings-modal .settings-advanced-title {
  font-size: 15px;
  font-variant-caps: normal;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.key-suffix { font-variant-caps: normal; }
html[data-screen="play"] .actions-panel .btn-inspire .btn-inspire-count { color: var(--ink-2); }
/* a wider page on a desk: the side margins read as the desk, not dead bands */
@media (min-width: 1280px) { html[data-screen="play"] .play-shell { --shell-page-max: 880px; } }
@media (min-width: 1600px) { html[data-screen="play"] .play-shell { --shell-page-max: 940px; } }
.iv.iv--asking.wizard-interview { padding-bottom: 0; }
.iv--asking .iv-foot { margin-bottom: var(--space-5); }

/* ============================================================================
   AAA INTEGRATION i3 — proof at the edges, one lit brass button, material.
   ============================================================================ */

/* ---- A. SCROLL EDGES. Every scroller fades 28px in at the top and 40px out
   at the bottom, and every bar that sits over scrolling text has a short fade
   in its own colour above/below it plus a shadow — no line of text ever meets
   an edge at full strength. scroll-padding keeps focus/snap targets clear. */
html[data-screen="play"] .play-shell .feed {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 28px, #000 calc(100% - 40px), transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, #000 28px, #000 calc(100% - 40px), transparent 100%);
  scroll-padding-top: 36px;
  scroll-padding-bottom: 48px;
}
@media (min-width: 769px) {
  html[data-screen="play"] .side-pane {
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 40px), transparent 100%);
            mask-image: linear-gradient(to bottom, #000 calc(100% - 40px), transparent 100%);
    scroll-padding-bottom: 64px;
    padding-bottom: 72px;
  }
}
/* the sticky tab row: a solid bar, one continuous rule, and a fade under it */
.side-pane .tab-row { position: sticky; }
.side-pane .tab-row::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 24px;
  pointer-events: none;
  background: linear-gradient(to bottom, var(--paper-2), transparent);
}
html[data-screen="play"] .side-pane .tab-row { border-bottom: 1px solid color-mix(in srgb, var(--gold) 20%, transparent); }
/* sheets: short fades above the footer / below the head, in the sheet colour */
.settings-foot::before {
  display: block;
  height: 32px;
  opacity: 1;
  background: linear-gradient(to top, var(--surface-raised), transparent), linear-gradient(to top, var(--paper), transparent);
}
.settings-modal .modal-head::after {
  display: block;
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 24px;
  pointer-events: none;
  background: linear-gradient(to bottom, var(--surface-raised), transparent), linear-gradient(to bottom, var(--paper), transparent);
}
.levelup-scroll {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 28px, #000 calc(100% - 40px), transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, #000 28px, #000 calc(100% - 40px), transparent 100%);
  scroll-padding-bottom: 48px;
}
.iv-actions--summary::before,
.iv-summary .iv-actions--summary::before {
  display: block;
  height: 32px;
  background: linear-gradient(to top, var(--paper), transparent);
}
@media (max-width: 768px) {
  .iv .iv-actions::before { display: block; height: 32px; }
  html[data-screen="play"] .drawer-close-bar::before { display: block; height: 32px; }
}
.iv .interview-answer-row::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 32px;
  pointer-events: none;
  background: linear-gradient(to top, var(--paper), transparent);
}
.iv .interview-answer-row { box-shadow: none; }
/* the menu page and the creation pages scroll as a whole: fade their foot */
.fd-screen.iv {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 40px), transparent 100%);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 40px), transparent 100%);
}
.fd-screen.iv.iv--asking,
.fd-screen.iv.iv--summary { -webkit-mask-image: none; mask-image: none; }

/* "More below": a quiet cue on the settings sheet while it can still scroll
   (a scroll-driven animation — no script; absent where unsupported). */
@supports (animation-timeline: scroll()) {
  .settings-foot::after {
    content: 'More below \2304';
    position: absolute;
    left: var(--space-6);
    top: 50%;
    transform: translateY(-50%);
    font-family: var(--font-serif);
    font-size: 15px;
    color: var(--ink-2);
    pointer-events: none;
    animation-name: aaaCueOut;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-duration: auto;
    animation-timeline: scroll(nearest block);
  }
  @keyframes aaaCueOut { 0%, 92% { opacity: 1; } 100% { opacity: 0; } }
}

/* bottom bars clear the home indicator */
@media (max-width: 768px) {
  html[data-screen="play"] .shell-dock { padding-bottom: max(16px, calc(env(safe-area-inset-bottom) + 12px)); }
  html[data-screen="play"] .drawer-close-bar { padding-bottom: max(16px, calc(env(safe-area-inset-bottom) + 12px)); }
  .levelup-step .levelup-actions { padding-bottom: max(16px, calc(env(safe-area-inset-bottom) + 12px)); }
  .iv .interview-answer-row {
    margin-left: -20px;
    margin-right: -20px;
    padding: var(--space-3) 20px max(16px, calc(env(safe-area-inset-bottom) + 12px));
  }
}
@media (max-width: 480px) {
  .settings-foot { padding-bottom: max(16px, calc(env(safe-area-inset-bottom) + 12px)); }
}
@media (min-width: 769px) {
  /* the answer panel is exactly the column's width */
  .iv .interview-answer-row { margin-left: 0; margin-right: 0; padding: var(--space-4) 0 var(--space-6); }
}

/* ---- B. ONE LIT BRASS PRIMARY, one quiet disabled state, one icon button.
   The plate is the palette's own CTA fill, lit: a highlight at the top, a
   darker foot, a 1px dark rim, a soft drop shadow (every palette). */
.btn-primary:not(:disabled),
.fd-btn--primary:not(:disabled),
.settings-modal .settings-done.btn,
.levelup-step .levelup-actions.is-ready .levelup-confirm.btn,
html[data-screen="play"] .shell-dock .play-composer .composer-send:not(:disabled) {
  color: var(--cta-ink);
  background: linear-gradient(180deg, color-mix(in srgb, var(--cta-fill) 80%, white) 0%, var(--cta-fill) 45%, color-mix(in srgb, var(--cta-fill) 80%, black) 100%);
  border: 1px solid color-mix(in srgb, var(--cta-fill) 35%, black);
  box-shadow: inset 0 1px 0 rgba(255, 246, 220, 0.5), inset 0 -1px 0 rgba(0, 0, 0, 0.25), 0 2px 8px rgba(0, 0, 0, 0.45);
}
.btn-primary:not(:disabled):hover,
.fd-btn--primary:not(:disabled):hover,
.settings-modal .settings-done.btn:hover {
  color: var(--cta-ink);
  background: linear-gradient(180deg, color-mix(in srgb, var(--cta-fill) 76%, white) 0%, var(--cta-fill) 50%, color-mix(in srgb, var(--cta-fill) 84%, black) 100%);
  box-shadow: inset 0 1px 0 rgba(255, 246, 220, 0.55), 0 0 0 3px color-mix(in srgb, var(--gold) 22%, transparent), 0 3px 12px rgba(0, 0, 0, 0.5);
}
.iv .wizard-cta-sticky::before {
  background: linear-gradient(180deg, color-mix(in srgb, var(--cta-fill) 80%, white) 0%, var(--cta-fill) 45%, color-mix(in srgb, var(--cta-fill) 80%, black) 100%);
  border-color: color-mix(in srgb, var(--cta-fill) 35%, black);
  box-shadow: inset 0 1px 0 rgba(255, 246, 220, 0.5), 0 2px 8px rgba(0, 0, 0, 0.45);
}
.btn-primary:disabled,
.fd-btn--primary:disabled,
.iv .iv-answer-btn:disabled,
.levelup-step .levelup-confirm.btn:disabled,
html[data-screen="play"] .shell-dock .play-composer .composer-send:disabled {
  opacity: 1;
  color: var(--ink-faded);
  background: color-mix(in srgb, var(--gold) 9%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 26%, transparent);
  box-shadow: none;
  text-shadow: none;
  cursor: not-allowed;
}
/* one 44x44 icon button for every header control */
html:not([data-screen="play"]) .tweaks-fab,
.iv-top .iv-top-exit.fd-icon-btn,
html[data-screen="play"] .shell-menu-btn {
  width: var(--tap);
  min-width: var(--tap);
  height: var(--tap);
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
  border-radius: var(--radius-1);
}
html:not([data-screen="play"]) .tweaks-fab:hover,
.iv-top .iv-top-exit.fd-icon-btn:hover,
html[data-screen="play"] .shell-menu-btn:hover { color: var(--ink); border-color: color-mix(in srgb, var(--gold) 60%, transparent); background: var(--hover-wash); }
html:not([data-screen="play"]) .tweaks-fab svg { display: inline-block; }
html:not([data-screen="play"]) .tweaks-fab .tweaks-fab-label { display: none; }
/* difficulty dots: a ramp you can tell apart */
html[data-screen="play"] .risk-badge[data-tier="moderate"] { color: var(--verdict-partial); }
html[data-screen="play"] .risk-badge[data-tier="hard"]     { color: color-mix(in srgb, var(--danger) 55%, var(--verdict-partial)); }
html[data-screen="play"] .actions-panel .risk-badge::before { width: 9px; height: 9px; }
/* the creation pages' sticky head: a short fade below it, so nothing scrolls
   under it at full strength; the eyebrow is one line */
.iv-top::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 28px;
  pointer-events: none;
  background: linear-gradient(to bottom, var(--paper), transparent);
}
.iv-eyebrow { flex-wrap: nowrap; white-space: nowrap; }
@media (max-width: 400px) { .iv-eyebrow, .iv-eyebrow > span { font-size: 14px; letter-spacing: 0.06em; } }
@media (max-width: 480px) { .settings-foot::after { display: none; } }
@media (max-width: 768px) {
  .iv-eyebrow { flex-direction: row; align-items: center; gap: 0 var(--space-2); }
  .iv-eyebrow-count::before { display: inline; }
}
@media (max-width: 768px) {
  /* the phone CTA dock: the bar is paper; only its inner plate is brass */
  .iv .wizard-cta-sticky.fd-btn--primary,
  .iv .wizard-cta-sticky.fd-btn--primary:hover {
    color: var(--cta-ink);
    background: var(--paper);
    border: 0;
    border-top: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
    box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
  }
}

/* ---- C4. THE PAGE ON A LIT DESK. On a desk the reading column is an ~820px
   sheet with grain, a soft inner vignette and feathered edges (the feather sits
   outside the text measure), under a warm lamp pool — the gutters read as the
   desk, not as dead space. The ambient motes stay on phone only. */
@media (min-width: 900px) {
  html[data-screen="play"] .play-shell { --shell-page-max: 820px; }
  html[data-screen="play"] .shell-main::before {
    width: min(calc(100% - 2 * var(--shell-gutter) + 96px), calc(var(--shell-page-max) + 96px));
    background:
      var(--tex-grain),
      var(--tex-grain),
      radial-gradient(ellipse 70% 38% at 50% 0%, color-mix(in srgb, var(--gold) 12%, transparent), transparent 80%),
      linear-gradient(180deg, var(--page), var(--page-deep));
    box-shadow: inset 0 0 120px rgba(0, 0, 0, 0.5);
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 48px, #000 calc(100% - 48px), transparent 100%);
            mask-image: linear-gradient(90deg, transparent 0, #000 48px, #000 calc(100% - 48px), transparent 100%);
  }
  html[data-screen="play"] .shell-ambient::before {
    background:
      radial-gradient(ellipse 55% 60% at 42% 35%, color-mix(in srgb, var(--gold) 9%, transparent), transparent 70%),
      var(--vignette-warm);
  }
  html[data-screen="play"] .shell-ambient .rt3-ambient { display: none; }
}

/* ---- D. THE ROLL AT REST (i3). A spotlit plate: the die large on a dark,
   warm-lit surface; the readout as chips (the roll, each modifier, the DC);
   the total as the headline; the outcome word in an outcome-tinted frame. The
   card keeps one height through the ceremony. */
html[data-screen="play"] .play-shell .feed .roll-card {
  --rc-die: 244px;
  max-width: 600px;
  padding: var(--space-4) var(--space-6) var(--space-6);
  background:
    var(--tex-grain),
    radial-gradient(ellipse 60% 55% at 50% 34%, color-mix(in srgb, var(--gold) 16%, transparent), transparent 70%),
    radial-gradient(ellipse 90% 70% at 50% 100%, color-mix(in srgb, var(--rc-verdict) 10%, transparent), transparent 70%),
    color-mix(in srgb, var(--paper) 70%, black);
  border: 1px solid color-mix(in srgb, var(--rc-verdict) 45%, transparent);
  border-top: 2px solid var(--rc-verdict);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 240, 210, 0.05);
}
html[data-screen="play"] .play-shell .feed .roll-card.rc-pair { --rc-die: 176px; }
html[data-screen="play"] .play-shell .feed .roll-card .rc-sentence {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--space-2);
  font-size: 18px;
}
html[data-screen="play"] .play-shell .feed .roll-card .rc-roll,
html[data-screen="play"] .play-shell .feed .roll-card .rc-mod,
html[data-screen="play"] .play-shell .feed .roll-card .rc-dc {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  min-height: 32px;
  padding: 3px var(--space-3);
  border: 1px solid color-mix(in srgb, var(--gold) 28%, transparent);
  border-radius: var(--radius-round);
  background: color-mix(in srgb, var(--gold) 7%, transparent);
  color: var(--ink);
  line-height: 1.4;
}
html[data-screen="play"] .play-shell .feed .roll-card .rc-dc {
  border-color: color-mix(in srgb, var(--ink-2) 40%, transparent);
  background: color-mix(in srgb, var(--ink-2) 8%, transparent);
}
html[data-screen="play"] .play-shell .feed .roll-card .rc-big { font-size: 60px; margin-top: var(--space-2); }
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict,
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.success,
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.fail,
html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.partial {
  font-size: 28px;
  padding: 2px var(--space-5);
  border: 1px solid color-mix(in srgb, currentColor 50%, transparent);
  border-radius: var(--radius-1);
  background: color-mix(in srgb, currentColor 12%, transparent);
}
@media (max-width: 600px) {
  html[data-screen="play"] .play-shell .feed .roll-card { --rc-die: 184px; padding: var(--space-4); }
  html[data-screen="play"] .play-shell .feed .roll-card.rc-pair { --rc-die: 132px; }
  html[data-screen="play"] .play-shell .feed .roll-card .rc-big { font-size: 52px; }
  html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict,
  html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.success,
  html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.fail,
  html[data-screen="play"] .play-shell .feed .roll-card .roll-card-verdict.partial { font-size: 26px; }
  html[data-screen="play"] .play-shell .feed .roll-card .rc-sentence { font-size: 17px; }
}

/* ---- C2/C3/C5. Art plates and the level-up moment. */
.iv-cartouche { display: block; width: min(100%, 520px); height: auto; margin: 0 auto var(--space-3); }
.side-pane .self-sigil { flex: none; filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.45)); }
.art-sigil text, .art-cartouche text { user-select: none; }
.levelup-seal { width: 96px; height: 96px; }
.levelup-seal-num { font-size: 46px; }
.levelup-headline {
  font-family: var(--font-display);
  font-size: 40px;
  font-weight: 600;
  line-height: 1.05;
  color: var(--gold);
  letter-spacing: 0.02em;
}
.levelup-step .level-header { font-size: 19px; color: var(--ink-2); }
.levelup-eyebrow { display: none; }
/* a real stepper: numbered marks joined by a rule; current / done / to do */
.levelup-steps { gap: 0; align-items: center; }
.levelup-steps-item { display: flex; align-items: center; }
.levelup-steps-item + .levelup-steps-item::before {
  content: '';
  width: 36px;
  height: 1px;
  margin: 0 var(--space-2);
  background: color-mix(in srgb, var(--gold) 40%, transparent);
}
.levelup-steps-btn,
.levelup-steps-item.is-current .levelup-steps-btn,
.levelup-steps-item.is-done .levelup-steps-btn {
  min-height: var(--tap);
  padding: 0 var(--space-2);
  background: none;
  border: 0;
  border-radius: var(--radius-1);
}
.levelup-steps-item.is-current .levelup-steps-btn { color: var(--gold); font-weight: 600; }
.levelup-steps-item.is-current .levelup-step-mark {
  background: color-mix(in srgb, var(--gold) 18%, transparent);
  border: 2px solid var(--gold);
  color: var(--gold);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--gold) 12%, transparent);
}
.levelup-step .feat-card { grid-template-columns: 24px 36px minmax(0, 1fr); }
.levelup-step .feat-glyph { display: inline-flex; color: color-mix(in srgb, var(--gold) 75%, var(--ink-faded)); padding-top: 2px; }
/* locked: muted (the glyph and ring at 55%), the words still legible, the
   requirement in the danger ink at full strength */
.levelup-step .feat-card.is-locked { background: color-mix(in srgb, var(--tint-sunken) 60%, transparent); border-style: solid; }
.levelup-step .feat-card.is-locked .feat-glyph,
.levelup-step .feat-card.is-locked .feat-check { opacity: 0.55; }
.levelup-step .feat-card.is-locked .feat-name { color: var(--ink-2); }
.levelup-step .feat-card.is-locked .feat-desc { color: var(--ink-faded); }
.levelup-step .feat-card.is-locked .feat-prereq { color: var(--danger); }
.levelup-step .feat-card.is-locked .feat-glyph { color: var(--ink-faded); }
.levelup-step .feat-prereq { display: inline-flex; align-items: center; gap: 6px; font-size: 15px; }
@media (max-width: 480px) {
  .levelup-seal { width: 76px; height: 76px; }
  .levelup-seal-num { font-size: 36px; }
  .levelup-headline { font-size: 32px; }
  .levelup-step .feat-card { grid-template-columns: 22px 30px minmax(0, 1fr); }
}

/* ---- C1. The front-door hero (three-stage.js Hero): a candle on the desk,
   behind the title page. A calm shade over the text column keeps the title and
   the list above 4.5:1; without WebGL / with reduced motion the CSS lamp below
   is all there is. */
.menu-screen .fd-ambient {
  background:
    radial-gradient(ellipse 38% 34% at 78% 82%, color-mix(in srgb, var(--gold) 13%, transparent), transparent 70%),
    radial-gradient(ellipse 80% 60% at 50% 110%, rgba(60, 40, 20, 0.35), transparent 70%);
}
.rt3-hero { position: absolute; inset: 0; opacity: 0; transition: opacity 1.6s ease; }
.rt3-hero.rt3-live { opacity: 1; }
.rt3-hero-canvas { display: block; width: 100%; height: 100%; }
.rt3-hero::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse 46% 70% at 50% 42%, rgba(12, 9, 6, 0.62), rgba(12, 9, 6, 0.25) 70%, transparent 100%);
}
@media (max-width: 600px) {
  .rt3-hero::after { background: linear-gradient(180deg, rgba(12, 9, 6, 0.55), rgba(12, 9, 6, 0.72) 60%, rgba(12, 9, 6, 0.4)); }
}

/* ---- E. TYPE, HINTS, COLUMNS (i3). Hints are roman, >= 15px; the
   creation page is one centred column with the idea as its epigraph; sheets
   are wider; the phone sheet opens at ~42% and grows by its grip; on a desk
   the sheet closes from the bar under it, as before (no drag pill). */
html[data-screen="play"] .composer-foot .composer-key-hint,
.levelup-status-text,
.iv-answer-why,
.iv-hint,
.iv-enter-hint,
.fd-colophon p,
.settings-modal .setting-hint,
.side-pane .inspiration-hint {
  font-style: normal;
  font-size: 15px;
}
html[data-screen="play"] .actions-handle-count { font-style: normal; }
.iv-ask.has-margin { grid-template-columns: minmax(0, 760px); row-gap: var(--space-4); }
.iv-ask.has-margin .iv-margin {
  text-align: left;
  max-width: 760px;
  padding: 0 0 var(--space-3);
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 18%, transparent);
}
@media (min-width: 900px) {
  .modal.settings-modal { max-width: min(880px, 92vw); }
  .modal-bg.levelup-backdrop > .modal.levelup-modal { max-width: min(820px, 92vw); }
  .modal-bg.sheet-backdrop { -webkit-backdrop-filter: blur(8px) saturate(90%); backdrop-filter: blur(8px) saturate(90%); }
  html[data-screen="play"] .actions-sheet-close { display: none; }
  html[data-screen="play"] .shell-dock:has(> .actions-panel.open) > .actions-handle { display: flex; }
  html[data-screen="play"] .shell-dock:has(> .actions-panel.open) { padding-top: var(--space-2); padding-bottom: var(--space-2); }
  html[data-screen="play"] .actions-panel.actions-panel { padding-top: var(--space-5); }
}
@media (max-width: 768px) {
  html[data-screen="play"] .actions-panel { max-height: 42vh; max-height: 42dvh; transition: max-height var(--dur-slow) var(--ease-in-out), transform 220ms ease, opacity 220ms ease, visibility 0s linear 220ms; }
  html[data-screen="play"] .actions-panel.open { transition: max-height var(--dur-slow) var(--ease-in-out), transform 220ms ease, opacity 220ms ease, visibility 0s; }
  html[data-screen="play"] .actions-panel.is-tall { max-height: calc(100vh - 80px); max-height: calc(100dvh - 80px); }
  html[data-screen="play"] .actions-sheet-grip { width: 36px; height: 4px; background: color-mix(in srgb, var(--ink) 22%, transparent); }
  html[data-screen="play"] .actions-sheet-close { touch-action: none; }
}
/* brighter chevrons */
.skills-block .skill-group-header .chevron { color: var(--ink-2); }
.iv .iv-option::after { color: var(--ink-2); opacity: 0.85; }
.fd-entry-go { color: var(--ink-2); }
@media (max-width: 480px) {
  .levelup-steps { flex-wrap: nowrap; }
  .levelup-steps-item + .levelup-steps-item::before { width: 18px; margin: 0 var(--space-1); }
}
/* the sheet's sticky composer: a short fade above it, so a choice scrolling
   under it dissolves instead of being sliced */
html[data-screen="play"] .actions-panel .play-composer::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 28px;
  pointer-events: none;
  background: linear-gradient(to top, var(--page), transparent);
}
/* the answer row's fade only shows while the page can still scroll under it
   (a scroll-driven animation; a page that fits never shows it) */
/* the fade above the answer bar is always there; the options leave it room
   (a 36px gap) so on a page that fits it lies over whitespace only */
.iv .iv-options { margin-bottom: 36px; }
/* (i3) the 'Or say it your own way' label now rides inside the answer bar */
.iv .interview-answer-row { margin-top: auto; }
.iv .interview-answer-row .iv-or { margin-top: 0; }
@media (max-width: 768px) {
  .iv-top { padding: calc(10px + env(safe-area-inset-top)) 20px 10px; }
}
@media (max-width: 768px) { .iv .interview-answer-row::before { height: 48px; } }
