/* ─────────────────────────────────────────────────────────────────────
   base.css — STRUCTURE, theme-agnostic. Layout, typography, components and
   animation, all painted through design TOKENS (CSS custom properties). The
   token VALUES live in the theme files (theme-manuscript.css / theme-pixel.css)
   and are selected by <html data-theme="…">. Add a theme = add a token set.
   Runtime-only vars (--wound, --depth) are set live by the controller.
   ───────────────────────────────────────────────────────────────────────── */
:root { --wound: 0; }

* { box-sizing: border-box; }
      body {
        margin: 0;
        background:
          radial-gradient(120% 80% at 50% -10%, #1a1410 0%, var(--bg) 60%);
        color: var(--ink);
        font: 16px/1.5 var(--font-body);
        min-height: 100vh;
      }

      /* ── Ambient layers (persist across screen swaps; never capture clicks) ── */
      .ambient { position: fixed; inset: 0; pointer-events: none; }
      .ambient.torch {
        z-index: 0;
        background:
          radial-gradient(120% 85% at 50% -12%, rgba(86, 60, 30, 0.5) 0%, rgba(20, 15, 11, 0) 55%);
        animation: flicker 6s ease-in-out infinite;
      }
      .ambient.blood {
        z-index: 5;
        background:
          radial-gradient(120% 100% at 50% 50%, transparent 46%, rgba(94, 20, 20, 0.3) 78%, var(--blood-edge) 100%);
        opacity: var(--wound);
        transition: opacity 700ms ease;
      }
      /* Difficulty as felt depth: a cold rises from below as the tier climbs,
         cooling the warm torch. Sits behind the content, like the torch. */
      .ambient.deep {
        z-index: 1;
        background:
          radial-gradient(130% 110% at 50% 118%, rgba(30, 44, 64, 0.55) 0%, rgba(20, 28, 40, 0) 56%);
        opacity: var(--depth, 0);
        transition: opacity 900ms ease;
      }
      /* The combat room scene: a foe backdrop behind the manuscript panel. Sits
         lowest (behind torch/deep/app), cooled by the same --depth as the world,
         with a vignette scrim that keeps text legible around the leaf and sinks
         the corners into the dark. Toggled on only during combat by the controller.
         Opacity is NOT transitioned: the fade-in always sits behind the opaque
         enter-room reveal (so it'd never be seen), while a fade-OUT on leaving
         combat would race the next panel's screen-in entrance — two overlapping
         transitions reading as a "double" load. Snapping it leaves one clean
         entrance. The filter still eases (the boss phase-2 colour shift). */
      .ambient.room {
        z-index: 0;
        background-size: cover; background-position: center; background-repeat: no-repeat;
        opacity: 0; transition: filter 600ms ease;
        filter: saturate(calc(1 - var(--depth, 0) * 0.5)) brightness(calc(0.92 - var(--depth, 0) * 0.18));
      }
      .ambient.room.on { opacity: 0.9; }
      .ambient.room.cold { filter: saturate(0.32) brightness(0.74) hue-rotate(8deg); } /* boss phase 2 */
      .ambient.room::after {
        content: ""; position: absolute; inset: 0;
        background:
          radial-gradient(125% 108% at 50% 30%, rgba(8,8,11,0) 24%, rgba(7,8,12,0.5) 70%, rgba(4,5,8,0.92) 100%),
          linear-gradient(rgba(8,9,12,0.34), rgba(8,9,12,0.5));
      }
      /* The keep's MAIN backdrop: the descent stair, shown behind the non-combat
         screens (creation, gates, run-end). Sits LOWEST of all (behind the foe
         room scene, torch and deep), so when combat begins the foe's `.ambient.room`
         fades in over it. Cover-positioned and dimmed by its own ::after scrim so
         the manuscript panel and text stay legible; cooled by the same `--depth`
         as the world. Toggled on by the controller on every non-combat screen
         (applyKeepBackdrop). Opacity is NOT transitioned for the same reason the
         room isn't — it always sits behind an opaque screen-in entrance. */
      .ambient.keep {
        z-index: -1;
        background-size: cover; background-position: center; background-repeat: no-repeat;
        opacity: 0;
        filter: saturate(calc(1 - var(--depth, 0) * 0.5)) brightness(calc(0.9 - var(--depth, 0) * 0.18));
      }
      .ambient.keep.on { opacity: 0.7; }
      .ambient.keep::after {
        content: ""; position: absolute; inset: 0;
        background:
          radial-gradient(125% 110% at 50% 28%, rgba(8,8,11,0) 18%, rgba(7,8,12,0.55) 68%, rgba(4,5,8,0.94) 100%),
          linear-gradient(rgba(8,9,12,0.42), rgba(8,9,12,0.58));
      }

      /* The boss bleeds the room even at full health; near death it beats. */
      body.boss-fight .ambient.blood { opacity: calc(0.14 + var(--wound) * 0.86); }
      /* Low on health: the wound vignette PULSES red like a racing heart — a
         stronger, faster double-thump than the resting bleed, so the danger reads
         at a glance (paired with the ember surge driven from the controller). */
      body.critical .ambient.blood { animation: heartbeat 1.15s ease-in-out infinite; }

      /* The ember field: fire sparks streaming up the whole screen. The canvas is
         painted by the controller; this just places it above the scene and keeps
         it from ever catching a click (inherited from .ambient). */
      .ambient.sparks { z-index: 6; width: 100%; height: 100%; }
      /* The keep's masonry is a procedural <canvas> (drawn by startMasonry) — over
         the room backdrop, under #app so the panel covers it. Hidden by default;
         the pixel skin switches it on (the only theme that wears stonework). */
      .ambient.masonry { display: none; z-index: 1; width: 100%; height: 100%; }

      @keyframes flicker {
        0%, 100% { opacity: 1; }
        20% { opacity: 0.9; }
        38% { opacity: 1; }
        52% { opacity: 0.82; }
        61% { opacity: 0.97; }
        76% { opacity: 0.88; }
        90% { opacity: 1; }
      }
      @keyframes heartbeat {
        0%, 100% { opacity: calc(0.42 * var(--wound)); }
        14% { opacity: calc(1.05 * var(--wound)); }
        28% { opacity: calc(0.55 * var(--wound)); }
        44% { opacity: var(--wound); }
        60% { opacity: calc(0.42 * var(--wound)); }
      }

      #app {
        position: relative;
        z-index: 2;
        max-width: 720px;
        margin: 0 auto;
        padding: 28px 20px 60px;
      }

      /* ── Headings: ruled small-caps, like a chapter head ── */
      header { text-align: center; margin-bottom: 18px; }
      h1 {
        font-size: 42px;
        font-variant: small-caps;
        letter-spacing: 0.12em;
        margin: 0;
        color: var(--gold);
        text-shadow: 0 2px 22px rgba(var(--accent-rgb), 0.3);
      }
      h2 {
        color: var(--gold);
        font-variant: small-caps;
        font-weight: 600;
        letter-spacing: 0.06em;
        margin: 0 0 14px;
        padding-bottom: 8px;
        border-bottom: 1px solid var(--rule);
      }
      .meta, .hint { color: var(--dim); }
      .hint { font-size: 14px; }
      /* The between-runs meta line doubles as a doorway to the Chronicle:
         difficulty, deepest-so-far and the Chronicle link share one clickable
         line. Styled as a faint gold-outlined pill so it reads as a button (a
         main feature, not a passive status readout) without competing with the
         Descend CTA. */
      .meta-chronicle {
        display: inline-block; margin: 6px auto 0; padding: 6px 14px;
        border: 1px solid color-mix(in srgb, var(--gold) 45%, var(--rule));
        background: color-mix(in srgb, var(--gold) 9%, transparent);
        border-radius: var(--radius);
        color: var(--ink); font: inherit; cursor: pointer;
        box-shadow: 0 1px 10px rgba(var(--accent-rgb), 0.10);
        transition: color 0.12s, background 0.12s, border-color 0.12s;
      }
      .meta-chronicle .chronicle-link {
        color: var(--gold); font-variant: small-caps; letter-spacing: 0.04em;
        font-weight: 600; transition: color 0.12s;
      }
      .meta-chronicle:hover, .meta-chronicle:focus-visible {
        color: var(--ink-bright);
        background: color-mix(in srgb, var(--gold) 16%, transparent);
        border-color: color-mix(in srgb, var(--gold) 65%, var(--rule));
      }

      /* The Chronicle teaser: the top deepest descents, inline on the creation
         screen, the whole card a doorway into the Chronicle. A leaderboard you
         can SEE between runs — names and marks to chase — not just a link. */
      .chronicle-teaser {
        display: block; width: 100%; max-width: 460px; margin: 14px auto 0;
        padding: 10px 14px 8px; text-align: left; font: inherit; cursor: pointer;
        color: var(--ink); border: 1px solid var(--rule); border-radius: var(--radius);
        background: color-mix(in srgb, var(--panel) 82%, transparent);
        transition: border-color 0.12s, background 0.12s;
      }
      .chronicle-teaser:hover, .chronicle-teaser:focus-visible {
        border-color: color-mix(in srgb, var(--gold) 55%, var(--rule));
        background: color-mix(in srgb, var(--panel) 92%, transparent);
      }
      .ct-head {
        display: flex; justify-content: space-between; align-items: baseline;
        gap: 10px; margin-bottom: 7px; padding-bottom: 6px;
        border-bottom: 1px solid var(--rule);
      }
      .ct-title {
        color: var(--gold); font-variant: small-caps; letter-spacing: 0.05em; font-size: 14px;
      }
      .ct-all { color: var(--steel); font-size: 12px; white-space: nowrap; }
      .chronicle-teaser:hover .ct-all, .chronicle-teaser:focus-visible .ct-all { color: var(--gold); }
      .ct-row {
        display: flex; align-items: baseline; gap: 10px;
        padding: 3px 0; font-size: 14px;
      }
      .ct-rank { color: var(--gold); width: 12px; flex: none; text-align: right; }
      .ct-hero { flex: 1 1 auto; color: var(--ink); min-width: 0;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      /* The hero name has priority over the build label: cap and ellipsis the
         build (usually a short preset name; a hand-rolled 3-stat hand can run
         wide) so it truncates before it starves the hero of room. */
      .ct-build { color: var(--dim); flex: 0 1 auto; max-width: 120px; min-width: 0;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      .ct-stat { color: var(--steel); flex: none; white-space: nowrap; }

      /* ── Name row: the hero's identity, randomised and customisable ── */
      .namerow {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 8px;
        margin: 10px 0 4px;
      }
      .namerow label { color: var(--dim); font-size: 14px; font-variant: small-caps; letter-spacing: 0.06em; }
      .namerow input {
        background: rgba(0, 0, 0, 0.25);
        border: 1px solid var(--rule);
        border-radius: var(--radius);
        color: var(--ink);
        font-family: inherit;
        font-size: 16px;
        padding: 5px 9px;
        max-width: 16em;
        text-align: center;
      }
      .namerow input:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }
      .namerow button { padding: 4px 8px; line-height: 1; }
      /* The reroll die is a bare icon action — no frame at all, ever. On hover it
         simply LIFTS: a small rise plus a raised drop-shadow on the glyph, so the
         die looks picked up rather than boxed. */
      .namerow .reroll {
        border: none; background: none; box-shadow: none; font-size: 18px;
        transition: transform 0.12s, text-shadow 0.12s;
      }
      .namerow .reroll:hover:not(:disabled),
      .namerow .reroll:focus-visible {
        border: none; background: none;
        transform: translateY(-2px);
        text-shadow: 2px 3px 2px rgba(0, 0, 0, 0.6);
      }

      /* ── Panel: a manuscript leaf with an etched gold hairline ── */
      .panel {
        position: relative;
        background: linear-gradient(180deg, var(--panel), var(--panel-2));
        border: 1px solid var(--edge);
        border-radius: var(--radius);
        padding: 24px 26px;
        box-shadow:
          0 12px 34px rgba(0, 0, 0, 0.55),
          inset 0 0 0 1px rgba(var(--accent-rgb), 0.05);
        margin-top: 16px;
      }

      button {
        font: inherit;
        color: var(--ink);
        background: var(--btn-bg);
        border: 1px solid var(--edge);
        border-radius: var(--radius);
        padding: 10px 14px;
        cursor: pointer;
        transition: border-color 0.12s, transform 0.12s, box-shadow 0.12s, background 0.12s;
      }
      button:hover:not(:disabled) { border-color: var(--gold); }
      button:active:not(:disabled) { transform: translateY(1px); }
      button:disabled { opacity: 0.4; cursor: default; }
      /* tactile hover-lift on the selection cards/buttons (not the +/− steppers) */
      .card:hover:not(:disabled),
      .tool:hover:not(:disabled),
      .item:hover:not(:disabled),
      .preset:hover:not(:disabled),
      .primary:hover:not(:disabled) {
        transform: translateY(-2px);
        box-shadow: 0 7px 20px rgba(0, 0, 0, 0.45);
        border-color: var(--gold);
      }
      .card:active:not(:disabled),
      .tool:active:not(:disabled),
      .item:active:not(:disabled),
      .preset:active:not(:disabled),
      .primary:active:not(:disabled) { transform: translateY(0); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); }
      /* Headings receive focus PROGRAMMATICALLY (to announce a new screen to a
         screen reader), not via the keyboard — so they must not paint the
         browser's default outline (it framed the title in an ugly box). */
      h1:focus, h2:focus { outline: none; }
      /* Keyboard focus on real controls: a clear, on-brand gold ring. The browser
         default was rgb(16,16,16) — all but invisible on the candlelit dark. */
      :focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
      .primary {
        background: var(--btn-primary-bg);
        border-color: var(--gold);
        color: var(--loot-ink);
        font-variant: small-caps;
        letter-spacing: 0.04em;
      }
      .ghost { background: transparent; }
      .wide { width: 100%; margin-top: 12px; }

      /* ── status bar ── */
      /* Sticky to the top of the scroll: the hero's vitals ride along as the page
         scrolls, and the whole bar is a button that opens the character sheet. */
      .status {
        display: flex; align-items: center; justify-content: space-between;
        gap: 14px; padding: 10px 14px;
        border: 1px solid var(--edge); border-radius: var(--radius); background: var(--surface-inset);
        position: sticky; top: 8px; z-index: 20;
        cursor: pointer;
        box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
        transition: border-color 0.12s, box-shadow 0.12s;
      }
      .status:hover, .status:focus-visible { border-color: var(--gold); }
      .status:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
      /* a quiet affordance that the bar opens the sheet */
      .status-cue {
        color: var(--dim); font-size: 12px; font-variant: small-caps;
        letter-spacing: 0.05em; white-space: nowrap; display: inline-flex;
        align-items: center; gap: 5px;
      }
      .status:hover .status-cue, .status:focus-visible .status-cue { color: var(--gold); }
      .hp { display: flex; align-items: center; gap: 10px; min-width: 200px; flex: 1 1 200px; }
      .hp > span { font-variant: small-caps; letter-spacing: 0.03em; }
      .hp-bar, .menace-bar {
        position: relative;
        height: 12px; flex: 1; min-width: 110px;
        background: var(--surface-sunk); border: 1px solid var(--edge); border-radius: var(--radius-sm);
        overflow: hidden;
        box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.6);
      }
      /* notched pips — reads as a carved gauge, not a loading bar */
      .hp-bar::after, .menace-bar::after {
        content: ""; position: absolute; inset: 0; pointer-events: none;
        background: repeating-linear-gradient(90deg, transparent 0 12px, rgba(0, 0, 0, 0.5) 12px 14px);
      }
      /* The fill's HUE tracks health, not just its width: vital amber-green when
         whole, sliding through caution-amber to blood as the hero is wounded
         (--wound: 0 whole → 1 at death's door, set live by the controller). The
         hsl mix walks the hue the short way, so it reads like a health gauge. */
      .hp-fill {
        height: 100%;
        background: linear-gradient(
          90deg,
          color-mix(in hsl, var(--blood) calc(var(--wound) * 100%), #41603f),
          color-mix(in hsl, var(--blood) calc(var(--wound) * 100%), var(--vital))
        );
        transition: width 0.3s ease, background 0.45s ease;
      }
      .status-meta { color: var(--dim); font-size: 14px; text-align: right; }

      /* ════════════════════════════════════════════════════════════════
         The descent as a ROUTE: an iconified spine (desktop margin rail),
         a compact ribbon (mobile), and a full branched map overlay. Every
         node wears an inked sigil saying what guards it; the road you took
         lights gold, the road not taken stays a faint ghost.
         ════════════════════════════════════════════════════════════════ */
      .sig { fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
      .sig-fill { fill: currentColor; }
      .sigil { display: block; width: 100%; height: 100%; }

      /* ── desktop margin rail: iconified vertical spine ── */
      .descent {
        display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
        margin-top: 10px; padding: 9px 14px;
        border: 1px solid var(--edge); border-radius: var(--radius); background: var(--surface-inset);
      }
      .descent-label {
        color: var(--dim); font-size: 12px; font-variant: small-caps;
        letter-spacing: 0.06em; white-space: nowrap;
      }
      .descent-rail { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
      .dstop { position: relative; display: flex; align-items: center; justify-content: center; }
      /* a node: a sigil in a tinted disc/bar/diamond. color drives the sigil ink. */
      .dnode {
        position: relative; width: 26px; height: 26px; border-radius: var(--radius-round);
        background: var(--node-bg); border: 1px solid var(--node-border); color: var(--node-ink);
        display: flex; align-items: center; justify-content: center; padding: 4px;
      }
      .dnode.gate { border-radius: var(--radius); }                 /* a barred gate */
      .dnode.boss { border-radius: var(--radius); transform: rotate(45deg); background: var(--boss-node-bg); border-color: var(--boss-node-border); width: 30px; height: 30px; }
      .dnode.boss .sigil { transform: rotate(-45deg); }   /* keep the crown upright */
      .dnode.entrance { width: 18px; height: 18px; color: var(--node-ink); border-color: var(--rule); padding: 3px; }
      .dnode.done { background: var(--gold); border-color: var(--accent-lit); color: #1a1206; }
      .dnode.current {
        background: var(--gold); border-color: var(--accent-lit); color: #1a1206;
        animation: nodepulse 1.6s ease-in-out infinite;
      }
      .dnode.boss.current { background: #c8612e; border-color: #f0b48a; color: #1a1206; }
      .dnode.boss.current.phase-1 { background: #5f8aa6; border-color: #bcd6e6; box-shadow: 0 0 10px rgba(110, 150, 180, 0.85); }
      /* the road not taken at a passed/active fork: a small ghost off the spine */
      .dnode.ghost {
        position: absolute; left: calc(100% + 4px); width: 16px; height: 16px;
        opacity: 0.5; color: var(--node-ink); border-color: var(--rule); background: var(--node-ghost-bg); padding: 2px;
      }
      .dnode.ghost.hoard { color: var(--node-ink); border-color: var(--hoard-edge); }
      /* a hairline stub from the spine out to a ghost, so it reads as a branch */
      .dnode.ghost::before {
        content: ""; position: absolute; right: 100%; top: 50%; width: 5px; height: 1px;
        background: var(--rule); transform: translateY(-50%);
      }
      /* a faint tick beside your best-ever depth — the line you reach for */
      .dnode.best::after {
        content: ""; position: absolute; left: 50%; top: -8px;
        width: 1px; height: 5px; background: var(--steel); transform: translateX(-50%);
      }
      .descent-map-btn {
        display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
        background: none; border: 1px solid var(--rule); border-radius: var(--radius);
        color: var(--steel); font: inherit; font-size: 12px; font-variant: small-caps;
        letter-spacing: 0.05em; padding: 5px 10px;
      }
      .descent-map-btn:hover { border-color: var(--gold); color: var(--ink); }
      .descent-map-btn .sigil { width: 16px; height: 16px; }
      @keyframes nodepulse {
        0%, 100% { box-shadow: 0 0 5px rgba(var(--accent-rgb), 0.45); }
        50% { box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.95); }
      }
      /* ── Phase-2 journey: the reached node IGNITES on a fresh advance (a one-shot
         burst over the steady current-pulse), so descending reads as arriving. ── */
      .dnode.arrived { animation: nodepulse 1.6s ease-in-out infinite, node-ignite 0.7s cubic-bezier(0.2, 0.8, 0.2, 1); }
      @keyframes node-ignite {
        0% { transform: scale(0.55); box-shadow: 0 0 0 rgba(200, 146, 58, 0); }
        45% { transform: scale(1.35); box-shadow: 0 0 22px rgba(240, 210, 130, 0.95); }
        100% { transform: scale(1); box-shadow: 0 0 12px rgba(200, 146, 58, 0.95); }
      }
      /* the boss diamond keeps its 45° rotation — flash via box-shadow only */
      .dnode.boss.arrived { animation: node-ignite-boss 0.7s ease; }
      @keyframes node-ignite-boss {
        0% { box-shadow: 0 0 0 rgba(200, 90, 46, 0); }
        45% { box-shadow: 0 0 24px rgba(240, 150, 90, 0.95); }
        100% { box-shadow: 0 0 10px rgba(200, 90, 46, 0.7); }
      }

      /* ── mobile ribbon (shown < 940px instead of the rail) ──
         A 3-column grid: the current sigil, the level plate and the map button
         sit on one centred row; the segment bar spans the full width beneath. */
      .descent-ribbon {
        display: grid; grid-template-columns: auto 1fr auto;
        grid-template-areas: "cur lvl map" "seg seg seg";
        align-items: center; column-gap: 10px; row-gap: 9px; margin-top: 10px; padding: 9px 12px;
        border: 1px solid var(--edge); border-radius: var(--radius); background: var(--ribbon-bg);
      }
      .ribbon-cur {
        grid-area: cur; width: 32px; height: 32px; border-radius: var(--radius-round); padding: 6px;
        background: var(--gold); border: 2px solid var(--accent-lit); color: #1a1206;
        display: flex; align-items: center; justify-content: center;
        box-shadow: 0 0 10px rgba(var(--accent-rgb), 0.55);
      }
      .ribbon-cur.boss { background: #c8612e; border-color: #f0b48a; }
      .ribbon-cur.arrived { animation: ribbon-ignite 0.7s cubic-bezier(0.2, 0.8, 0.2, 1); }
      @keyframes ribbon-ignite {
        0% { transform: scale(0.6); box-shadow: 0 0 0 rgba(200, 146, 58, 0); }
        45% { transform: scale(1.3); box-shadow: 0 0 20px rgba(240, 210, 130, 0.95); }
        100% { transform: scale(1); box-shadow: 0 0 10px rgba(200, 146, 58, 0.55); }
      }
      /* the level title as a small plate — matches the route-map's depth-band
         cartouches so the two surfaces read alike */
      .ribbon-lvl {
        grid-area: lvl; justify-self: start; max-width: 100%;
        color: var(--gold-lit, var(--accent-lit)); font-size: 12px; font-variant: small-caps;
        letter-spacing: 0.05em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
        border: 1px solid var(--rule); border-radius: var(--radius); background: var(--ribbon-lvl-bg); padding: 3px 10px;
      }
      .ribbon-seg { grid-area: seg; display: flex; align-items: center; }
      .ribbon-seg .rdot { width: 9px; height: 9px; border-radius: var(--radius-round); background: var(--rdot-bg); border: 1px solid var(--rdot-border); flex: 0 0 auto; }
      .ribbon-seg .rdot.gate { border-radius: var(--radius-sm); width: 11px; height: 8px; }
      .ribbon-seg .rdot.boss { width: 10px; height: 10px; transform: rotate(45deg); border-radius: var(--radius-sm); background: var(--boss-node-bg); border-color: var(--boss-node-border); }
      .ribbon-seg .rdot.done { background: var(--gold); border-color: var(--accent-lit); }
      .ribbon-seg .rdot.current { background: var(--gold); border-color: var(--accent-lit); box-shadow: 0 0 7px rgba(var(--accent-rgb), 0.7); }
      .ribbon-seg .rlink { height: 2px; flex: 1 1 auto; min-width: 6px; background: var(--rlink); }
      .ribbon-seg .rlink.done { background: var(--gold); }
      .ribbon-map {
        grid-area: map; cursor: pointer; background: none; border: 1px solid var(--rule);
        border-radius: var(--radius); color: var(--steel); font: inherit; font-size: 11px;
        font-variant: small-caps; letter-spacing: 0.04em; padding: 6px 9px; white-space: nowrap;
        display: inline-flex; align-items: center; gap: 5px;
      }
      .ribbon-map .sigil { width: 14px; height: 14px; flex: 0 0 auto; }
      .descent, .descent-ribbon { display: none; }   /* shown per width below */
      @media (max-width: 939.98px) { .descent-ribbon { display: grid; } }

      /* On a wide viewport the rail stands UP as a left-margin spine — you watch
         yourself descend it top to bottom. Narrow screens use the ribbon above.
         The breakpoint (940px) leaves a clear gap between the rail and the 720px
         content column so the two never touch. */
      @media (min-width: 940px) {
        .descent {
          display: flex;
          position: fixed; top: 90px;
          left: max(14px, calc(50% - 360px - 92px));
          flex-direction: column; gap: 8px; width: 78px; padding: 18px 8px 16px; z-index: 3;
          background: var(--rail-bg); border-color: var(--edge);
          /* a long (4-level) run's spine can outrun the viewport — let it scroll
             inside the rail rather than clip; scrollbar-width:none keeps it clean */
          max-height: calc(100vh - 104px); overflow-y: auto; scrollbar-width: none;
        }
        .descent::-webkit-scrollbar { display: none; }
        .descent-rail { flex-direction: column; gap: 18px; position: relative; padding: 4px 0; align-items: center; }
        /* the spine: a muted line, lit gold from the top down to the current stop */
        .descent-rail::before {
          content: ""; position: absolute; left: 50%; top: 6px; bottom: 6px;
          width: 2px; background: var(--rule); transform: translateX(-50%);
          border-radius: 1px; z-index: 0;
        }
        .descent-rail::after {
          content: ""; position: absolute; left: 50%; top: 6px;
          height: calc(var(--progress, 0) * (100% - 12px));
          width: 2px; transform: translateX(-50%); border-radius: 1px; z-index: 0;
          background: linear-gradient(var(--gold), var(--accent-lit));
          box-shadow: 0 0 7px rgba(var(--accent-rgb), 0.5);
          transition: height 0.4s ease;
        }
        /* on an advance the fill SWEEPS from its old level — the rail is rebuilt
           each render, so a keyframe from `--progress-from` does the travel */
        .descent-rail.rail-advance::after { animation: rail-fill 0.55s cubic-bezier(0.2, 0.8, 0.2, 1); }
        @keyframes rail-fill { from { height: calc(var(--progress-from, 0) * (100% - 12px)); } }
        .dstop, .dnode { z-index: 1; }
        .descent-label { order: -1; }
        .dnode.best::after { left: -9px; top: 50%; width: 5px; height: 1px; transform: translateY(-50%); }
      }

      /* ── full route-map overlay (opened from the rail/ribbon, any size) ── */
      .routemap-scrim {
        position: fixed; inset: 0; z-index: 30; display: flex; flex-direction: column;
        align-items: center; justify-content: flex-start;
        background: rgba(6, 5, 4, 0.82); backdrop-filter: blur(2px);
        animation: mapfade 220ms ease;
      }
      @keyframes mapfade { from { opacity: 0; } to { opacity: 1; } }
      .routemap {
        position: relative; margin: 0 auto; width: min(440px, 92vw);
        max-height: 92vh; display: flex; flex-direction: column;
      }
      .routemap-head {
        display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
        padding: 18px 6px 12px;
      }
      .routemap-head h2 { margin: 0; }
      .routemap-close {
        cursor: pointer; background: none; border: 1px solid var(--rule); border-radius: var(--radius);
        color: var(--ink); font: inherit; font-size: 20px; line-height: 1; padding: 6px 12px;
      }
      .routemap-close:hover { border-color: var(--gold); }
      .routemap-scroll {
        overflow-y: auto; overscroll-behavior: contain; border: 1px solid var(--edge);
        border-radius: var(--radius-lg); background: var(--routemap-bg);
        box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.5), 0 18px 50px rgba(0, 0, 0, 0.5);
      }
      .routemap-scroll svg { display: block; width: 100%; height: auto; }
      .routemap-legend { display: flex; flex-wrap: wrap; gap: 8px 16px; padding: 12px 6px 18px; color: var(--dim); font-size: 12.5px; }
      .routemap-legend span { display: inline-flex; align-items: center; gap: 6px; }
      .routemap-legend .sigil { width: 17px; height: 17px; color: var(--gold); }

      /* ── character sheet overlay (opened from the status/health bar) ── */
      .sheet-scrim {
        position: fixed; inset: 0; z-index: 40; display: flex;
        align-items: center; justify-content: center; padding: 20px;
        background: rgba(6, 5, 4, 0.82); backdrop-filter: blur(2px);
        animation: mapfade 220ms ease;
      }
      .charsheet {
        position: relative; width: min(460px, 94vw); max-height: 90vh;
        overflow-y: auto; overscroll-behavior: contain;
        background: linear-gradient(180deg, var(--panel), var(--panel-2));
        border: 1px solid var(--edge); border-radius: var(--radius);
        box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(var(--accent-rgb), 0.05);
        padding: 22px 24px 24px;
      }
      .charsheet-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
      .charsheet-head h2 { margin: 0; border: none; padding: 0; }
      .charsheet-close {
        cursor: pointer; background: none; border: none; border-radius: var(--radius);
        color: var(--ink); font: inherit; font-size: 20px; line-height: 1; padding: 4px 11px;
      }
      .charsheet-close:hover, .charsheet-close:focus-visible { color: var(--gold); }
      .cs-sub { color: var(--dim); font-size: 13px; margin: 2px 0 16px; }
      .cs-vitals {
        display: grid; grid-template-columns: repeat(auto-fit, minmax(76px, 1fr));
        gap: 8px; margin-bottom: 6px;
      }
      .cs-vital { padding: 8px 6px; text-align: center; border: 1px solid var(--rule); border-radius: var(--radius-sm); background: rgba(0, 0, 0, 0.15); }
      .cs-vnum { font-size: 22px; color: var(--gold); line-height: 1.1; font-variant: tabular-nums; }
      .cs-vcap { font-size: 11px; color: var(--dim); font-variant: small-caps; letter-spacing: 0.04em; margin-top: 2px; }
      .cs-label { color: var(--dim); font-size: 12px; font-variant: small-caps; letter-spacing: 0.06em; margin: 18px 0 6px; }
      .cs-statrow { display: flex; align-items: center; gap: 12px; padding: 5px 0; }
      .cs-statrow .alloc-name { width: 84px; color: var(--ink); font-variant: small-caps; letter-spacing: 0.03em; }
      .cs-statrow.locked { opacity: 0.45; }
      .cs-statrow .alloc-val { width: 22px; text-align: center; font-size: 18px; color: var(--gold); }
      .cs-statrow.locked .alloc-val { color: var(--dim); }
      .cs-statrow .alloc-pips { margin-left: auto; }
      .cs-items { color: var(--ink); font-size: 14px; line-height: 1.7; }
      .cs-empty { color: var(--dim); font-style: italic; font-size: 14px; }
      .charsheet .row-buttons { margin-top: 20px; }

      /* ── build allocation ── */
      .alloc-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; }
      .alloc-row.locked { opacity: 0.5; }
      /* The left columns (name · −/value/+) MUST NOT shrink: a long right-side
         note would otherwise flex-shrink them by different amounts per row, so
         the steppers and values wouldn't line up vertically down the column. */
      .alloc-name { width: 90px; flex: 0 0 auto; color: var(--ink); font-variant: small-caps; letter-spacing: 0.03em; }
      .alloc-val { width: 28px; flex: 0 0 auto; text-align: center; font-size: 20px; color: var(--gold); }
      .alloc-row.locked .alloc-val { color: var(--dim); }
      .alloc-note { color: var(--dim); font-size: 13px; font-style: italic; text-align: right; min-width: 0; }
      /* a pip gauge of the stat level — anchored right, so every row balances
         (every row's note/pips anchor to the same right edge, locked or live —
         incl. the unlocked Vigor row, whose hp readout would otherwise sit
         flush against the stepper) */
      .alloc-pips { margin-left: auto; display: flex; gap: 7px; flex: 0 0 auto; }
      .alloc-row .alloc-note { margin-left: auto; }
      .pip {
        width: 10px; height: 10px; border-radius: var(--radius-round);
        background: var(--surface-sunk); border: 1px solid var(--rule);
      }
      .pip.on { background: var(--gold); border-color: var(--accent-lit); box-shadow: 0 0 6px rgba(var(--accent-rgb), 0.4); }
      /* the reserve tally pulses gold as a held point is banked into the build */
      .reserve-count.spent { display: inline-block; color: var(--gold); animation: reserve-spend 0.6s ease; }
      @keyframes reserve-spend {
        0% { transform: scale(1); text-shadow: 0 0 0 rgba(240, 210, 130, 0); }
        40% { transform: scale(1.5); text-shadow: 0 0 14px rgba(240, 210, 130, 0.95); }
        100% { transform: scale(1); text-shadow: none; }
      }
      /* a freshly-allocated point POPS — the scarcity budget made tactile */
      .pip.just { animation: pip-pop 0.42s cubic-bezier(0.2, 0.8, 0.2, 1); }
      @keyframes pip-pop {
        0% { transform: scale(0.2); box-shadow: 0 0 0 rgba(240, 210, 130, 0); }
        50% { transform: scale(1.5); box-shadow: 0 0 12px rgba(240, 210, 130, 0.95); }
        100% { transform: scale(1); box-shadow: 0 0 6px rgba(200, 146, 58, 0.4); }
      }
      .step { width: 44px; flex: 0 0 auto; min-height: 44px; padding: 6px 0; text-align: center; font-size: 18px; }
      .row-buttons { display: flex; gap: 12px; margin-top: 18px; }
      /* The descent CTA sits OUTSIDE the build panel — the deliberate step from
         "set your hero" to "walk into the keep" — so it's full-width and large to
         carry that weight. */
      .descend-cta {
        display: block; width: 100%; margin-top: 18px;
        padding: 16px 20px; font-size: 19px; letter-spacing: 0.06em;
      }
      .restart-row {
        display: flex; align-items: center; justify-content: space-between;
        gap: 12px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--rule);
      }
      .danger { color: var(--dim); border-color: var(--edge); font-size: 14px; }
      .danger:hover:not(:disabled) { border-color: var(--blood); color: #d98c8c; }
      .presets { display: grid; grid-template-columns: repeat(auto-fill, minmax(155px, 1fr)); gap: 8px; margin-bottom: 16px; }
      .preset { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: baseline; padding: 8px 12px; }
      .preset.locked { opacity: 0.5; }

      /* ── between-run loadout (the coin shop on creation) ── */
      .loadout { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--rule); }
      .loadout-head { display: flex; align-items: baseline; justify-content: space-between; font-weight: 600; margin-bottom: 10px; }
      .loadout-head .coins { color: var(--gold); font-variant-numeric: tabular-nums; }
      /* Every loadout option is a uniform two-line tile (name over note), so the
         grid stays even whether a kind shows its cost or a "locked at difficulty N"
         note — no row is taller than another. */
      .loadout-item { flex-direction: column; flex-wrap: nowrap; align-items: flex-start; justify-content: flex-start; gap: 3px; }
      .loadout-item .li-name { font-weight: 600; }
      .loadout-item .alloc-note { margin: 0; }
      .loadout-item.selected { border-color: var(--gold); box-shadow: inset 0 0 0 1px var(--gold); color: var(--gold); }

      /* ── the Chronicle (personal leaderboard) ── */
      /* A Back button at the TOP-left of the screen (the boards can run long on a
         phone, so the footer Back is a scroll away); the headings stay centred
         below it. Its own line avoids overlapping the large centred title. */
      .lb-header { display: flex; flex-direction: column; }
      /* The top and footer Back buttons share one style so they match in height,
         width and weight; only the top one's placement (flush-left in the header)
         differs. */
      .lb-back {
        cursor: pointer; background: none; border: 1px solid var(--rule); border-radius: var(--radius);
        color: var(--ink); font: inherit; font-size: 15px; line-height: 1; padding: 8px 14px;
        white-space: nowrap;
      }
      .lb-back-top { align-self: flex-start; margin-bottom: 12px; }
      .lb-back:hover, .lb-back:focus-visible { border-color: var(--gold); color: var(--gold); }
      .lb-summary {
        display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
        gap: 10px; text-align: center;
      }
      .lb-card { padding: 10px 6px; border: 1px solid var(--rule); border-radius: var(--radius-sm); background: rgba(0,0,0,0.15); }
      .lb-num { font-size: 30px; color: var(--gold); line-height: 1.1; text-shadow: 0 0 14px rgba(var(--accent-rgb), 0.3); }
      .lb-cap { font-size: 12px; color: var(--dim); font-variant: small-caps; letter-spacing: 0.04em; margin-top: 2px; }
      .lb-tabs { display: flex; gap: 8px; margin: 4px 0 4px; }
      .lb-tab {
        flex: 0 0 auto; padding: 6px 18px; font-variant: small-caps; letter-spacing: 0.04em;
        color: var(--dim); background: transparent; border: 1px solid var(--edge); border-radius: var(--radius-sm);
      }
      .lb-tab.on { color: var(--gold); border-color: var(--gold); background: rgba(var(--accent-rgb), 0.08); }
      .lb-board { margin-top: 14px; }
      .lb-table { width: 100%; border-collapse: collapse; font-size: 15px; }
      .lb-table th {
        text-align: left; color: var(--dim); font-weight: normal; font-variant: small-caps;
        letter-spacing: 0.04em; font-size: 13px; padding: 4px 8px; border-bottom: 1px solid var(--rule);
      }
      .lb-table td { padding: 6px 8px; border-bottom: 1px solid var(--edge); color: var(--ink); }
      .lb-table tr:last-child td { border-bottom: none; }
      .lb-rank { color: var(--gold); width: 1.6em; text-align: right; font-variant: tabular-nums; }
      .lb-stat { color: var(--ink-bright); font-variant: tabular-nums; text-align: right; }
      .lb-build { color: var(--steel); font-variant: small-caps; letter-spacing: 0.02em; }
      .lb-when { color: var(--dim); font-size: 13px; }
      .lb-tag { font-size: 12px; font-variant: small-caps; letter-spacing: 0.03em; }
      .lb-tag.win { color: #8fb98f; }
      .lb-tag.lose { color: #d98c8c; }

      /* ── Codex (the keep's lore — a reader, not a board) ── */
      .codex { display: flex; flex-direction: column; gap: 16px; }
      .codex-entry { border-left: 2px solid var(--edge); padding: 2px 0 2px 14px; }
      .codex-entry h3 { margin: 0 0 4px; color: var(--gold); font-variant: small-caps; letter-spacing: 0.03em; }
      .codex-body { margin: 0 0 6px; color: var(--ink); line-height: 1.5; }
      .codex-meta { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; }
      .codex-tag { font-size: 12px; font-variant: small-caps; letter-spacing: 0.04em; color: var(--steel); border: 1px solid var(--edge); border-radius: 3px; padding: 0 6px; }
      .codex-tier { font-size: 12px; color: var(--dim); margin-left: auto; }

      /* ── forks ── */
      .cards { display: grid; gap: 12px; margin-top: 8px; }
      .card { text-align: left; padding: 16px; }
      /* a fork card wearing its route's scene (background-image set inline, with
         a scrim baked into it for legibility); min-height lets the art read */
      .card.art {
        background-size: cover; background-position: center; background-repeat: no-repeat;
        min-height: 96px; text-shadow: 0 1px 4px rgba(0, 0, 0, 0.85);
      }
      .card-name { font-size: 19px; color: var(--ink-bright); }
      .card-shape { color: var(--steel); font-size: 14px; margin-top: 3px; font-variant: small-caps; letter-spacing: 0.03em; }
      .card-read { color: var(--dim); font-size: 13px; margin-top: 6px; font-style: italic; }
      .card.hoard { border-color: var(--hoard-edge); background: var(--card-hoard-bg); }
      .card.hoard:hover:not(:disabled) { border-color: var(--gold); }
      /* a non-combat wayfarer route: a cool, parley-blue accent to set it apart
         from the warm fights and the gold hoard */
      .card.parley { border-color: var(--card-parley-border); background: var(--card-parley-bg); }
      .card.parley:hover:not(:disabled) { border-color: #8ab0c8; }
      /* an approach whose skill check the build doesn't meet: it will botch, so
         it's telegraphed with a warning tint (you may still choose to eat it) */
      .card.botch { border-color: #6a3a2a; }
      .card.botch .card-read { color: #cc9080; }

      /* ── combat ── */
      .foe-name { font-size: 25px; color: var(--ink-bright); letter-spacing: 0.02em; }
      .foe-shape { color: var(--steel); font-size: 14px; margin-top: 2px; font-variant: small-caps; letter-spacing: 0.03em; }
      /* The narrator's voice as a drop-capped manuscript passage. */
      .intro {
        font-style: italic; color: var(--ink); font-size: 17px; line-height: 1.7;
        margin: 14px 0; padding: 2px 0 2px 16px; border-left: 2px solid var(--rule);
      }
      .intro::first-letter {
        font-style: normal; float: left; font-size: 3.1em; line-height: 0.78;
        margin: 6px 10px 0 0; color: var(--gold); text-shadow: 0 0 14px rgba(var(--accent-rgb), 0.35);
      }
      /* the narration loads after the room is already playable */
      .intro-pending { color: var(--dim); }
      .intro-pending::first-letter { float: none; font-size: inherit; margin: 0; color: inherit; text-shadow: none; }
      .shimmer {
        background: linear-gradient(90deg, var(--dim) 0%, var(--ink-bright) 50%, var(--dim) 100%);
        background-size: 200% 100%;
        -webkit-background-clip: text; background-clip: text; color: transparent;
        animation: shimmer 1.7s linear infinite;
      }
      @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
      .intro-in { animation: fadein 500ms ease; }
      @keyframes fadein { from { opacity: 0; } to { opacity: 1; } }

      /* ── per-stage entrance ── every screen's panel rises softly into view on a
         stage change (added by the controller only when the view actually
         changes, so an in-combat re-render never replays it). */
      .panel.screen-in { animation: screen-in 380ms cubic-bezier(0.2, 0.8, 0.2, 1) both; }
      @keyframes screen-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
      .menace { display: flex; align-items: center; gap: 12px; margin: 14px 0 8px; }
      .menace-fill { height: 100%; background: linear-gradient(90deg, var(--hoard-edge), var(--gold)); box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.35); transition: width 0.3s ease; }
      /* The combat gauges (menace, intent) speak in the MANUSCRIPT voice — plain
         lowercase prose — by deliberate contrast with the engraved-chrome HUD
         (HP, DESCENT). See statusBar() in main.ts for the split. */
      .menace-num { color: var(--dim); font-size: 14px; min-width: 88px; }
      .intent { color: var(--steel); }
      /* Emphasis stays IN the manuscript voice: the dangerous blow's name is
         small-caps (not shouted in full caps), matching the rest of the prose. */
      .intent strong { font-variant: small-caps; letter-spacing: 0.06em; font-weight: 700; }
      /* maul wind-up telegraph: the readied blow breathes a red charge */
      .intent.maul {
        color: #e08a3c; font-weight: 600; letter-spacing: 0.02em;
        display: inline-block; padding: 2px 9px; margin: -2px -9px; border-radius: var(--radius);
        animation: maul-windup 1.15s ease-in-out infinite;
      }
      @keyframes maul-windup {
        0%, 100% { text-shadow: none; background: rgba(168, 49, 46, 0); box-shadow: 0 0 0 rgba(224, 138, 60, 0); }
        50% { text-shadow: 0 0 12px rgba(224, 138, 60, 0.7); background: rgba(168, 49, 46, 0.16); box-shadow: 0 0 16px rgba(224, 138, 60, 0.28); }
      }

      /* ── combat feedback — presentation only, fired from engine deltas the
         controller already sees (menace/hp/blunt/cancel), never affecting truth.
         Each effect lives on a DISTINCT element so two never share `animation`. ── */
      .foe.fx-hit { animation: fx-flinch 0.34s ease; }              /* foe recoils as it's hit */
      @keyframes fx-flinch { 0% { transform: none; } 28% { transform: translateX(5px); } 60% { transform: translateX(-3px); } 100% { transform: none; } }
      .menace-bar.fx-hit { animation: fx-menacejolt 0.42s ease; }   /* its gauge jolts as menace drops */
      @keyframes fx-menacejolt { 0% { box-shadow: 0 0 0 rgba(var(--accent-rgb), 0); } 40% { box-shadow: 0 0 13px rgba(var(--accent-rgb), 0.85); } 100% { box-shadow: 0 0 0 rgba(var(--accent-rgb), 0); } }
      .panel.combat.fx-hurt { animation: fx-shake 0.4s ease; }      /* the hero is struck: shake + red wash */
      @keyframes fx-shake { 0%, 100% { transform: none; } 20% { transform: translateX(-5px); } 45% { transform: translateX(5px); } 70% { transform: translateX(-3px); } 88% { transform: translateX(2px); } }
      .panel.combat.fx-hurt::after {
        content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
        background: radial-gradient(120% 100% at 50% 50%, rgba(168, 49, 46, 0) 45%, rgba(168, 49, 46, 0.3) 100%);
        animation: fx-hurtflash 0.5s ease forwards;
      }
      @keyframes fx-hurtflash { 0% { opacity: 0; } 16% { opacity: 1; } 100% { opacity: 0; } }
      .intent.fx-blunt { display: inline-block; color: var(--steel); animation: fx-deflect 0.4s ease; }  /* Charm cows the blow */
      @keyframes fx-deflect { 0%, 100% { transform: none; } 25% { transform: translateX(-4px); } 50% { transform: translateX(3px); } 75% { transform: translateX(-2px); } }
      .intent.fx-disarm { display: inline-block; animation: fx-disarm 0.5s ease; }  /* Finesse disarms the maul */
      @keyframes fx-disarm { 0% { transform: none; color: #e08a3c; } 30% { transform: scale(1.07); color: #bcd6e6; text-shadow: 0 0 12px rgba(110, 150, 180, 0.7); } 100% { transform: none; color: var(--steel); } }
      .hp-bar.fx-heal { animation: fx-heal 0.55s ease; }           /* a salve takes hold */
      @keyframes fx-heal { 0% { box-shadow: inset 0 1px 3px rgba(0,0,0,0.6); } 45% { box-shadow: 0 0 13px rgba(95, 125, 90, 0.9); } 100% { box-shadow: inset 0 1px 3px rgba(0,0,0,0.6); } }
      .hp-bar.fx-hurt-bar { animation: fx-hurtbar 0.5s ease; }     /* a blow lands: the gauge flares red */
      @keyframes fx-hurtbar { 0% { box-shadow: inset 0 1px 3px rgba(0,0,0,0.6); } 40% { box-shadow: 0 0 12px rgba(168,49,46,0.9); } 100% { box-shadow: inset 0 1px 3px rgba(0,0,0,0.6); } }

      /* ── Phase-1 game-feel: a bigger CRIT flinch on the open weakness, a reel
         badge, an expose flash, a whiffed-gamble fizzle, floating damage/heal
         numbers, and bars that DRAIN from their old level over a red ghost. All
         keyed off the same engine deltas, all purely cosmetic. ── */
      .foe { position: relative; }
      .foe.fx-crit { animation: fx-critflinch 0.5s cubic-bezier(0.2, 0.8, 0.2, 1); } /* the marquee blow */
      @keyframes fx-critflinch {
        0% { transform: none; } 18% { transform: translateX(10px) scale(1.04); }
        44% { transform: translateX(-6px) scale(0.99); } 72% { transform: translateX(3px); } 100% { transform: none; }
      }
      .crit-burst {
        position: absolute; inset: -8px; pointer-events: none; border-radius: 8px;
        background: radial-gradient(60% 60% at 50% 42%, rgba(212, 178, 90, 0.5), rgba(200, 146, 58, 0) 70%);
        animation: fx-critburst 0.6s ease forwards;
      }
      @keyframes fx-critburst { 0% { opacity: 0; transform: scale(0.7); } 25% { opacity: 1; transform: scale(1.05); } 100% { opacity: 0; transform: scale(1.28); } }
      .reel-burst {
        position: absolute; top: -2px; right: 0; pointer-events: none;
        color: #bcd6e6; font-variant: small-caps; letter-spacing: 0.06em; font-size: 13px; font-weight: 700;
        text-shadow: 0 0 10px rgba(110, 150, 180, 0.8);
        animation: fx-reel 0.9s ease forwards;
      }
      @keyframes fx-reel {
        0% { opacity: 0; transform: translateY(4px) rotate(-6deg); } 20% { opacity: 1; transform: translateY(0) rotate(4deg); }
        60% { transform: rotate(-3deg); } 100% { opacity: 0; transform: translateY(-7px); }
      }
      .foe-exposed.fx-expose { display: inline-block; animation: fx-expose 0.7s ease; }  /* the lock breaks open */
      @keyframes fx-expose {
        0% { color: #9ec48a; } 30% { color: #d8ffbf; text-shadow: 0 0 12px rgba(158, 196, 138, 0.9); transform: scale(1.08); }
        100% { color: #9ec48a; transform: none; }
      }
      .intent.fx-whiff { display: inline-block; color: #8a8276; animation: fx-whiff 0.55s ease; }  /* a gamble that didn't land */
      @keyframes fx-whiff {
        0% { transform: none; opacity: 1; } 25% { transform: translateX(-3px) skewX(-4deg); opacity: 0.45; }
        50% { transform: translateX(3px) skewX(3deg); opacity: 0.85; } 75% { transform: translateX(-2px); opacity: 0.5; } 100% { transform: none; opacity: 1; }
      }
      /* floating numbers (damage off the menace bar, heal/hurt off the hp bar) */
      .menace { position: relative; }
      .dmg-float {
        position: absolute; left: 50%; top: -4px; pointer-events: none;
        font-weight: 700; font-size: 20px; font-variant-numeric: tabular-nums;
        color: var(--steel); text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
        animation: fx-floatup 0.95s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
      }
      .dmg-float.crit { color: var(--gold); font-size: 27px; text-shadow: 0 0 13px rgba(200, 146, 58, 0.85); }
      .hp { position: relative; }
      .hp-float {
        position: absolute; right: 0; top: -17px; pointer-events: none;
        font-weight: 700; font-size: 18px; font-variant-numeric: tabular-nums;
        animation: fx-floatup-r 0.95s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
      }
      .hp-float.heal { color: var(--vital); text-shadow: 0 0 10px rgba(95, 125, 90, 0.85); }
      .hp-float.hurt { color: var(--blood); text-shadow: 0 0 10px rgba(168, 49, 46, 0.85); }
      @keyframes fx-floatup {
        0% { opacity: 0; transform: translate(-50%, 8px) scale(0.6); }
        18% { opacity: 1; transform: translate(-50%, -2px) scale(1.18); }
        40% { transform: translate(-50%, -11px) scale(1); }
        100% { opacity: 0; transform: translate(-50%, -36px) scale(1); }
      }
      @keyframes fx-floatup-r {
        0% { opacity: 0; transform: translateY(8px) scale(0.6); }
        18% { opacity: 1; transform: translateY(-2px) scale(1.18); }
        40% { transform: translateY(-11px) scale(1); }
        100% { opacity: 0; transform: translateY(-36px) scale(1); }
      }
      /* bars that DRAIN from the old level, with a red ghost marking the loss */
      .menace-fill, .hp-fill { position: relative; z-index: 1; }
      .menace-fill.fx-drain, .hp-fill.fx-drain { animation: fx-drain 0.5s cubic-bezier(0.2, 0.8, 0.2, 1); }
      @keyframes fx-drain { from { width: var(--from); } }
      .menace-ghost, .hp-ghost {
        position: absolute; left: 0; top: 0; height: 100%; z-index: 0;
        background: linear-gradient(90deg, #5a201d, #a8312e);
        animation: fx-ghostfade 0.65s ease forwards;
      }
      @keyframes fx-ghostfade { 0% { opacity: 0.85; } 100% { opacity: 0; } }
      .tools { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-top: 18px; }
      .tool { display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; align-items: baseline; text-align: left; padding: 12px 14px; }
      .tool-key { grid-row: span 2; font-size: 20px; color: var(--gold); }
      .tool-name { font-size: 17px; font-variant: small-caps; letter-spacing: 0.03em; }
      .tool-dmg { color: var(--ink); }
      /* The tool's energy cost — the certainty/economy axis: a reliable answer
         (Force/Wits) costs 2, a chancy rider (Charm/Finesse) costs 1. */
      .tool-cost { color: var(--gold); font-variant-numeric: tabular-nums; font-size: 13px; }
      .tool-rider { grid-column: 2; color: var(--dim); font-size: 13px; }
      /* The shown odds a gambled rider (a sub-mastery Charm/Finesse) lands. */
      .tool-odds {
        margin-left: 4px;
        padding: 0 5px;
        border-radius: var(--radius);
        font-size: 11px;
        font-variant-numeric: tabular-nums;
        color: #d8b25a;
        background: rgba(176, 138, 58, 0.18);
      }
      .log { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--rule); color: var(--dim); font-size: 14px; }
      .log:empty { display: none; }   /* no rule + gap before any blow has landed */
      .log div { padding: 1px 0; }
      /* The undefendable clock — a sickly, festering green, set apart from the
         blow's numbers so it reads as a thing your mitigation cannot touch. */
      .rot {
        display: inline-block;
        margin-left: 5px;
        padding: 0 5px;
        border-radius: var(--radius);
        font-size: 0.82em;
        font-weight: 600;
        letter-spacing: 0.02em;
        color: #a9c46a;
        background: rgba(90, 110, 40, 0.22);
      }
      /* Champion-affix telegraph chips on the foe line — steel for armour, a
         locked amber for a guarded (un-exposed) weakness, green once exposed. */
      .foe-armour  { color: #b9c2cc; font-weight: 600; }
      .foe-guarded { color: #d8a24a; font-weight: 600; }
      .foe-exposed { color: #9ec48a; font-weight: 600; }
      .item-key { color: var(--gold); font-size: 14px; margin-right: 2px; }
      .key-hint {
        display: inline-block; margin-left: 8px; padding: 1px 8px;
        border: 1px solid var(--rule); border-radius: var(--radius);
        color: var(--dim); font-size: 12px; font-variant: small-caps; letter-spacing: 0.04em;
      }
      .energy-out { margin-top: 12px; color: #c9a06a; font-style: italic; font-size: 14px; }

      /* ── onboarding glossary (native <details>) ── */
      .legend { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--rule); }
      .legend > summary {
        cursor: pointer; color: var(--steel); font-size: 13px;
        font-variant: small-caps; letter-spacing: 0.05em; list-style: none;
      }
      .legend > summary::-webkit-details-marker { display: none; }
      .legend[open] > summary { color: var(--gold); }
      .gloss { margin-top: 10px; display: grid; gap: 7px; }
      .gloss-row { display: grid; grid-template-columns: 92px 1fr; gap: 10px; align-items: baseline; }
      .gloss-key { color: var(--gold); font-variant: small-caps; letter-spacing: 0.03em; font-size: 14px; }
      .gloss-def { color: var(--dim); font-size: 13px; }
      /* The primer lives inside the centred creation header; force its prose +
         bullets left so the list markers line up instead of centring. */
      .primer-body { margin-top: 10px; color: var(--dim); font-size: 13px; line-height: 1.55; text-align: left; }
      .primer-body p { margin: 0 0 8px; }
      .primer-body b { color: var(--steel); font-weight: 600; }
      .primer-body ul { margin: 0; padding-left: 18px; display: grid; gap: 5px; }

      /* ── boss: the leaf darkens, the wound-light deepens ── */
      body.boss-fight .panel.combat {
        border-color: #5a2420;
        box-shadow:
          0 12px 40px rgba(0, 0, 0, 0.7),
          inset 0 0 70px rgba(120, 20, 20, 0.14);
      }
      body.boss-fight .foe-name { color: #e8c79a; text-shadow: 0 0 18px rgba(168, 49, 46, 0.4); }

      /* ── loot & items ── */
      .loot-toast {
        margin-top: 12px; padding: 10px 14px; border-radius: var(--radius);
        background: var(--loot-bg); border: 1px solid var(--gold); color: var(--loot-ink);
      }
      .inv { margin-top: 10px; color: var(--steel); font-size: 14px; }
      .label { margin-top: 16px; color: var(--dim); font-size: 13px; font-variant: small-caps; letter-spacing: 0.06em; }
      .items { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin-top: 6px; }
      .item { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; text-align: left; padding: 10px 14px; border-color: var(--item-border); }
      .item:hover:not(:disabled) { border-color: var(--gold); }
      .item-name { color: var(--ink-bright); }
      .item-eff { color: var(--dim); font-size: 13px; }

      /* ── run end ── */
      .end h2 { font-size: 26px; }
      .end.win h2 { color: #d8b25a; }
      .end.lose h2 { color: #b04a4a; }
      /* ── Phase-3 run-end tonal divergence: a win is GILDED and rises with a gold
         glow; a fall DESATURATES and sinks into settling ash. The body washes are
         full-screen overlays behind the panel; the ember field surges/settles via
         sparkMood (see startSparks). ── */
      .end.win { animation: end-win-rise 0.9s cubic-bezier(0.16, 1, 0.3, 1) both; }
      @keyframes end-win-rise {
        0% { opacity: 0; transform: translateY(14px) scale(0.98); }
        100% { opacity: 1; transform: none; }
      }
      .end.win h2 { text-shadow: 0 0 18px rgba(216, 178, 90, 0.55); }
      .end.lose { animation: end-lose-sink 1.1s ease both; }
      @keyframes end-lose-sink {
        0% { opacity: 0; transform: translateY(-6px); filter: saturate(1); }
        100% { opacity: 1; transform: none; filter: saturate(0.62) brightness(0.92); }
      }
      /* full-screen washes keyed off the body class set in applyAtmosphere */
      body.run-won::after, body.run-fell::after {
        content: ""; position: fixed; inset: 0; z-index: 2; pointer-events: none;
      }
      body.run-won::after {
        background: radial-gradient(120% 90% at 50% 108%, rgba(216, 178, 90, 0.22), rgba(216, 178, 90, 0) 60%);
        animation: wash-in 1.2s ease both;
      }
      body.run-fell::after {
        background: radial-gradient(130% 100% at 50% 50%, rgba(10, 8, 7, 0) 30%, rgba(6, 5, 4, 0.55) 100%);
        animation: wash-in 1.6s ease both;
      }
      @keyframes wash-in { from { opacity: 0; } to { opacity: 1; } }
      .chronicle { margin: 16px 0; padding-top: 12px; border-top: 1px solid var(--rule); color: var(--dim); font-size: 14px; }
      .chronicle div { padding: 1px 0; }

      /* ── small screens ── */
      @media (max-width: 480px) {
        #app { padding: 18px 14px 48px; }
        h1 { font-size: 34px; }
        .panel { padding: 18px 16px; }
        .status { flex-wrap: wrap; }
        .status-meta { text-align: left; }
        .tools { grid-template-columns: 1fr 1fr; }
        /* Tighten the allocation row so the right-side note keeps enough width to
           stay within two short lines — otherwise it wraps tall and the rows lose
           their even height (the steppers/values are already shrink-locked, so the
           columns stay aligned; this keeps the ROW heights even too). */
        .alloc-row { gap: 8px; }
        .alloc-name { width: 58px; }
        .alloc-note { font-size: 12px; }
        /* The teaser's build column is the first to crowd a phone row — drop it
           and keep hero + the deepest mark, which carry the meaning. */
        .ct-build { display: none; }
      }

      /* ── Variance swing chip: makes a rolled blow/cow legible AS a swing ──
         The number it sits beside already includes the swing (full information);
         the chip colours by good-for-you, not by raw sign. */
      .swing {
        display: inline-block;
        margin-left: 5px;
        padding: 0 5px;
        border-radius: var(--radius);
        font-size: 0.82em;
        font-weight: 600;
        letter-spacing: 0.02em;
        animation: swing-pop 0.32s ease;
      }
      .swing-bad  { color: #e7a06a; background: rgba(176, 70, 46, 0.20); }
      .swing-good { color: #9ec48a; background: rgba(95, 125, 90, 0.22); }
      @keyframes swing-pop { 0% { transform: scale(0.6); opacity: 0; } 60% { transform: scale(1.12); } 100% { transform: none; opacity: 1; } }

      /* ── Enter-room reveal: a full-screen NAME callout of the foe ──
         Pure presentation over an already-rendered (interactive) combat screen;
         dismisses on its decisive exit, a tap, or any key. Reduced to just the
         foe's name (the full telegraph lives on the combat screen below).
         Shape (grounded in how games show boss/title cards — enter with a
         flourish, HOLD legibly, then exit decisively, not a slow dissolve):
           · enter   — the name slides up + sharpens, a gold streak draws under it
           · HOLD    — a long, still beat so the name is actually readable (~1.3s)
           · exit    — the card LIFTS away (ease-in, departing) while the scrim
                       clears quickly behind it: motion-led, not a uniform fade */
      .reveal {
        position: fixed; inset: 0; z-index: 50;
        display: grid; place-items: center;
        background: radial-gradient(120% 90% at 50% 42%, rgba(20, 14, 10, 0.84), rgba(6, 5, 4, 0.96));
        backdrop-filter: blur(2px);
        cursor: pointer;
        animation: revealGone 2.6s ease forwards;
      }
      /* Cover instantly (opaque from the first frame) so the combat screen being
         built behind it never flashes through; hold fully, then clear briskly. */
      @keyframes revealGone { 0%, 88% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
      .reveal-card {
        text-align: center; padding: 24px; max-width: 92%;
        /* Holds dead still through the read, then lifts away at the very end
           (ease-in = accelerating departure). The scrim fade above is timed a
           hair later so the card leads the exit. */
        animation: reveal-card-exit 2.6s ease-in both;
      }
      @keyframes reveal-card-exit {
        0%, 86% { transform: none; opacity: 1; }
        100% { transform: translateY(-26px); opacity: 0; }
      }
      /* The name slides up into focus — blurred, then sharpening to a stop, with
         a gold streak sweeping in beneath it as it lands. NB: only transform /
         opacity / filter animate (visual-only, never reflow). letter-spacing is
         deliberately NOT animated — it changes the text's real width, so a name
         that wraps would re-flow its line count mid-reveal (e.g. 3 lines → 2),
         which reads as the text jumping after it settles. */
      .reveal-name {
        position: relative; display: inline-block;
        font-size: 48px; color: var(--ink-bright); letter-spacing: 0.03em;
        text-shadow: 0 2px 34px rgba(var(--accent-rgb), 0.38);
        opacity: 0; animation: reveal-name 0.72s cubic-bezier(0.16, 1, 0.3, 1) 0.05s forwards;
      }
      .reveal-name::after {
        content: ""; position: absolute; left: 0; right: 0; bottom: -12px; height: 2px;
        background: linear-gradient(90deg, transparent, var(--gold) 18%, var(--gold) 82%, transparent);
        box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.6);
        transform: scaleX(0); transform-origin: center;
        animation: reveal-streak 0.55s cubic-bezier(0.2, 0.8, 0.2, 1) 0.42s forwards;
      }
      .reveal-boss { background: radial-gradient(120% 90% at 50% 42%, rgba(34, 12, 12, 0.9), rgba(8, 4, 4, 0.98)); }
      .reveal-boss .reveal-name { color: #e8c79a; text-shadow: 0 0 30px rgba(168, 49, 46, 0.55); }
      .reveal-boss .reveal-name::after {
        background: linear-gradient(90deg, transparent, #c8612e 18%, #c8612e 82%, transparent);
        box-shadow: 0 0 14px rgba(200, 70, 46, 0.7);
      }
      @keyframes reveal-name {
        0%   { opacity: 0; transform: translateY(22px) scale(0.94); filter: blur(7px); }
        55%  { opacity: 1; filter: blur(0); }
        100% { opacity: 1; transform: none; filter: blur(0); }
      }
      @keyframes reveal-streak { to { transform: scaleX(1); } }

      /* ── victory beat: a room cleared ──────────────────────────────────────
         A short gold flourish over the next screen as a foe falls — the win as a
         MOMENT, not an instant cut. NON-BLOCKING (pointer-events:none): the next
         stage is live beneath it, and it lifts away on its own. A lighter scrim
         than the reveal so the screen behind it still reads through. */
      .victory {
        position: fixed; inset: 0; z-index: 48; pointer-events: none;
        display: grid; place-items: center;
        background: radial-gradient(120% 90% at 50% 44%, rgba(28, 22, 8, 0.46), rgba(8, 6, 3, 0.18));
        animation: victoryGone 1.5s ease forwards;
      }
      @keyframes victoryGone { 0%, 62% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
      .victory-card {
        text-align: center; padding: 24px; max-width: 92%;
        animation: victory-card-exit 1.5s ease-in both;
      }
      @keyframes victory-card-exit {
        0%, 58% { transform: none; opacity: 1; }
        100% { transform: translateY(-22px); opacity: 0; }
      }
      .victory-tag {
        font-variant: small-caps; letter-spacing: 0.28em; font-size: 14px;
        color: var(--gold); opacity: 0; animation: victory-tag 0.5s ease 0.05s forwards;
      }
      .victory-name {
        position: relative; display: inline-block; margin-top: 6px;
        font-size: 40px; color: var(--ink-bright); letter-spacing: 0.03em;
        text-shadow: 0 2px 30px rgba(var(--accent-rgb), 0.34);
        opacity: 0; animation: victory-name 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.04s forwards;
      }
      /* the killing strike, drawn across the name as it lands */
      .victory-name::after {
        content: ""; position: absolute; left: -4%; right: -4%; top: 52%; height: 3px;
        background: linear-gradient(90deg, transparent, var(--gold) 16%, var(--gold) 84%, transparent);
        box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.6);
        transform: scaleX(0); transform-origin: left center;
        animation: reveal-streak 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) 0.3s forwards;
      }
      .victory-boss .victory-name { color: #e8c79a; }
      @keyframes victory-tag { to { opacity: 0.85; } }
      @keyframes victory-name {
        0%   { opacity: 0; transform: translateY(14px) scale(0.96); filter: blur(5px); }
        60%  { opacity: 1; filter: blur(0); }
        100% { opacity: 1; transform: none; filter: blur(0); }
      }

      /* A tool too dear for the energy left this turn, or an item already spent:
         held in place, dimmed and struck out — the row never reflows mid-turn. */
      .tool.tool-dear .tool-cost { color: var(--danger, #c8612e); }
      .item.item-used .item-name { text-decoration: line-through; }
      .item.item-used .item-eff { font-style: italic; }

      /* ── respect reduced-motion: keep the light, drop the flicker/beat ── */
      @media (prefers-reduced-motion: reduce) {
        .ambient.torch,
        body.critical .ambient.blood,
        .dnode.current,
        .dnode.arrived, .dnode.boss.arrived, .ribbon-cur.arrived, .descent-rail.rail-advance::after,
        .routemap-scrim,
        .shimmer,
        .intro-in,
        .panel.screen-in,
        .intent.maul,
        .foe.fx-hit, .menace-bar.fx-hit, .panel.combat.fx-hurt,
        .intent.fx-blunt, .intent.fx-disarm, .hp-bar.fx-heal,
        .foe.fx-crit, .crit-burst, .reel-burst, .foe-exposed.fx-expose,
        .intent.fx-whiff, .hp-bar.fx-hurt-bar,
        .menace-fill.fx-drain, .hp-fill.fx-drain,
        .menace-ghost, .hp-ghost, .dmg-float, .hp-float,
        .pip.just, .reserve-count.spent, .end.win, .end.lose, body.run-won::after, body.run-fell::after,
        .swing,
        .reveal-card,
        .reveal-name, .reveal-name::after,
        .victory-card, .victory-tag, .victory-name, .victory-name::after { animation: none; }
        /* the reveal stays (the scrim's opacity fade is motion-free) but the card
           is static: no lift, and the name shows at rest with its streak drawn */
        .reveal-card { transform: none; opacity: 1; }
        .reveal-name { opacity: 1; filter: none; transform: none; letter-spacing: 0.03em; }
        .reveal-name::after { transform: scaleX(1); }
        /* the victory beat shows at rest (its strike drawn) and the safety-net
           timer removes it — its only motion was the card lift/name slide */
        .victory-card { transform: none; opacity: 1; }
        .victory-tag { opacity: 0.85; }
        .victory-name { opacity: 1; filter: none; transform: none; }
        .victory-name::after { transform: scaleX(1); }
        .panel.combat.fx-hurt::after { display: none; }
        /* the transient overlays carry information only via motion — with motion
           off they would just linger over the next render, so drop them entirely */
        .crit-burst, .reel-burst, .dmg-float, .hp-float, .menace-ghost, .hp-ghost { display: none; }
        .shimmer { color: var(--dim); -webkit-text-fill-color: var(--dim); }
        /* keep the shadow/border hover cues, drop the motion (the lift) */
        .card:hover:not(:disabled), .tool:hover:not(:disabled), .item:hover:not(:disabled),
        .preset:hover:not(:disabled), .primary:hover:not(:disabled),
        button:active:not(:disabled) { transform: none; }
        * { transition: none !important; }
      }
