﻿/* Mini-game overlay + HUD. Reuses the site's palette (--gold-*,
   --cream, --ink) and fonts (Cinzel, Rajdhani) so gameplay reads as
   an extension of the battlefield rather than a foreign UI. Sits
   above .page (z:1), below .topbar (z:3) and .dock (z:5) so those
   remain interactive as escape hatches. */

/* Fullscreen overlay wrapper. Fades in/out via .is-visible. Contains
   the dirt-road shoulder texture (visible on desktop where the stage
   is narrower than the viewport, or as thin top/bottom strips on
   ultra-tall phones). */
.game-overlay {
  position: fixed;
  inset: 0;
  z-index: 2;
  opacity: 0;
  pointer-events: none;
  transition: opacity 320ms ease;
  display: grid;
  place-items: center;
  overflow: hidden;
  /* Layered dirt-road: base warm-brown gradient + SVG turbulence for
     gravel/grain + subtle vignette darker toward outer edges to fake
     depth. The SVG layer is what animates via background-position for
     the scrolling loop; base + vignette stay put so only the texture
     moves, which reads as forward motion without eye strain. */
  background-color: #2f1e10;
  background-image:
    linear-gradient(90deg,
      rgba(0, 0, 0, 0.55) 0%,
      rgba(0, 0, 0, 0.15) 20%,
      transparent 32%,
      transparent 68%,
      rgba(0, 0, 0, 0.15) 80%,
      rgba(0, 0, 0, 0.55) 100%),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='320'><defs><filter id='d'><feTurbulence type='fractalNoise' baseFrequency='1.4' numOctaves='2' seed='7'/><feColorMatrix values='0.18 0 0 0 0.24  0.10 0 0 0 0.16  0.05 0 0 0 0.08  0 0 0 0.55 0'/></filter></defs><rect width='100%25' height='100%25' filter='url(%23d)'/></svg>"),
    radial-gradient(ellipse at 50% 40%, #4a3220 0%, #3a2818 55%, #251610 100%);
  background-size: 100% 100%, 240px 320px, 100% 100%;
  background-repeat: no-repeat, repeat, no-repeat;
  /* `--road-scroll` is updated per-tick by the runner's game loop
     (index.js) from world.scrollY, so the shoulder texture moves in
     lockstep with the obstacles. When no game is running, or the
     value hasn't been set yet, the road is stationary. */
  background-position: 0 0, 0 var(--road-scroll, 0px), 0 0;
}
@media (prefers-reduced-motion: reduce) {
  .game-overlay { background-position: 0 0, 0 0, 0 0; }
}
.game-overlay.is-visible {
  opacity: 1;
  pointer-events: auto;
}
.game-overlay[hidden] { display: none; }

/* Playfield frame — locks the game to a 9:16 phone aspect ratio,
   centered in the overlay. Height-driven; width derives from aspect.
   `max-width: 100dvw` clamps on portrait phones narrower than 9:16
   so the stage fills the width and letterboxes vertically instead of
   overflowing. `dvh` matches the visible viewport including mobile
   URL-bar chrome collapse, avoiding a resize jump mid-game. */
.game-stage {
  position: relative;
  aspect-ratio: 9 / 16;
  height: 100dvh;
  max-height: 100dvh;
  max-width: 100dvw;
  overflow: hidden;
  /* Establish this element as a size CONTAINER so every child can
     size itself relative to the STAGE, not the browser viewport.
     Fixes the "big desktop window resized short" case: with vw-based
     text on a stage that's now only 200 px wide, text would render
     at browser-viewport scale (huge). cqi/cqb inside a container-
     query scope scale to the container's dimensions — text stays
     proportional to the stage no matter how the browser is sized. */
  container-type: size;
  container-name: stage;

  /* Fluid typography scale — one place to keep the whole game's
     type hierarchy consistent. cqi = 1% of container inline (width);
     clamp mins guarantee readability on tiny stages, maxes cap the
     largest desktop sizes. Reused by every text element inside the
     stage (title, HUD, cutscenes, tutorials, game-over). Change a
     number here and the whole game rescales at once. */
  --font-2xs:  clamp(8px,   1.8cqi, 11px);
  --font-xs:   clamp(9px,   2.2cqi, 12px);
  --font-sm:   clamp(10px,  2.8cqi, 14px);
  --font-base: clamp(12px,  3.4cqi, 16px);
  --font-md:   clamp(13px,  4.0cqi, 18px);
  --font-lg:   clamp(15px,  4.8cqi, 22px);
  --font-xl:   clamp(18px,  6.0cqi, 28px);
  --font-2xl:  clamp(22px,  8.0cqi, 40px);
  --font-3xl:  clamp(26px, 10.0cqi, 52px);
  --font-4xl:  clamp(56px, 22.0cqi, 160px);
  --font-4xl-sm: clamp(48px, 18.0cqi, 130px);

  /* Fluid spacing — same pattern applied to paddings + margins.
     Keeps rhythm consistent across the game's screens. */
  --pad-2xs: clamp(4px,   1.2cqi, 8px);
  --pad-xs:  clamp(6px,   1.8cqi, 12px);
  --pad-sm:  clamp(10px,  2.6cqi, 16px);
  --pad-md:  clamp(14px,  3.8cqi, 24px);

  /* Tutorial visual language — shared between the STEER cutscene
     (.tut__*) and the AVOID / COLLECT / BLASTER practice-overlay
     (.practice-overlay__*). Update these here to change the look of
     every tutorial header at once. */
  --tut-title-font: clamp(15px, 5cqi, 26px);
  --tut-hint-font:  clamp(9px,  2.8cqi, 14px);
  --tut-dot-size:   clamp(8px,  2.8cqi, 14px);
  --tut-dot-gap:    clamp(4px,  1.6cqi, 10px);
  --tut-icon-size:  clamp(20px, 7cqi, 40px);
  --tut-pill-pad-y: clamp(4px,  1.6cqi, 10px);
  --tut-pill-pad-x: clamp(9px,  3cqi, 18px);

  /* Bottom-edge layout stack — defines the hero-card row's footprint
     so ANY overlay that sits above the cards (tutorial action bar,
     future popups, etc.) can pin itself just above without a fixed
     magic-number offset. Everything down here scales with the stage.
     Height is derived from card WIDTH via aspect-ratio (see
     .game-hud__team-slot) so cards stay portrait-y square regardless
     of how tall the stage happens to be — cqi (width) is the honest
     measure for card size, not cqb (height). */
  /* A PHONE IS THE SMALL CASE, NOT THE SPECIAL ONE.
     The stage is 9:16 capped at the viewport, so a 390 px phone gets a
     ~390 px stage and a desktop gets ~510 — and at 11cqi with a 28 px
     floor the cards came out around 43 px on the phone, where they are
     also the primary way to switch hero mid-fight. The floor now
     guarantees a real touch target (52 px, just over the 44 px minimum
     both platform guidelines ask for) and the percentage is up so the
     gap between phone and desktop closes. Three cards plus gaps at the
     floor is ~170 px — comfortable inside any stage we render. */
  --hero-card-width:  clamp(52px, 14cqi, 78px);
  --hero-card-height: calc(var(--hero-card-width) * 1.08);
  --hero-card-bottom: clamp(4px,  1.2cqi, 12px);
  --above-heroes-gap: clamp(4px,  1.2cqi, 12px);
  /* Total distance from stage bottom to the TOP of the hero card row.
     Overlays that need to sit above the cards use
     calc(var(--above-heroes) + N) — see .coach__stack and .bx-actions.
     Note that several overlays land on this same line, which is why
     the tutorial measures before it settles rather than trusting it. */
  --above-heroes: calc(var(--hero-card-bottom) + var(--hero-card-height) + var(--above-heroes-gap));

  /* Subtle inner shadow to seat the playfield into the road frame. */
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35),
              0 0 24px rgba(0, 0, 0, 0.55);
  /* Kill browser scroll gestures anywhere over the stage — touch-action
     doesn't inherit, and the HUD wrapper is pointer-events:none so
     drag starts on empty HUD area would bubble here without this. */
  touch-action: none;
}

/* Both canvases fill the stage (not the overlay). Foreground redraws
   every frame; background at ~15fps. `touch-action: none` is the
   critical mobile fix — without it, the browser interprets touch
   drags as page-scroll gestures and the pointerdown events we listen
   for never fire on the canvas. `user-select: none` kills the
   long-press text-selection halo on iOS Safari. */
.game-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  touch-action: none;
  -webkit-user-select: none;
          user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.game-canvas--bg { z-index: 1; }
.game-canvas--fg { z-index: 2; }

/* Belt + braces: also disable native touch gestures on the whole
   overlay so any UI element that ends up as the initial touch target
   (during transitions, or the score bar) doesn't hand the gesture
   over to the browser before it reaches our handlers. Individual
   interactive HUD elements can opt back in with touch-action: auto. */
.game-overlay {
  touch-action: none;
}
.game-hud__audio,
.game-hud__audio-panel,
.game-hud__rules,
.game-hud__over-actions,
.game-hud__start,
.game-hud__rules-link {
  touch-action: auto;
}

/* HUD sits on top of everything inside the overlay. Wrapper is
   pointer-events: none so tap/drag events on empty HUD regions fall
   through to the canvas (crucial for touch steering). Interactive
   descendants — buttons, panels, screens — opt back in with
   pointer-events: auto in their own rules below. */
.game-hud {
  position: absolute;
  inset: 0;
  z-index: 3;
  color: var(--cream);
  font-family: "Rajdhani", sans-serif;
  pointer-events: none;
}

/* Audio settings dropdown, top-right corner (below the topbar which
   is z:3 globally, but the overlay is z:2, so the topbar always wins).
   Same circular button footprint as the old single-mute — the panel
   drops below it on click. */
.game-hud__audio {
  position: absolute;
  /* Pinned to the top-right CORNER — mirrors the TUTORIAL badge
     in the top-left. Sits above the tutorial header (which starts
     at top: 44 px min) so it's never covered on any stage size. */
  top:   clamp(8px, 2.2cqi, 16px);
  right: clamp(8px, 2.2cqi, 16px);
  pointer-events: auto;
  z-index: 6;
}

/* ===================================================================
   PAUSE — the phone's half of a control the keyboard already has.
   Immediately left of the audio button, same size and same idiom, so
   the corner reads as one cluster of session controls rather than as
   two unrelated buttons. Offset by the audio button's own width plus a
   gap, in the same clamp() so the two stay aligned at every stage size.
   =================================================================== */
.game-hud__pause-btn {
  position: absolute;
  top:   clamp(8px, 2.2cqi, 16px);
  right: calc(clamp(8px, 2.2cqi, 16px) + clamp(22px, 8cqi, 40px) + clamp(6px, 1.6cqi, 10px));
  appearance: none;
  width:  clamp(22px, 8cqi, 40px);
  height: clamp(22px, 8cqi, 40px);
  border-radius: 999px;
  border: 1px solid rgba(200, 205, 215, 0.28);
  background: rgba(6, 8, 12, 0.65);
  color: var(--cream);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  pointer-events: auto;
  z-index: 6;
  transition: border-color 180ms ease, background 180ms ease, color 180ms ease;
}
.game-hud__pause-btn:hover,
.game-hud__pause-btn.is-paused {
  border-color: rgba(246, 201, 91, 0.65);
  color: #fff5d6;
}
.game-hud__pause-btn[hidden] { display: none; }
.game-hud__pause-icon {
  width:  clamp(12px, 4cqi, 20px);
  height: clamp(12px, 4cqi, 20px);
  fill: currentColor;
}

/* The paused state. A full-stage scrim: it is the thing that stops a
   pocketed phone steering the car, so it must COVER the play area
   rather than sit beside it. */
.game-hud__pause {
  position: absolute;
  inset: 0;
  z-index: 65;          /* over the game and its HUD, under the summary */
  display: grid;
  place-items: center;
  pointer-events: auto;
  background: radial-gradient(ellipse at 50% 45%,
              rgba(14, 9, 4, 0.86) 0%, rgba(6, 4, 2, 0.94) 75%);
  backdrop-filter: blur(2px);
  animation: gamePauseIn 180ms ease-out both;
}
.game-hud__pause[hidden] { display: none; }
@keyframes gamePauseIn { from { opacity: 0; } to { opacity: 1; } }
.game-hud__pause-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(6px, 1.8cqb, 14px);
  padding: clamp(14px, 4cqb, 30px) clamp(18px, 6cqi, 40px);
  text-align: center;
  background: linear-gradient(180deg, rgba(38, 24, 12, 0.96), rgba(22, 13, 6, 0.97));
  border: 2px solid rgba(255, 200, 110, 0.4);
  border-radius: 14px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.75);
  max-width: min(92cqi, 420px);
}
.game-hud__pause-title {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-lg);
  letter-spacing: 0.22em;
  color: #ffe6b4;
  text-shadow: 0 0 18px rgba(255, 180, 80, 0.3);
}
.game-hud__pause-note {
  margin: 0;
  font-family: "Rajdhani", sans-serif;
  font-size: var(--font-xs);
  line-height: 1.4;
  color: rgba(244, 234, 214, 0.75);
}
.game-hud__pause-resume {
  appearance: none;
  cursor: pointer;
  /* A thumb target first and a button second: 44 px is the smallest
     thing a person reliably hits on a phone they are half looking at,
     which is every phone that is being un-paused. */
  min-height: 44px;
  padding: 0 clamp(18px, 5cqi, 34px);
  border-radius: 10px;
  border: 1px solid rgba(255, 210, 120, 0.55);
  background: linear-gradient(180deg, #f6c95b, #b9822a);
  color: #2a1806;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-sm);
  letter-spacing: 0.16em;
}
.game-hud__pause-resume:hover  { filter: brightness(1.08); }
.game-hud__pause-resume:active { transform: translateY(1px); }
.game-hud__pause-hint {
  margin: 0;
  font-family: "Rajdhani", sans-serif;
  font-size: var(--font-2xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(244, 234, 214, 0.45);
}
.game-hud__pause-hint[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
  .game-hud__pause { animation: none; }
}
.game-hud__audio-btn {
  appearance: none;
  width:  clamp(22px, 8cqi, 40px);
  height: clamp(22px, 8cqi, 40px);
  border-radius: 999px;
  border: 1px solid rgba(200, 205, 215, 0.28);
  background: rgba(6, 8, 12, 0.65);
  color: var(--cream);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color 180ms ease, background 180ms ease, color 180ms ease;
}
.game-hud__audio-btn:hover,
.game-hud__audio-btn[aria-expanded="true"] {
  border-color: rgba(246, 201, 91, 0.65);
  color: #fff5d6;
}
.game-hud__audio-icon {
  width:  clamp(12px, 4cqi, 20px);
  height: clamp(12px, 4cqi, 20px);
  fill: currentColor;
}
.game-hud__audio-off { display: none; }
.game-hud__audio-btn.is-muted .game-hud__audio-on  { display: none; }
.game-hud__audio-btn.is-muted .game-hud__audio-off { display: inline; }

/* Dropdown panel below the button — right-aligned so it never
   overflows the viewport. Same steel/gold aesthetic as the Discord
   presence popover. */
.game-hud__audio-panel {
  position: absolute;
  top: calc(100% + var(--pad-2xs));
  right: 0;
  min-width: clamp(120px, 40cqi, 180px);
  background: rgba(8, 10, 14, 0.98);
  border: 1px solid rgba(246, 201, 91, 0.32);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
  padding: var(--pad-xs) var(--pad-2xs);
  z-index: 5;
}
.game-hud__audio-panel[hidden] { display: none; }
.game-hud__audio-head {
  padding: 2px var(--pad-xs) var(--pad-2xs);
  margin-bottom: 4px;
  border-bottom: 1px solid rgba(200, 205, 215, 0.12);
  font-family: "Cinzel", serif;
  font-weight: 700;
  font-size: var(--font-2xs);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: rgba(184, 188, 196, 0.78);
}

/* Each row is a <label> wrapping a native checkbox (visually hidden)
   plus a custom-drawn box + text. Native checkbox stays focusable +
   keyboard-toggleable for a11y; the visible box mirrors :checked. */
/* Restore tutorials. Separated by a rule because it is a one-shot
   action, not a setting — everything above it toggles, this one does
   something and then has nothing left to do. */
.game-hud__audio-sep {
  margin-top: var(--pad-2xs);
  padding-top: var(--pad-2xs);
  border-top: 1px solid rgba(217, 178, 107, 0.28);
}
.game-hud__audio-sep[hidden] { display: none; }
.game-hud__audio-reset {
  width: 100%;
  appearance: none;
  padding: var(--pad-2xs) var(--pad-xs);
  border: 1px solid rgba(217, 178, 107, 0.45);
  border-radius: 3px;
  background: transparent;
  font-family: "Rajdhani", sans-serif;
  font-size: var(--font-sm);
  letter-spacing: 0.12em;
  color: rgba(255, 236, 194, 0.9);
  text-align: left;
  cursor: pointer;
}
.game-hud__audio-reset:hover,
.game-hud__audio-reset:focus-visible {
  background: rgba(217, 178, 107, 0.16);
  outline: none;
}

.game-hud__audio-opt {
  display: flex;
  align-items: center;
  gap: var(--pad-2xs);
  padding: var(--pad-2xs) var(--pad-xs);
  cursor: pointer;
  border-radius: 3px;
  font-family: "Rajdhani", sans-serif;
  font-size: var(--font-sm);
  letter-spacing: 0.14em;
  color: var(--cream);
  transition: background 140ms ease;
}
.game-hud__audio-opt:hover { background: rgba(255, 255, 255, 0.05); }
.game-hud__audio-opt input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.game-hud__audio-opt-box {
  width: 16px;
  height: 16px;
  border: 1px solid rgba(200, 205, 215, 0.5);
  background: rgba(4, 6, 8, 0.6);
  position: relative;
  display: inline-block;
  flex: 0 0 auto;
  transition: border-color 140ms ease, background 140ms ease;
}
.game-hud__audio-opt input[type="checkbox"]:checked + .game-hud__audio-opt-box {
  background: rgba(246, 201, 91, 0.85);
  border-color: rgba(246, 201, 91, 0.95);
}
.game-hud__audio-opt input[type="checkbox"]:checked + .game-hud__audio-opt-box::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 0px;
  width: 5px;
  height: 10px;
  border: solid rgba(20, 15, 5, 0.95);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.game-hud__audio-opt input[type="checkbox"]:focus-visible + .game-hud__audio-opt-box {
  outline: 2px solid rgba(246, 201, 91, 0.65);
  outline-offset: 2px;
}
.game-hud__audio-opt-label { flex: 1 1 auto; }

/* Screens: title / play / game-over swap via [hidden]. */
.game-hud__screen {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--pad-md);
  pointer-events: auto;
}
.game-hud__screen[hidden] { display: none; }

/* --- Title screen ------------------------------------------------- */
.game-hud__title-inner {
  text-align: center;
  max-width: 640px;
  animation: gameFadeUp 500ms ease both;
}
@keyframes gameFadeUp {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}
.game-hud__eyebrow {
  margin: 0 0 var(--pad-xs);
  font-weight: 500;
  font-size: var(--font-2xs);
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: rgba(184, 188, 196, 0.7);
}
.game-hud__title-h {
  margin: 0 0 var(--pad-2xs);
  font-family: "Cinzel", serif;
  font-weight: 900;
  /* Scales with the STAGE (not the viewport) via the container-query
     unit cqi. On a normal-sized phone the type reads as designed; on
     a wide-but-short desktop window where the stage is narrow, it
     shrinks proportionally instead of blowing out. */
  font-size: clamp(22px, 8cqi, 48px);
  letter-spacing: 0.06em;
  line-height: 1.05;
  background: linear-gradient(180deg, #eef0f3 0%, #b8bcc4 55%, #6d7178 100%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  text-shadow: 0 0 24px rgba(246, 201, 91, 0.08);
}
.game-hud__tagline {
  margin: 0 0 var(--pad-md);
  font-size: var(--font-sm);
  letter-spacing: 0.12em;
  color: rgba(200, 205, 215, 0.72);
}

/* Loading bar — animated fill; reveals PRESS START on completion. */
.game-hud__loading {
  margin: var(--pad-md) auto 0;
  max-width: min(360px, 90cqi);
}
.game-hud__loading[hidden] { display: none; }
.game-hud__loading-track {
  position: relative;
  height: 6px;
  border: 1px solid rgba(200, 205, 215, 0.22);
  background: rgba(4, 6, 8, 0.6);
  overflow: hidden;
}
.game-hud__loading-fill {
  display: block;
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, rgba(246, 201, 91, 0.4), rgba(246, 201, 91, 0.9));
  box-shadow: 0 0 10px rgba(246, 201, 91, 0.45);
  transition: width 180ms ease;
}
.game-hud__loading-label {
  margin: var(--pad-2xs) 0 0;
  font-size: var(--font-2xs);
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: rgba(160, 165, 175, 0.65);
}

/* Press START — sheen sweep echoing .cta-btn */
.game-hud__start {
  margin-top: var(--pad-md);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--pad-xs);
  animation: gameFadeUp 400ms ease both;
}
.game-hud__start[hidden] { display: none; }
.game-hud__start-btn {
  position: relative;
  appearance: none;
  padding: var(--pad-sm) var(--pad-md);
  border: 1px solid rgba(246, 201, 91, 0.65);
  background: linear-gradient(180deg, rgba(246, 201, 91, 0.16), rgba(246, 201, 91, 0.04));
  color: #fff5d6;
  font-family: "Cinzel", serif;
  font-weight: 700;
  font-size: var(--font-base);
  letter-spacing: 0.28em;
  text-transform: uppercase;
  cursor: pointer;
  overflow: hidden;
  transition: border-color 200ms ease, background 200ms ease, transform 200ms ease;
}
.game-hud__start-btn::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(120deg, transparent 30%, rgba(255, 245, 214, 0.28) 50%, transparent 70%);
  transform: translateX(-120%);
  animation: gameSheen 2.6s ease-in-out infinite;
}
@keyframes gameSheen {
  0%   { transform: translateX(-120%); }
  60%  { transform: translateX(120%); }
  100% { transform: translateX(120%); }
}
.game-hud__start-btn:hover  { border-color: rgba(246, 201, 91, 0.95); transform: translateY(-1px); }
.game-hud__start-mark       { display: inline-block; margin-right: 10px; color: rgba(246, 201, 91, 0.85); }
.game-hud__meta {
  margin: 0;
  font-family: "Rajdhani", sans-serif;
  font-size: var(--font-xs);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(200, 205, 215, 0.68);
}
.game-hud__meta[hidden] { display: none; }

/* ================================================================
   Speed-up flash — HUD event when the difficulty step raises the
   speed target. Giant transparent overlay at the stage center flashes
   the new value, then shrinks + slides into the speedometer's exact
   coordinates so the "look at this new number" motion terminates at
   the digit the player needs to keep watching.
   ================================================================ */
.game-hud__speed-flash {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(0.4);
  z-index: 8;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(2px, 0.8cqmin, 8px);
  font-family: "Cinzel", serif;
  font-weight: 900;
  color: rgba(255, 245, 214, 0.98);
  text-shadow:
    0 0 22px rgba(255, 200, 100, 0.85),
    0 6px 24px rgba(0, 0, 0, 0.7);
  opacity: 0;
  animation: speedFlashIn 480ms cubic-bezier(0.2, 1.6, 0.3, 1) forwards;
  filter: drop-shadow(0 0 24px rgba(255, 200, 100, 0.5));
}
.game-hud__speed-flash-label {
  font-size: var(--font-lg);
  letter-spacing: 0.36em;
  padding-left: 0.36em;   /* balance the letter-spacing so text stays centered */
  color: rgba(255, 220, 130, 0.9);
  text-transform: uppercase;
}
.game-hud__speed-flash-value {
  font-size: clamp(56px, 22cqmin, 160px);
  line-height: 0.9;
  letter-spacing: 0.02em;
  background: linear-gradient(180deg, #fff5c0 0%, #ffd070 45%, #d8a040 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}
.game-hud__speed-flash-unit {
  font-size: var(--font-md);
  letter-spacing: 0.28em;
  padding-left: 0.28em;
  color: rgba(244, 234, 214, 0.85);
}
.game-hud__speed-flash.is-arriving {
  animation: speedFlashArrive 620ms cubic-bezier(0.5, 0, 0.75, 0.2) forwards;
}
@keyframes speedFlashIn {
  0%   { transform: translate(-50%, -50%) scale(0.4); opacity: 0; }
  35%  { transform: translate(-50%, -50%) scale(1.15); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1);    opacity: 1; }
}
@keyframes speedFlashArrive {
  0%   {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
    filter: drop-shadow(0 0 24px rgba(255, 200, 100, 0.5));
  }
  60%  {
    transform: translate(calc(-50% + var(--flash-target-x, 0px) * 0.65),
                          calc(-50% + var(--flash-target-y, 0px) * 0.65))
               scale(0.4);
    opacity: 0.9;
  }
  100% {
    transform: translate(calc(-50% + var(--flash-target-x, 0px)),
                          calc(-50% + var(--flash-target-y, 0px)))
               scale(0.05);
    opacity: 0;
    filter: drop-shadow(0 0 4px rgba(255, 200, 100, 0.3));
  }
}
@media (prefers-reduced-motion: reduce) {
  .game-hud__speed-flash { animation-duration: 200ms !important; }
  .game-hud__speed-flash.is-arriving { animation-duration: 200ms !important; }
}

/* Toast tray — top-center transient chips. Each toast lives ~2.6s
   then removes itself. Stacked vertically for the rare case where
   two saves land in quick succession. Sits above the scoreboard
   with an animation that reads as "system feedback" rather than
   "in-world thing that happened". */
.game-hud__toast-tray {
  position: absolute;
  top: clamp(8px, 2.4cqmin, 16px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
  display: flex;
  flex-direction: column;
  gap: clamp(4px, 1cqmin, 8px);
  pointer-events: none;
  max-width: min(360px, 78cqmin);
}
.game-hud__toast {
  display: inline-flex;
  align-items: center;
  gap: clamp(6px, 1.6cqmin, 10px);
  padding: clamp(6px, 1.8cqmin, 10px) clamp(12px, 3cqmin, 18px);
  background: linear-gradient(180deg, rgba(20, 12, 4, 0.92), rgba(10, 6, 2, 0.96));
  border: 1.5px solid rgba(255, 220, 130, 0.85);
  border-radius: 999px;
  color: #f5efe0;
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  font-size: var(--font-sm);
  letter-spacing: 0.06em;
  box-shadow:
    0 6px 16px rgba(0, 0, 0, 0.6),
    0 0 24px rgba(255, 200, 100, 0.35);
  animation: hudToastIn 300ms cubic-bezier(0.2, 1.4, 0.3, 1);
}
.game-hud__toast--success { border-color: rgba(120, 210, 130, 0.9); box-shadow: 0 6px 16px rgba(0,0,0,0.6), 0 0 24px rgba(120, 210, 130, 0.4); }
.game-hud__toast--warn    { border-color: rgba(230, 140, 80, 0.9);  box-shadow: 0 6px 16px rgba(0,0,0,0.6), 0 0 24px rgba(230, 140, 80, 0.4); }
.game-hud__toast.is-out { animation: hudToastOut 780ms ease-in forwards; }
/* A toast that carries an UNDO. Reserved for notices that are useless
   without one — "your tutorial is switched off" being the case this
   exists for, where the alternative was a settings panel the player
   had to already know about. */
.game-hud__toast-action {
  margin-left: 0.7em;
  appearance: none;
  padding: 0.25em 0.7em;
  border: 1px solid rgba(255, 236, 194, 0.7);
  border-radius: 999px;
  background: rgba(255, 236, 194, 0.12);
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: 0.92em;
  letter-spacing: 0.08em;
  color: #ffecc2;
  white-space: nowrap;
  cursor: pointer;
  pointer-events: auto;
}
.game-hud__toast-action:hover,
.game-hud__toast-action:focus-visible {
  background: rgba(255, 236, 194, 0.26);
  outline: none;
}
/* The tray is click-through so it never eats a tap meant for the game;
   a toast WITH a button has to opt back in. */
.game-hud__toast:has(.game-hud__toast-action) { pointer-events: auto; }
.game-hud__toast-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: clamp(18px, 4.4cqmin, 24px);
  height: clamp(18px, 4.4cqmin, 24px);
  border-radius: 999px;
  background: rgba(255, 220, 130, 0.18);
  color: rgba(255, 245, 214, 1);
  font-weight: 900;
  font-size: var(--font-sm);
}
.game-hud__toast--success .game-hud__toast-icon { background: rgba(120, 210, 130, 0.25); color: rgba(190, 255, 200, 1); }
.game-hud__toast--warn    .game-hud__toast-icon { background: rgba(230, 140, 80, 0.25);  color: rgba(255, 220, 190, 1); }
@keyframes hudToastIn {
  from { transform: translateY(-16px) scale(0.94); opacity: 0; }
  to   { transform: translateY(0)     scale(1);    opacity: 1; }
}
@keyframes hudToastOut {
  from { transform: translateY(0)     scale(1);    opacity: 1; }
  to   { transform: translateY(-12px) scale(0.94); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .game-hud__toast, .game-hud__toast.is-out { animation-duration: 100ms !important; }
}

/* Resume-from-checkpoint list under the Press Start CTA. Hidden
   entirely when no saved checkpoints exist so first-time players
   see the standard "Press SPACE to drive" flow. Compact rows: a
   distance chip, relative-time text, gold Resume button, and a
   subtle × delete corner. */
.game-hud__resume[hidden] { display: none; }
.game-hud__resume {
  margin-top: clamp(8px, 2cqmin, 14px);
  max-width: min(360px, 80cqmin);
  width: 100%;
  text-align: left;
}
/* Game-over context — centered under the leaderboard, wider on
   full-screen stages so the mission label + time-ago don't crush
   together. */
.game-hud__resume--over {
  margin: clamp(8px, 2cqmin, 14px) auto;
  max-width: min(420px, 90cqmin);
}
.game-hud__resume-title {
  margin: 0 0 clamp(4px, 1.2cqmin, 8px);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(246, 201, 91, 0.85);
  text-align: center;
  /* Now hosts a "Clear all" button on the right — display as a flex
     row so the title text + button share the line cleanly. */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(6px, 1.6cqmin, 12px);
}
.game-hud__resume-title-text { flex: 0 0 auto; }
.game-hud__resume-clear {
  flex: 0 0 auto;
  padding: 2px 10px;
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  font-size: var(--font-2xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(230, 140, 130, 0.85);
  background: rgba(0, 0, 0, 0.35);
  border: 1px solid rgba(230, 140, 130, 0.4);
  border-radius: 999px;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.game-hud__resume-clear:hover {
  background: rgba(180, 60, 60, 0.35);
  color: rgba(255, 220, 210, 0.95);
}
.game-hud__resume-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(4px, 1cqmin, 8px);
}
.game-hud__resume-row {
  display: grid;
  /* Mission name gets the flex column so short 2-3 word labels fit
     without truncation on narrow stages. */
  grid-template-columns: 1fr auto auto auto;
  align-items: center;
  gap: clamp(6px, 1.6cqmin, 12px);
  padding: clamp(4px, 1.2cqmin, 8px) clamp(8px, 2cqmin, 12px);
  background: linear-gradient(180deg, rgba(30, 18, 8, 0.85), rgba(14, 8, 3, 0.9));
  border: 1px solid rgba(255, 220, 130, 0.35);
  border-radius: 999px;
}
.game-hud__resume-mission {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-sm);
  color: rgba(255, 220, 130, 1);
  letter-spacing: 0.06em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.game-hud__resume-when {
  font-family: "Rajdhani", sans-serif;
  font-size: var(--font-xs);
  color: rgba(244, 234, 214, 0.7);
  letter-spacing: 0.06em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.game-hud__resume-btn {
  padding: clamp(3px, 0.9cqmin, 6px) clamp(10px, 2.4cqmin, 14px);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-xs);
  letter-spacing: 0.14em;
  color: #1a0f04;
  background: linear-gradient(180deg, #ffe090, #d8a040);
  border: 1px solid rgba(120, 70, 15, 0.9);
  border-radius: 999px;
  cursor: pointer;
  transition: transform 120ms ease;
}
.game-hud__resume-btn:hover  { transform: translateY(-1px); }
.game-hud__resume-btn:active { transform: translateY(1px); }
.game-hud__resume-del {
  width: clamp(18px, 4cqmin, 24px);
  height: clamp(18px, 4cqmin, 24px);
  padding: 0;
  border: 1px solid rgba(255, 220, 130, 0.35);
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.4);
  color: rgba(244, 234, 214, 0.75);
  font-size: var(--font-sm);
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.game-hud__resume-del:hover {
  background: rgba(180, 60, 60, 0.5);
  color: rgba(255, 235, 214, 1);
}

/* "How to play" link under the Start button. Understated so it doesn't
   compete with the primary CTA. */
.game-hud__rules-link {
  appearance: none;
  background: transparent;
  border: none;
  margin-top: 4px;
  padding: var(--pad-2xs) var(--pad-xs);
  color: rgba(200, 205, 215, 0.6);
  font-family: "Rajdhani", sans-serif;
  font-size: var(--font-2xs);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  text-decoration: underline;
  text-decoration-color: rgba(246, 201, 91, 0.35);
  text-underline-offset: 4px;
  cursor: pointer;
  transition: color 180ms ease, text-decoration-color 180ms ease;
}
.game-hud__rules-link:hover {
  color: rgba(255, 245, 214, 0.9);
  text-decoration-color: rgba(246, 201, 91, 0.85);
}

/* Rules sheet modal. Full-overlay backdrop; click outside the card
   closes. Renders live sprite thumbnails so pro-art swaps flow through
   with zero copy changes in games/runner/manifest.js. */
.game-hud__rules {
  position: absolute;
  inset: 0;
  z-index: 4;
  background: rgba(2, 4, 6, 0.72);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--pad-md);
  pointer-events: auto;
  animation: gameFadeUp 220ms ease both;
}
.game-hud__rules[hidden] { display: none; }
.game-hud__rules-card {
  max-width: min(520px, 94cqi);
  width: 100%;
  background: linear-gradient(180deg, rgba(8, 10, 14, 0.98), rgba(4, 6, 8, 0.94));
  border: 1px solid rgba(246, 201, 91, 0.32);
  padding: var(--pad-md);
  color: var(--cream);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
}
.game-hud__rules-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--pad-xs);
  padding-bottom: var(--pad-xs);
  margin-bottom: var(--pad-2xs);
  border-bottom: 1px solid rgba(200, 205, 215, 0.14);
}
.game-hud__rules-title {
  margin: 0;
  font-family: "Cinzel", serif;
  font-weight: 700;
  font-size: var(--font-md);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold-0);
}
.game-hud__rules-close {
  appearance: none;
  width:  clamp(22px, 8cqi, 34px);
  height: clamp(22px, 8cqi, 34px);
  border-radius: 999px;
  border: 1px solid rgba(200, 205, 215, 0.28);
  background: rgba(6, 8, 12, 0.6);
  color: var(--cream);
  font-size: var(--font-lg);
  line-height: 1;
  cursor: pointer;
  transition: border-color 180ms ease, color 180ms ease;
}
.game-hud__rules-close:hover { border-color: rgba(246, 201, 91, 0.85); color: #fff5d6; }

.game-hud__rules-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--pad-xs);
}
.game-hud__rules-row {
  display: grid;
  grid-template-columns: clamp(40px, 14cqi, 72px) 1fr;
  gap: var(--pad-sm);
  align-items: center;
  padding: var(--pad-2xs) var(--pad-xs);
  background: rgba(0, 0, 0, 0.28);
  border: 1px solid rgba(200, 205, 215, 0.08);
}
.game-hud__rules-thumb {
  width:  clamp(34px, 12cqi, 64px);
  height: clamp(34px, 12cqi, 64px);
  display: block;
  background: rgba(4, 6, 8, 0.6);
  border: 1px solid rgba(200, 205, 215, 0.12);
}
.game-hud__rules-text { display: flex; flex-direction: column; gap: 4px; }
.game-hud__rules-row-title {
  font-family: "Cinzel", serif;
  font-weight: 700;
  font-size: var(--font-sm);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold-0);
}
.game-hud__rules-row-desc {
  font-family: "Rajdhani", sans-serif;
  font-size: var(--font-sm);
  line-height: 1.4;
  color: rgba(200, 205, 215, 0.82);
}

.game-hud__rules-controls {
  margin: var(--pad-sm) 0 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-top: var(--pad-xs);
  border-top: 1px solid rgba(200, 205, 215, 0.14);
  font-family: "Rajdhani", sans-serif;
  font-size: var(--font-xs);
  letter-spacing: 0.14em;
  color: rgba(200, 205, 215, 0.7);
  text-align: center;
}
.game-hud__rules-ctrl-line { display: block; }

/* --- Gameplay HUD ------------------------------------------------- */
.game-hud__play {
  align-items: flex-start;
  justify-content: center;
  /* Scoreboard hugs the top of the stage. The TUTORIAL badge + audio
     button live in the top corners; the scoreboard is centered so
     they never collide even when it's near the top edge. */
  padding-top: clamp(8px, 2cqb, 20px);
  pointer-events: none;   /* canvas gets clicks; only mute/back are interactive */
}
.game-hud__scoreboard {
  display: flex;
  gap: clamp(6px, 2cqi, 12px);
  align-items: flex-end;
  background: linear-gradient(180deg, rgba(4, 6, 8, 0.72), rgba(4, 6, 8, 0.35));
  border: 1px solid rgba(200, 205, 215, 0.16);
  padding: clamp(5px, 1.6cqi, 8px) clamp(9px, 3cqi, 14px);
  border-radius: 4px;
}
.game-hud__score-block {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: clamp(42px, 14cqi, 60px);
}
.game-hud__score-block--best  { min-width: clamp(36px, 12cqi, 50px); }
.game-hud__score-block--speed { min-width: clamp(32px, 11cqi, 46px); }
.game-hud__score-block--coins { min-width: clamp(32px, 11cqi, 46px); }
.game-hud__score-label {
  font-size: var(--font-2xs);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(184, 188, 196, 0.65);
}
.game-hud__score-value {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-lg);
  letter-spacing: 0.04em;
  color: var(--gold-0);
  font-variant-numeric: tabular-nums;
}
.game-hud__score-value--muted { color: rgba(200, 205, 215, 0.68); font-size: var(--font-md); }
/* Live speedometer — same visual weight as the distance block but
   tinted a driving-instrument cyan so it reads as a separate telemetry
   channel rather than another gold "score." Tabular-nums keeps the
   ticker from jittering as it counts up through 3-digit speeds. */
.game-hud__score-value--speed {
  color: #7ce0ff;
  text-shadow: 0 0 10px rgba(124, 224, 255, 0.35);
  font-size: 20px;
}
/* Coin counter — brighter gold than the "muted" best block so the
   in-run currency reads clearly against the two dimmer trailing stats.
   Same tabular-nums so the counter doesn't jitter as it climbs. */
.game-hud__score-value--coins {
  color: #ffd070;
  text-shadow: 0 0 8px rgba(255, 208, 106, 0.35);
  font-size: 20px;
}
.game-hud__banner {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: 32px;
  letter-spacing: 0.14em;
  color: #fff5d6;
  text-shadow: 0 0 14px rgba(246, 201, 91, 0.55);
  animation: gameFadeUp 250ms ease both;
}
.game-hud__banner[hidden] { display: none; }

/* --- Game over screen -------------------------------------------- */
/* Every size below uses the fluid --font-* / --pad-* scale defined
   on .game-stage — the card scales cleanly from a wide-short desktop
   window all the way down to a small phone. */
.game-hud__over-inner {
  text-align: center;
  max-width: min(540px, 96cqi);
  padding: var(--pad-md);
  background: linear-gradient(180deg, rgba(4, 6, 8, 0.88), rgba(4, 6, 8, 0.72));
  border: 1px solid rgba(200, 205, 215, 0.16);
  animation: gameFadeUp 380ms ease both;
}
.game-hud__over-score {
  margin: var(--pad-2xs) 0;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-3xl);
  letter-spacing: 0.06em;
  color: var(--gold-0);
  font-variant-numeric: tabular-nums;
  line-height: 1.05;
}
.game-hud__over-best {
  margin: 0 0 var(--pad-sm);
  font-size: var(--font-xs);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(200, 205, 215, 0.72);
}

/* --- Cleared-stage variant ---------------------------------------
   Same screen, same numbers, same buttons — but a run that ENDED
   somewhere must not look like a run that was ended. The card goes
   gold-lit instead of cold, and the eyebrow (swapped in JS) stops
   announcing that the road ran out. */
.game-hud__over.is-cleared .game-hud__over-inner {
  border-color: rgba(246, 201, 91, 0.45);
  background:
    radial-gradient(120% 80% at 50% 0%, rgba(246, 201, 91, 0.14), transparent 60%),
    linear-gradient(180deg, rgba(20, 14, 6, 0.92), rgba(6, 5, 4, 0.82));
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 20px 60px rgba(0, 0, 0, 0.5);
}
.game-hud__over.is-cleared .game-hud__eyebrow {
  color: #f6c95b;
  letter-spacing: 0.34em;
  font-weight: 700;
}
/* The one line that says what the stage opened up. Only rendered on
   a cleared run — see renderGameOver. */
.game-hud__over-teaser {
  margin: 0 0 var(--pad-sm);
  max-width: 46ch;
  margin-inline: auto;
  font-size: var(--font-xs);
  line-height: 1.5;
  color: rgba(240, 226, 200, 0.86);
}
.game-hud__over-teaser[hidden] { display: none; }
.game-hud__over-newbest {
  display: inline-flex;
  align-items: center;
  gap: var(--pad-2xs);
  margin: 0 0 var(--pad-sm);
  padding: var(--pad-2xs) var(--pad-sm);
  border: 1px solid rgba(246, 201, 91, 0.75);
  background: rgba(246, 201, 91, 0.12);
  color: #fff5d6;
  font-family: "Cinzel", serif;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  font-size: var(--font-2xs);
}
.game-hud__over-newbest[hidden] { display: none; }
.game-hud__newbest-mark { color: rgba(246, 201, 91, 0.9); }

.game-hud__leaderboard {
  margin: var(--pad-sm) 0 var(--pad-md);
  text-align: left;
}
.game-hud__leaderboard[hidden] { display: none; }
.game-hud__leaderboard-title {
  margin: 0 0 var(--pad-2xs);
  font-family: "Cinzel", serif;
  font-weight: 700;
  font-size: var(--font-2xs);
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: rgba(184, 188, 196, 0.78);
  text-align: center;
}
.game-hud__leaderboard-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid rgba(200, 205, 215, 0.14);
  background: rgba(2, 4, 6, 0.4);
}
.game-hud__lb-row {
  display: grid;
  grid-template-columns: clamp(24px, 8cqi, 44px) 1fr auto;
  align-items: center;
  gap: var(--pad-xs);
  padding: var(--pad-2xs) var(--pad-xs);
  border-bottom: 1px solid rgba(200, 205, 215, 0.06);
  font-size: var(--font-sm);
}
.game-hud__lb-row:last-child { border-bottom: 0; }
.game-hud__lb-place {
  text-align: center;
  font-family: "Cinzel", serif;
  color: rgba(246, 201, 91, 0.85);
  font-weight: 700;
}
.game-hud__lb-name {
  display: inline-flex;
  align-items: baseline;
  gap: var(--pad-2xs);
  color: var(--cream);
}
.game-hud__lb-rank, .game-hud__lb-owner {
  font-size: clamp(7px, 2cqi, 10px);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  padding: 1px clamp(4px, 1.4cqi, 8px);
  border: 1px solid rgba(200, 205, 215, 0.28);
  color: rgba(200, 205, 215, 0.85);
}
.game-hud__lb-owner {
  color: #1a140a;
  background: rgba(246, 201, 91, 0.95);
  border-color: rgba(246, 201, 91, 0.75);
}
.game-hud__lb-score {
  font-family: "Cinzel", serif;
  color: var(--gold-0);
  font-variant-numeric: tabular-nums;
}

/* ONE PRIMARY, THEN THE REST.
   Stacked rather than one flat row of equals: the forward action gets
   its own line and full width, and everything else sits under it in a
   quieter row. A row of three same-sized buttons makes the player read
   all three every time; this makes the common case unmissable and the
   detours findable. */
.game-hud__over-actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(6px, 1.6cqmin, 11px);
  margin-top: var(--pad-xs);
}
.game-hud__over-actions > .game-hud__btn--primary {
  width: min(100%, 340px);
  padding-top: clamp(9px, 2.4cqmin, 15px);
  padding-bottom: clamp(9px, 2.4cqmin, 15px);
  font-size: clamp(13px, 3.4cqmin, 19px);
  letter-spacing: 0.14em;
}
.game-hud__over-minor {
  display: flex;
  justify-content: center;
  gap: var(--pad-xs);
  flex-wrap: wrap;
}
.game-hud__over-minor .game-hud__btn {
  font-size: clamp(9px, 2.5cqmin, 13px);
  opacity: 0.82;
}
.game-hud__over-minor .game-hud__btn:hover { opacity: 1; }

/* ---- Replay picker ------------------------------------------------
   A modal, because choosing where to restart from is a detour off the
   main path and should look like one. It used to be an always-open
   panel on the summary, taller than the score. */
.game-hud__replay {
  position: absolute;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: auto;
}
.game-hud__replay[hidden] { display: none; }
.game-hud__replay-scrim {
  position: absolute;
  inset: 0;
  background: rgba(4, 3, 2, 0.72);
  backdrop-filter: blur(2px);
}
.game-hud__replay-panel {
  position: relative;
  width: min(88cqi, 440px);
  max-height: 80cqb;
  overflow-y: auto;
  padding: clamp(12px, 3cqmin, 22px);
  border-radius: clamp(6px, 1.6cqmin, 12px);
  border: 1px solid rgba(217, 178, 107, 0.5);
  background: linear-gradient(180deg, rgba(30, 20, 12, 0.98), rgba(12, 8, 5, 0.98));
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.7);
  animation: replayIn 200ms cubic-bezier(0.2, 1, 0.3, 1) both;
}
@keyframes replayIn {
  from { opacity: 0; transform: translateY(10px) scale(0.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.game-hud__replay-title {
  margin: 0 0 clamp(8px, 2cqmin, 14px);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(255, 236, 194, 0.9);
  text-align: center;
}
.game-hud__replay-foot {
  margin-top: clamp(8px, 2cqmin, 14px);
  display: flex;
  justify-content: center;
}
@media (prefers-reduced-motion: reduce) {
  .game-hud__replay-panel { animation-duration: 80ms; }
}

.game-hud__btn {
  appearance: none;
  padding: var(--pad-xs) var(--pad-md);
  border: 1px solid rgba(200, 205, 215, 0.32);
  background: rgba(6, 8, 12, 0.6);
  color: var(--cream);
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  font-size: var(--font-xs);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color 180ms ease, background 180ms ease, color 180ms ease;
}
.game-hud__btn:hover { border-color: rgba(246, 201, 91, 0.85); color: #fff5d6; }
.game-hud__btn--primary {
  border-color: rgba(246, 201, 91, 0.7);
  background: linear-gradient(180deg, rgba(246, 201, 91, 0.15), rgba(246, 201, 91, 0.03));
  color: #fff5d6;
}
.game-hud__btn--primary:hover { border-color: rgba(246, 201, 91, 1); }
.game-hud__btn--ghost {
  background: transparent;
  border-color: rgba(200, 205, 215, 0.35);
  color: rgba(200, 205, 215, 0.75);
}
.game-hud__btn--ghost:hover { border-color: rgba(200, 205, 215, 0.65); color: rgba(244, 234, 214, 0.95); }
.game-hud__btn[hidden] { display: none; }

/* When the game is running, dim the .battlefield slightly for extra
   contrast against gameplay elements. */
body[data-game="running"] .battlefield::after {
  background:
    radial-gradient(1400px 900px at 50% 55%, rgba(0, 0, 0, 0.45), transparent 70%),
    radial-gradient(900px 600px at 20% 30%, rgba(60, 30, 10, 0.15), transparent 65%);
}

/* ================================================================
   Cut-scenes — DOM overlay above the canvases, below the HUD.
   Transparent-scrim design so the road (bg canvas) stays visible
   through the scene, while obstacles + player are paused in place
   by the runner's tick guard.
   ================================================================ */
.game-cutscene {
  position: absolute;
  inset: 0;
  z-index: 4;                 /* above canvases (2) and the stick pad (2); ABOVE the hud (3) */
  pointer-events: none;
  overflow: hidden;
}
.game-cutscene[hidden] { display: none; }

/* Subtle dark vignette so text + bike pop off the road. Slides in
   with the scene, out with .is-exiting. */
.cutscene__scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(80% 60% at 60% 55%, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.15) 60%, transparent 100%),
    linear-gradient(to bottom, rgba(0, 0, 0, 0.25), transparent 30%, transparent 70%, rgba(0, 0, 0, 0.3));
  opacity: 0;
  animation: cutSceneScrimIn 320ms ease-out forwards;
}
.game-cutscene.is-exiting .cutscene__scrim {
  animation: cutSceneScrimOut 460ms ease-in forwards;
}
@keyframes cutSceneScrimIn  { from { opacity: 0; } to { opacity: 1; } }
@keyframes cutSceneScrimOut { from { opacity: 1; } to { opacity: 0; } }

/* Speed-lines behind the bike — thin gold streaks that arc in from
   the right. Each span is a shifted line; keyframe offsets stagger
   them for a "whoosh" read. */
.cutscene__streaks {
  position: absolute;
  inset: 0;
  overflow: hidden;
  opacity: 0;
  animation: cutSceneScrimIn 220ms ease-out forwards;
}
.game-cutscene.is-exiting .cutscene__streaks {
  animation: cutSceneScrimOut 340ms ease-in forwards;
}
.cutscene__streaks > span {
  position: absolute;
  right: -30%;
  height: 2px;
  width: 60%;
  background: linear-gradient(to left, rgba(255, 245, 214, 0), rgba(255, 220, 130, 0.85) 30%, rgba(255, 245, 214, 0) 100%);
  transform: translateX(120%) skewX(-8deg);
  animation: cutSceneStreak 560ms cubic-bezier(0.2, 0.8, 0.3, 1) forwards;
}
.cutscene__streaks > span:nth-child(1) { top: 22%; animation-delay:  40ms; }
.cutscene__streaks > span:nth-child(2) { top: 34%; animation-delay: 100ms; width: 68%; }
.cutscene__streaks > span:nth-child(3) { top: 46%; animation-delay: 160ms; width: 74%; }
.cutscene__streaks > span:nth-child(4) { top: 58%; animation-delay: 220ms; width: 66%; }
.cutscene__streaks > span:nth-child(5) { top: 70%; animation-delay: 280ms; width: 58%; }
@keyframes cutSceneStreak {
  from { transform: translateX(120%) skewX(-8deg); opacity: 0; }
  40%  { opacity: 1; }
  to   { transform: translateX(-160%) skewX(-8deg); opacity: 0; }
}

/* Reaction-shot layout — vertical stack, centered. Burst text on
   top, bike below. Both scale together via cqmin so on tall/narrow
   or short/wide stages they always fit without overlap. Previous
   left/right absolute placement collided in quarter-height layouts
   and on phones; a centered vertical composition avoids that class
   of bug entirely. */
.cutscene__reaction {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: clamp(12px, 4cqi, 32px);
  gap: clamp(10px, 4cqi, 28px);
  pointer-events: none;
}

/* Motorbike sprite — bottom of the vertical stack. Enters sliding
   in from the right, settles centered under the burst, bobs, and
   flies out on exit. Width is capped in cqmin so a super-wide stage
   doesn't blow it up past the point where the burst text overlaps. */
.cutscene__bike {
  display: block;
  width: clamp(120px, 42cqmin, 320px);
  height: auto;
  transform: translateX(120cqi) rotate(-6deg) scale(0.85);
  filter: drop-shadow(0 12px 24px rgba(0, 0, 0, 0.6));
  animation: cutSceneBikeIn 620ms cubic-bezier(0.15, 0.9, 0.2, 1.15) forwards,
             cutSceneBikeBob 1400ms ease-in-out 620ms infinite;
}
.game-cutscene.is-exiting .cutscene__bike {
  animation: cutSceneBikeOut 580ms cubic-bezier(0.4, 0, 0.9, 0.7) forwards;
}
@keyframes cutSceneBikeIn {
  from { transform: translateX(120cqi) rotate(-14deg) scale(0.8);  }
  70%  { transform: translateX(-8px)   rotate(4deg)  scale(1.06); }
  100% { transform: translateX(0)      rotate(0)     scale(1);    }
}
@keyframes cutSceneBikeBob {
  0%, 100% { transform: translateY(0)    rotate(0)     scale(1);    }
  50%      { transform: translateY(-4px) rotate(-2deg) scale(1.02); }
}
@keyframes cutSceneBikeOut {
  from { transform: translate(0, 0)         rotate(0)    scale(1);   opacity: 1; }
  to   { transform: translate(30cqi, -20cqb) rotate(18deg) scale(0.3); opacity: 0; }
}

/* Comic-burst text — big yellow-gold shout with a jagged star
   shape behind it. Scale-bounces in, holds, shrinks out. Sits in
   the top row of the reaction grid; width is capped in cqmin so
   the text wraps to 2 lines on narrow stages instead of running
   past the star polygon's narrow waist. */
.cutscene__burst {
  position: relative;
  box-sizing: border-box;
  max-width: min(300px, 62cqmin);
  transform: scale(0.4) rotate(-6deg);
  opacity: 0;
  animation: cutSceneBurstIn 520ms cubic-bezier(0.15, 1.4, 0.25, 1) 100ms forwards,
             cutSceneBurstShake 240ms ease-in-out 620ms infinite;
}
.game-cutscene.is-exiting .cutscene__burst {
  animation: cutSceneBurstOut 500ms ease-in forwards;
}
@keyframes cutSceneBurstIn {
  from { transform: scale(0.4)  rotate(-16deg); opacity: 0; }
  70%  { transform: scale(1.14) rotate(2deg);   opacity: 1; }
  100% { transform: scale(1)    rotate(-4deg);  opacity: 1; }
}
@keyframes cutSceneBurstShake {
  0%, 100% { transform: scale(1)    rotate(-4deg); }
  50%      { transform: scale(1.03) rotate(-3deg); }
}
@keyframes cutSceneBurstOut {
  from { transform: scale(1)   rotate(-4deg);  opacity: 1; }
  to   { transform: scale(0.4) rotate(-18deg); opacity: 0; }
}

/* Jagged star polygon behind the text — CSS-only comic-book burst.
   `inset` is negative so the star extends BEYOND the text bounds on
   every side, keeping a visible rim around long or short text alike. */
.cutscene__burst-star {
  position: absolute;
  inset: -14px -18px;
  background:
    radial-gradient(ellipse at 30% 30%, rgba(255, 245, 214, 0.55), transparent 55%),
    linear-gradient(135deg, rgba(246, 201, 91, 0.98), rgba(255, 168, 40, 0.98));
  clip-path: polygon(
    50% 0%, 62% 16%, 84% 8%, 78% 30%, 100% 34%,
    82% 50%, 100% 66%, 76% 68%, 84% 92%, 62% 82%,
    50% 100%, 38% 82%, 16% 92%, 24% 68%, 0% 66%,
    18% 50%, 0% 34%, 22% 30%, 16% 8%, 38% 16%
  );
  filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.55));
  z-index: 0;
}
.cutscene__burst-text {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: clamp(8px, 2.6cqmin, 16px) clamp(14px, 4.5cqmin, 26px);
  font-family: "Cinzel", serif;
  font-weight: 900;
  /* Scales down to fit the star's clip-path bounds on narrow stages. */
  font-size: clamp(16px, 5.4cqmin, 30px);
  line-height: 1.05;
  /* Solid gold pill behind the text so it stays legible even where
     the text extends beyond the star polygon's narrow waist. */
  background: linear-gradient(180deg, rgba(255, 220, 130, 0.98), rgba(246, 190, 90, 0.98));
  border: 1.5px solid rgba(120, 70, 15, 0.75);
  border-radius: clamp(6px, 1.8cqmin, 12px);
  box-shadow: inset 0 1px 0 rgba(255, 245, 214, 0.7),
              inset 0 -2px 0 rgba(140, 80, 20, 0.35);
  letter-spacing: 0.04em;
  color: #1a0f04;
  text-shadow:
    0 2px 0 rgba(255, 245, 214, 0.7),
    0 -1px 0 rgba(180, 100, 20, 0.7);
  /* Allow wrap to 2 lines for long languages, with pretty balancing. */
  white-space: normal;
  text-wrap: balance;
  overflow-wrap: break-word;
  hyphens: auto;
  text-align: center;
  -webkit-text-stroke: 1px rgba(90, 40, 5, 0.55);
}

/* Respect user motion preferences — fade instead of animate. */
@media (prefers-reduced-motion: reduce) {
  .cutscene__bike,
  .cutscene__burst,
  .cutscene__scrim,
  .cutscene__streaks,
  .cutscene__streaks > span {
    animation-duration: 200ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* ================================================================
   UNLOCKED! hero-card reveal
   ================================================================
   Design conventions borrowed from gacha / collectible card games:
     - Rarity-gradient border (gold = legendary)
     - Radial rays behind the card sweep outward (draws the eye)
     - Sparkle particles rise from below
     - "UNLOCKED!" banner in heavy uppercase at top
     - Card portrait fills upper 62%, name/tag ribbon at bottom
     - Subtle shine sweep across the card face (metallic feel)
     - Persistent "Tap to continue" pulse at bottom
   ================================================================ */

/* Darker scrim so the card reads against a near-black backdrop. */
.cutscene__scrim--reveal {
  background:
    radial-gradient(65% 55% at 50% 50%, rgba(20, 12, 4, 0.75), rgba(6, 4, 2, 0.95) 60%, rgba(0, 0, 0, 0.97) 100%);
}

/* Gold light rays radiating from the card — CSS-only conic gradient
   spun slowly for a subtle "energy" feel behind the reveal. */
.cutscene__rays {
  position: absolute;
  left: 50%;
  top: 44%;
  width: 220vmax;
  height: 220vmax;
  transform: translate(-50%, -50%) rotate(0);
  background:
    conic-gradient(from 0deg,
      rgba(246, 201, 91, 0.32) 0deg,
      transparent 8deg,
      transparent 22deg,
      rgba(246, 201, 91, 0.32) 30deg,
      transparent 38deg,
      transparent 52deg,
      rgba(246, 201, 91, 0.42) 60deg,
      transparent 68deg,
      transparent 82deg,
      rgba(246, 201, 91, 0.28) 90deg,
      transparent 98deg,
      transparent 118deg,
      rgba(246, 201, 91, 0.38) 126deg,
      transparent 134deg,
      transparent 158deg,
      rgba(246, 201, 91, 0.32) 166deg,
      transparent 174deg,
      transparent 198deg,
      rgba(246, 201, 91, 0.42) 206deg,
      transparent 214deg,
      transparent 238deg,
      rgba(246, 201, 91, 0.28) 246deg,
      transparent 254deg,
      transparent 278deg,
      rgba(246, 201, 91, 0.38) 286deg,
      transparent 294deg,
      transparent 318deg,
      rgba(246, 201, 91, 0.34) 326deg,
      transparent 334deg,
      transparent 360deg);
  opacity: 0;
  animation:
    cutSceneRaysIn   380ms ease-out forwards,
    cutSceneRaysSpin 14s linear infinite;
  filter: blur(6px);
}
.game-cutscene.is-exiting .cutscene__rays {
  animation: cutSceneRaysOut 420ms ease-in forwards, cutSceneRaysSpin 14s linear infinite;
}
@keyframes cutSceneRaysIn  { from { opacity: 0; } to { opacity: 0.55; } }
@keyframes cutSceneRaysOut { from { opacity: 0.55; } to { opacity: 0; } }
@keyframes cutSceneRaysSpin { to { transform: translate(-50%, -50%) rotate(360deg); } }

/* Sparkle motes floating up around the card. */
.cutscene__sparkles {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}
.cutscene__sparkles > span {
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: radial-gradient(circle, #fff5d6 0%, rgba(246, 201, 91, 0.85) 50%, transparent 100%);
  box-shadow: 0 0 12px rgba(255, 220, 130, 0.75);
  opacity: 0;
  animation: cutSceneSparkle 3s ease-in-out infinite;
}
.cutscene__sparkles > span:nth-child(1)  { left: 12%; bottom: 20%; animation-delay: 0.10s; }
.cutscene__sparkles > span:nth-child(2)  { left: 88%; bottom: 32%; animation-delay: 0.40s; width: 4px; height: 4px; }
.cutscene__sparkles > span:nth-child(3)  { left: 22%; bottom: 60%; animation-delay: 0.75s; width: 8px; height: 8px; }
.cutscene__sparkles > span:nth-child(4)  { left: 78%; bottom: 70%; animation-delay: 1.10s; }
.cutscene__sparkles > span:nth-child(5)  { left: 40%; bottom: 78%; animation-delay: 1.45s; width: 5px; height: 5px; }
.cutscene__sparkles > span:nth-child(6)  { left: 60%; bottom: 12%; animation-delay: 0.20s; width: 5px; height: 5px; }
.cutscene__sparkles > span:nth-child(7)  { left: 32%; bottom: 10%; animation-delay: 0.60s; }
.cutscene__sparkles > span:nth-child(8)  { left: 70%; bottom: 45%; animation-delay: 0.85s; width: 4px; height: 4px; }
.cutscene__sparkles > span:nth-child(9)  { left: 18%; bottom: 38%; animation-delay: 1.30s; width: 6px; height: 6px; }
.cutscene__sparkles > span:nth-child(10) { left: 92%; bottom: 15%; animation-delay: 1.70s; width: 5px; height: 5px; }
@keyframes cutSceneSparkle {
  0%   { transform: translateY(20px) scale(0.6); opacity: 0; }
  20%  { opacity: 1; }
  80%  { opacity: 0.6; }
  100% { transform: translateY(-140px) scale(1.2); opacity: 0; }
}

/* Big UNLOCKED! headline above the card. Rotates in with scale-
   bounce, then a slow underline pulse to hold the eye. */
.cutscene__unlocked {
  position: absolute;
  top: 8%;
  left: 50%;
  transform: translate(-50%, 0) scale(0.5);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(28px, 8cqi, 56px);
  letter-spacing: 0.18em;
  color: #fff5d6;
  text-shadow:
    0 2px 0 rgba(60, 30, 5, 0.9),
    0 0 24px rgba(246, 201, 91, 0.9),
    0 0 8px rgba(255, 245, 214, 0.7);
  opacity: 0;
  animation:
    cutSceneUnlockedIn   520ms cubic-bezier(0.2, 1.4, 0.3, 1) forwards,
    cutSceneUnlockedGlow 1.8s ease-in-out 520ms infinite;
}
.game-cutscene.is-exiting .cutscene__unlocked {
  animation: cutSceneUnlockedOut 460ms ease-in forwards;
}
@keyframes cutSceneUnlockedIn {
  from { transform: translate(-50%, -20px) scale(0.5) rotate(-8deg); opacity: 0; }
  70%  { transform: translate(-50%, 0)     scale(1.12) rotate(2deg); opacity: 1; }
  100% { transform: translate(-50%, 0)     scale(1)    rotate(0);   opacity: 1; }
}
@keyframes cutSceneUnlockedGlow {
  0%, 100% { text-shadow: 0 2px 0 rgba(60,30,5,0.9), 0 0 20px rgba(246,201,91,0.7), 0 0 8px rgba(255,245,214,0.6); }
  50%      { text-shadow: 0 2px 0 rgba(60,30,5,0.9), 0 0 36px rgba(246,201,91,1.0), 0 0 14px rgba(255,245,214,0.95); }
}
@keyframes cutSceneUnlockedOut {
  to { transform: translate(-50%, -30px) scale(0.7); opacity: 0; }
}

/* The card. Scales in from below the frame, hovers with a slow bob. */
.cutscene__card {
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(72%, 320px);
  aspect-ratio: 3 / 4;
  transform: translate(-50%, -50%) scale(0.5);
  opacity: 0;
  border-radius: 14px;
  overflow: hidden;
  background: linear-gradient(180deg, #1a120a 0%, #0d0704 100%);
  box-shadow:
    0 0 0 3px rgba(246, 201, 91, 0.85),
    0 0 0 6px rgba(90, 55, 10, 0.9),
    0 20px 60px rgba(0, 0, 0, 0.7),
    0 0 60px rgba(246, 201, 91, 0.4);
  animation:
    cutSceneCardIn    560ms cubic-bezier(0.2, 1.4, 0.3, 1) 100ms forwards,
    cutSceneCardBob   3.6s ease-in-out 660ms infinite;
}
.game-cutscene.is-exiting .cutscene__card {
  animation: cutSceneCardOut 480ms ease-in forwards;
}
@keyframes cutSceneCardIn {
  from { transform: translate(-50%, -20%) scale(0.4) rotate(-10deg); opacity: 0; }
  70%  { transform: translate(-50%, -50%) scale(1.08) rotate(2deg);  opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1)    rotate(0);     opacity: 1; }
}
@keyframes cutSceneCardBob {
  0%, 100% { transform: translate(-50%, -50%) scale(1)    rotate(0); }
  50%      { transform: translate(-50%, -52%) scale(1.02) rotate(-1deg); }
}
@keyframes cutSceneCardOut {
  to { transform: translate(-50%, -60%) scale(0.6) rotate(6deg); opacity: 0; }
}

/* Legendary rarity — richer gold gradient border. */
.cutscene__card--legendary {
  box-shadow:
    0 0 0 3px rgba(255, 220, 130, 1),
    0 0 0 6px rgba(120, 70, 10, 0.95),
    0 20px 60px rgba(0, 0, 0, 0.75),
    0 0 70px rgba(255, 200, 100, 0.55);
}
.cutscene__card--legendary::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg,
    rgba(246, 201, 91, 0.20) 0%,
    transparent 40%,
    transparent 60%,
    rgba(246, 201, 91, 0.15) 100%);
  pointer-events: none;
}

/* Slow shine sweep across the card face — metallic collectible feel. */
.cutscene__card-shine {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}
.cutscene__card-shine::before {
  content: "";
  position: absolute;
  inset: -20% -50%;
  background: linear-gradient(115deg,
    transparent 30%,
    rgba(255, 255, 255, 0.25) 46%,
    rgba(255, 255, 255, 0.55) 50%,
    rgba(255, 255, 255, 0.25) 54%,
    transparent 70%);
  transform: translateX(-140%);
  animation: cutSceneCardShine 2.6s ease-in-out 900ms infinite;
}
@keyframes cutSceneCardShine {
  0%   { transform: translateX(-140%); }
  55%  { transform: translateX(140%); }
  100% { transform: translateX(140%); }
}

/* Card portrait — fills the upper 62% of the card. */
.cutscene__card-portrait {
  position: relative;
  height: 62%;
  background: radial-gradient(70% 60% at 50% 45%, rgba(70, 45, 15, 0.8), rgba(15, 8, 3, 0.95));
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-bottom: 1px solid rgba(246, 201, 91, 0.35);
}
.cutscene__card-portrait img {
  max-width: 88%;
  max-height: 92%;
  filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.7));
  transform: scale(1);
}

/* Card info ribbon — hero name + tagline in the lower 38%. */
.cutscene__card-info {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 38%;
  padding: 10px 14px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 4px;
  background: linear-gradient(180deg, rgba(30, 18, 6, 0.4) 0%, rgba(10, 6, 2, 0.95) 100%);
}
.cutscene__card-name {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(16px, 4.4cqi, 24px);
  letter-spacing: 0.14em;
  color: #ffe4a0;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.8);
  text-align: center;
  line-height: 1.1;
}
.cutscene__card-tag {
  font-family: "Rajdhani", sans-serif;
  font-weight: 600;
  font-size: clamp(10px, 2.8cqi, 13px);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(230, 200, 150, 0.85);
  text-align: center;
}

/* Tap to continue pulse at bottom. */
.cutscene__tap-prompt {
  position: absolute;
  bottom: 6%;
  left: 50%;
  transform: translateX(-50%);
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  font-size: clamp(11px, 3cqi, 14px);
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: rgba(255, 245, 214, 0.85);
  text-shadow: 0 0 12px rgba(246, 201, 91, 0.6);
  opacity: 0;
  animation:
    cutSceneTapIn    400ms ease-out 700ms forwards,
    cutSceneTapPulse 1.4s ease-in-out 1100ms infinite;
}
/* Settle lock — the prompt does not invite a press it is going to
   swallow. Paused for the scene's settle window (settleMs in
   ui/cutscene.js: 600ms by default, 1.4s on a stage clear), so "press
   any key" fades in exactly when a key would work. Pausing rather than
   hiding keeps the animation's own 700ms entrance intact — it simply
   starts counting from the moment the beat becomes advanceable. */
.game-cutscene.is-locked .cutscene__tap-prompt,
.game-cutscene.is-locked .cutscene__dlg-tap {
  opacity: 0;
  animation-play-state: paused;
}
.game-cutscene.is-exiting .cutscene__tap-prompt { animation: cutSceneTapOut 260ms ease-in forwards; }
@keyframes cutSceneTapIn    { to { opacity: 1; } }
@keyframes cutSceneTapOut   { to { opacity: 0; } }
@keyframes cutSceneTapPulse {
  0%, 100% { opacity: 0.6; transform: translateX(-50%) scale(1);    }
  50%      { opacity: 1;   transform: translateX(-50%) scale(1.06); }
}

/* ================================================================
   GET READY! countdown + companion drive-in
   ================================================================
   The scene runs 3800ms total: 4 windows × 950ms each. Each number
   animates in with cutSceneCountIn, holds, then fades out via a
   staggered animation-delay so exactly one number is prominent at a
   time. The bike sprite drives in from bottom-center over the full
   duration and lands at bottom-right on the "GO!" beat.
   ================================================================ */
.cutscene__scrim--dim {
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.35), rgba(0, 0, 0, 0.15) 40%, rgba(0, 0, 0, 0.15) 60%, rgba(0, 0, 0, 0.35));
}

.cutscene__ready {
  position: absolute;
  top: 20%;
  left: 50%;
  transform: translate(-50%, 0);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(24px, 6.5cqi, 40px);
  letter-spacing: 0.22em;
  color: #fff5d6;
  text-shadow:
    0 2px 0 rgba(60, 30, 5, 0.9),
    0 0 20px rgba(246, 201, 91, 0.85);
  opacity: 0;
  animation: cutSceneReadyIn 400ms cubic-bezier(0.2, 1.4, 0.3, 1) forwards;
}
.game-cutscene.is-exiting .cutscene__ready {
  animation: cutSceneReadyOut 400ms ease-in forwards;
}
@keyframes cutSceneReadyIn {
  from { transform: translate(-50%, -20px) scale(0.6); opacity: 0; }
  100% { transform: translate(-50%, 0)     scale(1);   opacity: 1; }
}
@keyframes cutSceneReadyOut {
  to { transform: translate(-50%, -30px) scale(0.7); opacity: 0; }
}

/* Countdown numbers: absolutely stacked in the center, each with a
   staggered delay so exactly one is visible per beat.
   Timeline: 0-900ms = "3", 900-1800 = "2", 1800-2700 = "1",
             2700-3800 = "GO!"                                       */
.cutscene__count {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 60%;
  height: 30%;
}
.cutscene__count-n {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: "Cinzel", serif;
  font-weight: 900;
  color: #fff5d6;
  text-shadow:
    0 4px 0 rgba(60, 30, 5, 0.95),
    0 0 40px rgba(246, 201, 91, 0.95),
    0 0 14px rgba(255, 245, 214, 0.9);
  opacity: 0;
  transform: scale(0.4);
  animation: cutSceneCountBeat 900ms cubic-bezier(0.2, 1.4, 0.3, 1) forwards;
}
.cutscene__count-n--3  { font-size: clamp(80px, 22cqi, 160px); animation-delay: 0ms; }
.cutscene__count-n--2  { font-size: clamp(80px, 22cqi, 160px); animation-delay: 900ms; }
.cutscene__count-n--1  { font-size: clamp(80px, 22cqi, 160px); animation-delay: 1800ms; }
.cutscene__count-n--go { font-size: clamp(70px, 18cqi, 130px); letter-spacing: 0.08em; animation-delay: 2700ms; animation-duration: 1100ms; color: #ffdc82; }
@keyframes cutSceneCountBeat {
  0%   { transform: scale(0.4);  opacity: 0; }
  20%  { transform: scale(1.20); opacity: 1; }
  50%  { transform: scale(1.0);  opacity: 1; }
  85%  { transform: scale(0.98); opacity: 1; }
  100% { transform: scale(1.15); opacity: 0; }
}

/* Companion motorbike drives in from bottom-center, arcs across
   the stage, and settles at bottom-right on the "GO!" beat.
   Uses a two-keyframe animation: 0-70% drives up + right (arc),
   70-100% settles at final rest position. */
/* ================================================================
   Persistent companion pinned in formation next to the player.
   Rendered inside .game-stage after the countdown ends; stays until
   the run ends. Sits low-right so it doesn't fight the player's
   sightline for oncoming obstacles.
   ================================================================ */
.game-companion {
  position: absolute;
  bottom: 8%;
  right: 6%;
  width: min(18%, 72px);
  height: auto;
  pointer-events: none;
  z-index: 3;                 /* above canvases (2); ties with the hud (3) — DOM order decides */
  filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.55));
  animation: gameCompanionIdle 2.2s ease-in-out infinite;
  transform-origin: 50% 90%;
}
.game-companion[hidden] { display: none; }
@keyframes gameCompanionIdle {
  0%, 100% { transform: translateY(0)     rotate(0); }
  50%      { transform: translateY(-4px) rotate(-2deg); }
}
@media (prefers-reduced-motion: reduce) {
  .game-companion { animation: none; }
}

/* ================================================================
   MISSION FAILED — dramatic red banner
   ================================================================
   Bold red uppercase title bounces in with a shake; subtitle fades
   in beneath. Deep-red scrim darkens the road. Wired to end with
   the runner going to the game-over screen automatically.
   ================================================================ */
.cutscene__scrim--fail {
  background:
    radial-gradient(80% 60% at 50% 50%, rgba(120, 10, 10, 0.55), rgba(30, 4, 4, 0.9) 60%, rgba(10, 2, 2, 0.98) 100%);
}
.cutscene__fail {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 6%;
  gap: 12px;
}
.cutscene__fail-title {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(32px, 9cqi, 60px);
  letter-spacing: 0.18em;
  color: #ff4a4a;
  text-shadow:
    0 3px 0 rgba(50, 5, 5, 0.95),
    0 0 30px rgba(255, 60, 60, 0.85),
    0 0 12px rgba(255, 100, 100, 0.7);
  transform: scale(0.4);
  opacity: 0;
  animation:
    cutSceneFailIn    500ms cubic-bezier(0.2, 1.4, 0.3, 1) forwards,
    cutSceneFailShake 180ms ease-in-out 500ms 4;
}
.game-cutscene.is-exiting .cutscene__fail-title {
  animation: cutSceneFailOut 480ms ease-in forwards;
}
@keyframes cutSceneFailIn {
  from { transform: scale(0.4)  rotate(-6deg); opacity: 0; }
  70%  { transform: scale(1.14) rotate(2deg);  opacity: 1; }
  100% { transform: scale(1)    rotate(0);     opacity: 1; }
}
@keyframes cutSceneFailShake {
  0%, 100% { transform: translate(0, 0)   scale(1); }
  25%      { transform: translate(-4px, 0) scale(1); }
  75%      { transform: translate(4px, 0)  scale(1); }
}
@keyframes cutSceneFailOut {
  to { transform: scale(0.8) translateY(-20px); opacity: 0; }
}
.cutscene__fail-sub {
  font-family: "Rajdhani", sans-serif;
  font-weight: 600;
  font-size: clamp(13px, 3.6cqi, 17px);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 190, 180, 0.9);
  opacity: 0;
  animation: cutSceneFailSubIn 400ms ease-out 500ms forwards;
}
.game-cutscene.is-exiting .cutscene__fail-sub {
  animation: cutSceneFailSubOut 400ms ease-in forwards;
}
@keyframes cutSceneFailSubIn  { to { opacity: 1; } }
@keyframes cutSceneFailSubOut { to { opacity: 0; } }

/* What happens next. Deliberately calmer than the two lines above it —
   cream rather than red, no shake, and it arrives last: the failure is
   the drama, the handover is the reassurance. A thin rule and a pair of
   travelling dots read as "working" without a spinner. */
.cutscene__fail-note {
  margin-top: clamp(10px, 2.6cqi, 20px);
  padding-top: clamp(8px, 2cqi, 14px);
  border-top: 1px solid rgba(217, 178, 107, 0.35);
  font-family: "Rajdhani", sans-serif;
  font-weight: 500;
  font-size: clamp(11px, 3cqi, 15px);
  letter-spacing: 0.1em;
  color: rgba(238, 228, 210, 0.82);
  opacity: 0;
  animation: cutSceneFailSubIn 500ms ease-out 1100ms forwards;
}
.cutscene__fail-note::after {
  content: '';
  display: inline-block;
  width: 3px; height: 3px;
  margin-left: 0.5em;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 6px 0 0 currentColor, 12px 0 0 currentColor;
  animation: failNoteDots 1.2s steps(3, end) infinite;
}
@keyframes failNoteDots {
  0%   { opacity: 0.25; }
  100% { opacity: 1; }
}
.game-cutscene.is-exiting .cutscene__fail-note {
  animation: cutSceneFailSubOut 400ms ease-in forwards;
}
@media (prefers-reduced-motion: reduce) {
  .cutscene__fail-note::after { animation: none; }
}

/* ================================================================
   Dialogue cut-scene (RPG / visual-novel style)
   ================================================================
   Character portrait on the left, rounded speech bubble on the
   right with a triangular tail pointing back to the portrait,
   name banner above the text, tap-to-continue arrow pulsing at
   the bottom-right of the bubble. The portrait bounces in first,
   then the bubble scales in with a small overshoot 120ms later.
   ================================================================ */
.cutscene__scrim--dialogue {
  background:
    radial-gradient(60% 50% at 40% 55%, rgba(6, 4, 2, 0.75), rgba(6, 4, 2, 0.92) 60%, rgba(0, 0, 0, 0.95) 100%);
}

/* Fluid dialogue tokens — one source of truth for portrait +
   bubble sizing so every character dialogue looks proportional at
   any stage aspect. cqmin scales with the SMALLER stage axis so
   the panel doesn't blow up on wide-quarter stages. */
.cutscene__dialogue {
  --dlg-portrait: clamp(56px, 22cqmin, 130px);
  --dlg-gap:      clamp(8px,  2.8cqmin, 18px);
  --dlg-pad-x:    clamp(10px, 3.2cqmin, 20px);
  --dlg-pad-y:    clamp(10px, 3.0cqmin, 18px);
  --dlg-radius:   clamp(10px, 3cqmin, 16px);

  position: absolute;
  left:   clamp(8px, 3cqi, 24px);
  right:  clamp(8px, 3cqi, 24px);
  bottom: clamp(10px, 4cqb, 32px);
  display: grid;
  grid-template-columns: var(--dlg-portrait) 1fr;
  gap: var(--dlg-gap);
  align-items: end;
}

/* Portrait — framed with a gold ring, drop-shadow, bounces in.
   Fixed square via aspect-ratio; width derives from --dlg-portrait
   so it stays proportional to the bubble at every size. */
.cutscene__dlg-portrait {
  position: relative;
  width: var(--dlg-portrait);
  height: var(--dlg-portrait);
  border-radius: 999px;
  overflow: hidden;
  background: radial-gradient(circle at 50% 40%, rgba(70, 45, 15, 0.85), rgba(10, 6, 2, 0.95));
  box-shadow:
    0 0 0 3px rgba(255, 220, 130, 0.95),
    0 0 0 6px rgba(90, 55, 10, 0.9),
    0 12px 24px rgba(0, 0, 0, 0.65),
    0 0 40px rgba(255, 200, 100, 0.35);
  transform: translateY(30px) scale(0.6);
  opacity: 0;
  animation: cutSceneDlgPortIn 420ms cubic-bezier(0.2, 1.4, 0.3, 1) forwards;
  align-self: end;
}
.cutscene__dlg-portrait img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 30%;
  filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.6));
}
.game-cutscene.is-exiting .cutscene__dlg-portrait {
  animation: cutSceneDlgPortOut 320ms ease-in forwards;
}
@keyframes cutSceneDlgPortIn {
  from { transform: translateY(30px) scale(0.6); opacity: 0; }
  100% { transform: translateY(0)    scale(1);   opacity: 1; }
}
@keyframes cutSceneDlgPortOut {
  to { transform: translateY(30px) scale(0.7); opacity: 0; }
}

/* Speech bubble — dark rounded rectangle with a gold border and
   inner glow. Extra bottom pad reserves space for the "tap" line
   so long dialogue text never runs into it. */
.cutscene__dlg-bubble {
  position: relative;
  padding: var(--dlg-pad-y) var(--dlg-pad-x)
           calc(var(--dlg-pad-y) + clamp(10px, 2.6cqmin, 18px))
           var(--dlg-pad-x);
  background: linear-gradient(180deg, rgba(14, 8, 3, 0.95), rgba(6, 4, 2, 0.98));
  border: 2px solid rgba(255, 220, 130, 0.85);
  border-radius: var(--dlg-radius);
  box-shadow:
    0 12px 28px rgba(0, 0, 0, 0.7),
    inset 0 0 20px rgba(255, 200, 100, 0.15),
    0 0 40px rgba(255, 200, 100, 0.25);
  transform: translateY(20px) scale(0.9);
  opacity: 0;
  animation: cutSceneDlgBubbleIn 380ms cubic-bezier(0.2, 1.4, 0.3, 1) 120ms forwards;
  min-height: var(--dlg-portrait);
  min-width: 0;   /* let grid track shrink instead of overflowing */
  align-self: end;
}
.game-cutscene.is-exiting .cutscene__dlg-bubble {
  animation: cutSceneDlgBubbleOut 320ms ease-in forwards;
}
@keyframes cutSceneDlgBubbleIn {
  from { transform: translateY(20px) scale(0.9); opacity: 0; }
  100% { transform: translateY(0)    scale(1);   opacity: 1; }
}
@keyframes cutSceneDlgBubbleOut {
  to { transform: translateY(15px) scale(0.9); opacity: 0; }
}

/* Triangular tail on the left edge of the bubble, pointing back
   to the portrait. Sized in cqmin so it stays proportional. */
.cutscene__dlg-tail {
  --tail-h: clamp(8px, 2.4cqmin, 12px);
  --tail-w: clamp(9px, 2.8cqmin, 14px);
  position: absolute;
  top: 45%;
  left: calc(var(--tail-w) * -1);
  width: 0;
  height: 0;
  border-top:    var(--tail-h) solid transparent;
  border-bottom: var(--tail-h) solid transparent;
  border-right:  var(--tail-w) solid rgba(255, 220, 130, 0.85);
  filter: drop-shadow(-1px 0 0 rgba(255, 220, 130, 0.6));
}
.cutscene__dlg-tail::after {
  content: "";
  position: absolute;
  top: calc(var(--tail-h) * -0.8);
  left: 2px;
  width: 0;
  height: 0;
  border-top:    calc(var(--tail-h) * 0.8) solid transparent;
  border-bottom: calc(var(--tail-h) * 0.8) solid transparent;
  border-right:  calc(var(--tail-w) * 0.83) solid rgba(10, 6, 2, 0.98);
}

/* Name banner — small gold ribbon at the top-left of the bubble,
   overlapping the top border slightly. Uses the shared font-2xs
   token so it stays legible without dwarfing the bubble text. */
.cutscene__dlg-name {
  position: absolute;
  top: calc(var(--font-2xs) * -0.9);
  left: var(--dlg-pad-x);
  max-width: calc(100% - var(--dlg-pad-x) * 2);
  padding: clamp(2px, 0.6cqmin, 4px) clamp(8px, 2.2cqmin, 14px);
  background: linear-gradient(180deg, rgba(255, 220, 130, 1), rgba(220, 160, 60, 1));
  color: #1a0f04;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-2xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.55);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Body text — capped in height so a runaway localised string
   scrolls inside the bubble instead of blowing past the stage. */
.cutscene__dlg-text {
  font-family: "Rajdhani", sans-serif;
  font-weight: 500;
  font-size: var(--font-sm);
  line-height: 1.4;
  color: #f5efe0;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
  max-height: clamp(64px, 32cqb, 240px);
  overflow-y: auto;
  overflow-wrap: break-word;
}

/* Tap prompt — pinned to the bottom-right of the bubble with a
   pulsing chevron. Classic RPG "keep going" affordance. */
.cutscene__dlg-tap {
  position: absolute;
  bottom: clamp(4px, 1.4cqmin, 8px);
  right:  var(--dlg-pad-x);
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  font-size: var(--font-2xs);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(255, 220, 130, 0.9);
  animation: cutSceneDlgTapPulse 1.2s ease-in-out infinite;
}
.cutscene__dlg-tap span {
  display: inline-block;
  margin-left: 4px;
  transform: translateY(1px);
  animation: cutSceneDlgTapArrow 1.2s ease-in-out infinite;
}
@keyframes cutSceneDlgTapPulse {
  0%, 100% { opacity: 0.65; }
  50%      { opacity: 1;    }
}
@keyframes cutSceneDlgTapArrow {
  0%, 100% { transform: translate(0, 1px); }
  50%      { transform: translate(4px, 1px); }
}

/* ================================================================
   Conversation scene — multi-turn NPC exchange
   ================================================================
   Two portraits anchored to opposite shoulders of the stage; the
   active speaker lifts and lights up while the listener dims to
   ~60% opacity. Bubble spans the bottom half with the current line
   + turn counter + tap-to-continue pulse + a skip button in the
   corner. All sizing in cqmin so it fits every stage aspect.
   ================================================================ */
.convo {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  padding: clamp(8px, 3cqmin, 20px);
  gap: clamp(6px, 2cqmin, 14px);
  pointer-events: auto;
}
.convo__cast {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: clamp(8px, 3cqmin, 20px);
  padding: 0 clamp(4px, 1.6cqmin, 12px);
}
.convo__portrait {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(4px, 1cqmin, 8px);
  opacity: 0.55;
  filter: saturate(0.75) brightness(0.7);
  transform: translateY(6px) scale(0.94);
  transition: opacity 260ms ease, filter 260ms ease, transform 260ms ease;
}
.convo__portrait.is-speaking {
  opacity: 1;
  filter: saturate(1.1) brightness(1);
  transform: translateY(0) scale(1);
}
.convo__portrait--left  { align-self: flex-end; }
.convo__portrait--right { align-self: flex-end; }
.convo__portrait-frame {
  width: clamp(90px, 30cqmin, 180px);
  height: clamp(120px, 40cqmin, 240px);
  border-radius: clamp(8px, 2.4cqmin, 16px);
  overflow: hidden;
  background: radial-gradient(circle at 50% 40%, rgba(70, 45, 15, 0.85), rgba(10, 6, 2, 0.95));
  box-shadow:
    0 0 0 2px rgba(255, 220, 130, 0.85),
    0 0 0 5px rgba(90, 55, 10, 0.7),
    0 12px 24px rgba(0, 0, 0, 0.65),
    0 0 30px rgba(255, 200, 100, 0.25);
}
.convo__portrait.is-speaking .convo__portrait-frame {
  box-shadow:
    0 0 0 2.5px rgba(255, 245, 214, 1),
    0 0 0 6px rgba(255, 210, 120, 0.9),
    0 12px 26px rgba(0, 0, 0, 0.75),
    0 0 42px rgba(255, 200, 100, 0.55);
}
.convo__portrait-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 25%;
  display: block;
}
.convo__portrait-name {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-2xs);
  letter-spacing: 0.18em;
  color: rgba(255, 220, 130, 0.95);
  text-transform: uppercase;
  padding: 2px 10px;
  background: rgba(6, 4, 2, 0.7);
  border: 1px solid rgba(255, 220, 130, 0.5);
  border-radius: 999px;
}

.convo__bubble {
  position: relative;
  align-self: stretch;
  flex: 0 0 auto;
  padding: clamp(10px, 3cqmin, 18px) clamp(14px, 3.6cqmin, 24px);
  background: linear-gradient(180deg, rgba(14, 8, 3, 0.95), rgba(6, 4, 2, 0.98));
  border: 2px solid rgba(255, 220, 130, 0.85);
  border-radius: clamp(10px, 3cqmin, 18px);
  box-shadow:
    0 12px 28px rgba(0, 0, 0, 0.7),
    inset 0 0 22px rgba(255, 200, 100, 0.15),
    0 0 40px rgba(255, 200, 100, 0.25);
  min-height: clamp(90px, 22cqmin, 160px);
  color: #f5efe0;
}
.convo__bubble.is-turn-in {
  animation: convoTurnIn 340ms cubic-bezier(0.2, 1.4, 0.3, 1);
}
@keyframes convoTurnIn {
  from { transform: translateY(8px) scale(0.98); opacity: 0.4; }
  to   { transform: translateY(0)   scale(1);    opacity: 1;   }
}
/* Tail pointing at the active speaker — swaps sides when the
   turn changes speaker (see `data-side` attribute set in mount). */
.convo__bubble::before {
  content: "";
  position: absolute;
  top: -14px;
  width: 0; height: 0;
  border-left:  10px solid transparent;
  border-right: 10px solid transparent;
  border-bottom: 14px solid rgba(255, 220, 130, 0.85);
  transition: left 260ms ease, right 260ms ease;
}
.convo__bubble[data-side="left"]::before  { left: 12%; }
.convo__bubble[data-side="right"]::before { left: auto; right: 12%; }
.convo__bubble::after {
  content: "";
  position: absolute;
  top: -11px;
  width: 0; height: 0;
  border-left:  8px solid transparent;
  border-right: 8px solid transparent;
  border-bottom: 11px solid rgba(14, 8, 3, 0.95);
  transition: left 260ms ease, right 260ms ease;
}
.convo__bubble[data-side="left"]::after  { left: calc(12% + 2px); }
.convo__bubble[data-side="right"]::after { left: auto; right: calc(12% + 2px); }

.convo__bubble-name {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-sm);
  letter-spacing: 0.18em;
  color: rgba(255, 220, 130, 1);
  text-transform: uppercase;
  margin-bottom: clamp(4px, 1cqmin, 8px);
}
.convo__bubble-line {
  font-family: "Rajdhani", sans-serif;
  font-weight: 500;
  font-size: var(--font-base);
  line-height: 1.42;
  color: #f5efe0;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
  min-height: clamp(40px, 10cqmin, 72px);
}
.convo__bubble-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: clamp(4px, 1.4cqmin, 10px);
  font-size: var(--font-2xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.convo__bubble-count {
  color: rgba(244, 234, 214, 0.55);
}
/* Settle-lock state — bubble sits with a dimmed "tap to continue"
   pulse during the per-turn read window so an anxious tapper can
   see WHEN they're allowed to advance. Removed after ~550 ms; the
   pulse restores. */
.convo__bubble.is-locked .convo__bubble-tap { opacity: 0.15; animation-play-state: paused; }
.convo__bubble-tap {
  color: rgba(255, 220, 130, 0.9);
  animation: cutSceneDlgTapPulse 1.2s ease-in-out infinite;
}
.convo__bubble-tap span {
  display: inline-block;
  margin-left: 4px;
  animation: cutSceneDlgTapArrow 1.2s ease-in-out infinite;
}
.convo__skip {
  position: absolute;
  /* BELOW THE SESSION CONTROLS, not on top of them.
     Pause and audio own the stage's top-right corner and are drawn
     above the cutscene layer, so SKIP was landing on them: three
     controls in one place, one of them half-covered, and the two
     underneath are the ones a player reaches for without looking.
     Dropped by the height of that row plus a gap, computed from the
     same clamps the buttons use so the two stay in step at every
     stage size rather than agreeing at one width. */
  top: calc(clamp(8px, 2.2cqi, 16px) + clamp(22px, 8cqi, 40px)
            + clamp(6px, 1.6cqi, 10px));
  right: clamp(8px, 2cqmin, 14px);
  padding: clamp(4px, 1.2cqmin, 8px) clamp(10px, 2.4cqmin, 14px);
  background: rgba(6, 4, 2, 0.7);
  border: 1px solid rgba(255, 220, 130, 0.55);
  border-radius: 999px;
  color: rgba(255, 220, 130, 0.9);
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  font-size: var(--font-2xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 120ms ease, transform 120ms ease;
}
.convo__skip:hover { background: rgba(255, 200, 100, 0.18); }
.convo__skip:active { transform: translateY(1px); }

@media (prefers-reduced-motion: reduce) {
  .convo__portrait, .convo__bubble, .convo__bubble.is-turn-in {
    animation: none !important;
    transition: none !important;
  }
}

/* Choice buttons row — sits inside the dialogue bubble on `choice`
   scenes. Primary is a big gold CTA that pulses to draw the eye;
   secondary is understated so the player reads primary as "the
   move to make". */
.cutscene__choice-row {
  display: flex;
  align-items: center;
  gap: clamp(6px, 2cqmin, 14px);
  margin-top: clamp(8px, 2cqmin, 14px);
  justify-content: flex-end;
}
.cutscene__choice-btn {
  padding: clamp(6px, 1.8cqmin, 12px) clamp(12px, 3cqmin, 20px);
  border-radius: clamp(6px, 1.6cqmin, 10px);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-sm);
  letter-spacing: 0.16em;
  cursor: pointer;
  transition: transform 120ms ease, box-shadow 120ms ease;
}
.cutscene__choice-btn--secondary {
  background: rgba(0, 0, 0, 0.4);
  border: 1.5px solid rgba(200, 205, 215, 0.4);
  color: rgba(200, 205, 215, 0.75);
}
.cutscene__choice-btn--secondary:hover {
  border-color: rgba(200, 205, 215, 0.7);
  color: rgba(244, 234, 214, 0.95);
}
.cutscene__choice-btn--primary {
  position: relative;
  padding-right: clamp(18px, 4cqmin, 26px);
  background: linear-gradient(180deg, #ffe090, #d8a040);
  border: 2px solid rgba(255, 245, 214, 0.9);
  color: #1a0f04;
  box-shadow:
    0 4px 12px rgba(0, 0, 0, 0.4),
    0 0 22px rgba(255, 200, 100, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.4);
  animation: cutSceneChoicePulse 1.4s ease-in-out infinite;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
@keyframes cutSceneChoicePulse {
  0%, 100% { box-shadow: 0 4px 12px rgba(0,0,0,0.4), 0 0 22px rgba(255, 200, 100, 0.55), inset 0 1px 0 rgba(255,255,255,0.4); transform: translateY(0); }
  50%      { box-shadow: 0 6px 16px rgba(0,0,0,0.5), 0 0 40px rgba(255, 200, 100, 0.95), inset 0 1px 0 rgba(255,255,255,0.5); transform: translateY(-2px); }
}
.cutscene__choice-btn--primary:hover {
  transform: translateY(-2px) scale(1.02);
  box-shadow: 0 8px 18px rgba(0,0,0,0.55), 0 0 48px rgba(255, 200, 100, 1);
}
.cutscene__choice-btn-glow {
  position: absolute;
  inset: -4px;
  border-radius: inherit;
  background: radial-gradient(60% 100% at 50% 50%, rgba(255, 245, 214, 0.5), transparent 70%);
  pointer-events: none;
  filter: blur(4px);
}
.cutscene__choice-btn-arrow { font-size: 1.1em; }
@media (prefers-reduced-motion: reduce) {
  .cutscene__choice-btn--primary { animation: none; }
}

/* ================================================================
   The war behind the stage-1 gate
   ================================================================
   Three stacked layers per blast, mounted over the stage by the
   runner at --bx / --by (the gate's live top edge):

     __sky   blows the top band of the frame out to white. This is
             the "you cannot look at it" layer, and it's what makes
             the detonation read as enormous rather than as a bright
             dot — an explosion big enough to matter overexposes the
             sky, it doesn't sit in it.
     __core  the fireball proper, radiating outward from the origin.
     __ring  the shock front, which outruns the light.

   The origin (--by) is set ABOVE the top edge by the runner, so the
   upper half of every blast is clipped away by the frame. That crop
   is doing the heavy lifting: a fireball you can see the top of is
   one happening HERE, at a size and distance the eye can measure.
   With no top, it reads as bigger than the screen and somewhere
   else. Everything below is built to be seen bottom-half-only —
   which is why the gradients are weighted toward their outer stops
   rather than their cores.

   Light curve is modelled on the real thing: near-instant attack
   (~70ms to full), a brief hold, then a long decay. Getting that
   asymmetry right matters more than any amount of colour work —
   a symmetric fade reads as a lamp on a dimmer.
   ================================================================ */
.war-blast {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 6;
}
.war-blast__sky,
.war-blast__core,
.war-blast__ring { position: absolute; }

/* Blown-out sky. Anchored to the TOP of the stage and fading down, so
   the horizon is annihilated and the road stays readable. */
.war-blast__sky {
  left: 0; right: 0; top: 0;
  height: 54%;
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 1)    0%,
    rgba(255, 255, 252, 0.98) 18%,
    rgba(255, 246, 222, 0.80) 40%,
    rgba(255, 206, 132, 0.36) 68%,
    transparent 100%);
  opacity: 0;
  animation: warSky 820ms cubic-bezier(0.05, 0.85, 0.25, 1) forwards;
}
@keyframes warSky {
  0%   { opacity: 0; }
  8%   { opacity: 1; }
  30%  { opacity: 0.75; }
  100% { opacity: 0; }
}
/* Fireball. Scales out from the gate's top edge. */
.war-blast__core {
  left: 0; right: 0; top: 0;
  height: 74%;
  transform-origin: var(--bx, 50%) var(--by, -8%);
  /* Stops are pushed outward compared to a centred fireball: with the
     core off screen, the visible band is the 25-70% region, so that's
     where the brightness has to live. */
  background: radial-gradient(135% 155% at var(--bx, 50%) var(--by, -8%),
    rgba(255, 255, 255, 1)     0%,
    rgba(255, 250, 235, 0.99) 22%,
    rgba(255, 226, 158, 0.94) 38%,
    rgba(250, 156, 56, 0.72)  56%,
    rgba(130, 52, 18, 0.34)   74%,
    transparent 92%);
  opacity: 0;
  animation: warCore 1000ms cubic-bezier(0.06, 0.8, 0.22, 1) forwards;
}
@keyframes warCore {
  0%   { opacity: 0;    transform: scale(0.18); }
  7%   { opacity: 1;    transform: scale(1.00); }
  34%  { opacity: 0.9;  transform: scale(1.28); }
  100% { opacity: 0;    transform: scale(1.9); }
}
/* Shock front — outruns the fireball and thins as it goes. */
.war-blast__ring {
  left: var(--bx, 50%);
  top:  var(--by, -8%);
  width: 14px; height: 14px;
  margin: -7px 0 0 -7px;
  border-radius: 50%;
  border: 4px solid rgba(255, 240, 205, 0.9);
  box-shadow: 0 0 40px rgba(255, 200, 120, 0.8);
  opacity: 0;
  animation: warRing 760ms cubic-bezier(0.1, 0.85, 0.25, 1) forwards;
}
@keyframes warRing {
  0%   { opacity: 0;   transform: scale(1);  border-width: 6px; }
  8%   { opacity: 1; }
  100% { opacity: 0;   transform: scale(46); border-width: 0.5px; }
}

/* Secondary detonations: same anatomy, dialled down. Still overexpose
   the sky — just less of it, and for less time. */
.war-blast--secondary .war-blast__sky   { height: 34%; animation-duration: 620ms; }
.war-blast--secondary .war-blast__core  { height: 56%; animation-duration: 780ms; }
.war-blast--secondary .war-blast__ring  { animation-duration: 620ms; }
.war-blast--secondary .war-blast__core  { filter: brightness(0.86); }

/* Small-arms fire. No fireball and no shock front — just a low glow
   guttering above the wall, because a rifle doesn't light the sky. */
.war-blast--chatter .war-blast__core,
.war-blast--chatter .war-blast__ring { display: none; }
.war-blast--chatter .war-blast__sky {
  height: 26%;
  background: linear-gradient(180deg,
    rgba(255, 236, 196, 0.42) 0%,
    rgba(255, 190, 110, 0.18) 46%,
    transparent 100%);
  animation: warChatterGlow 900ms steps(9, end) forwards;
}
/* Stepped, not eased — the glow flickers with the shots instead of
   swelling like a light on a fader. */
@keyframes warChatterGlow {
  0%   { opacity: 0.9; }
  50%  { opacity: 0.55; }
  100% { opacity: 0; }
}

/* Camera kick. Detonations only — shaking on every rifle burst would
   turn the whole beat to mush. */
.game-stage.is-war-kick { animation: warKick 600ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both; }
@keyframes warKick {
  0%, 100% { transform: translate3d(0, 0, 0); }
  8%       { transform: translate3d(-7px, 4px, 0); }
  19%      { transform: translate3d(8px, -5px, 0); }
  31%      { transform: translate3d(-6px, 3px, 0); }
  44%      { transform: translate3d(5px, -3px, 0); }
  58%      { transform: translate3d(-3px, 2px, 0); }
  76%      { transform: translate3d(2px, -1px, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .game-stage.is-war-kick { animation: none; }
  .war-blast__core, .war-blast__ring { animation-duration: 420ms; }
  .war-blast__sky { animation-duration: 380ms; }
}

/* ================================================================
   Coin streak
   ================================================================
   Three surfaces, escalating in loudness:

     .game-hud__streak  — the live chip. Ticks on every coin. Quiet
                          enough to live on screen for a whole run.
     .streak-flash      — the milestone. Full-stage, once per tier.
     .is-streak-pulse   — a gold wash over the whole stage behind it,
                          so a tier reads as happening to the WORLD
                          and not inside a HUD widget.

   The chip's palette climbs with `data-tier`, gold → orange →
   white-hot, so peripheral vision alone tells you how deep you are
   without reading the number.
   ================================================================ */
.game-hud__streak {
  position: absolute;
  top: clamp(46px, 13cqb, 96px);       /* clears the top scoreboard */
  left: 50%;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: clamp(3px, 1.2cqi, 7px);
  padding: clamp(2px, 0.9cqi, 5px) clamp(7px, 2.4cqi, 13px);
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(40, 26, 10, 0.92), rgba(12, 8, 4, 0.94));
  border: 1px solid rgba(246, 201, 91, 0.55);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);
  font-family: "Rajdhani", system-ui, sans-serif;
  white-space: nowrap;
  z-index: 4;
  pointer-events: none;
}
.game-hud__streak[hidden] { display: none; }
.game-hud__streak-flame {
  width: clamp(6px, 2cqi, 10px);
  height: clamp(8px, 2.6cqi, 13px);
  border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
  background: linear-gradient(180deg, #fff3cf, #f6a03a 60%, #c8501a);
  box-shadow: 0 0 8px rgba(246, 160, 58, 0.8);
  animation: streakFlame 620ms ease-in-out infinite alternate;
}
@keyframes streakFlame {
  from { transform: scaleY(0.86) translateY(1px); opacity: 0.85; }
  to   { transform: scaleY(1.12) translateY(-1px); opacity: 1; }
}
.game-hud__streak-count {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(11px, 3.4cqi, 20px);
  color: #ffe6a8;
  font-variant-numeric: tabular-nums;
}
.game-hud__streak-label {
  font-size: clamp(6px, 1.9cqi, 10px);
  letter-spacing: 0.24em;
  color: rgba(232, 220, 196, 0.6);
}
.game-hud__streak-mult {
  margin-left: clamp(2px, 0.8cqi, 5px);
  padding: 0 clamp(3px, 1.2cqi, 7px);
  border-radius: 999px;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(10px, 3cqi, 17px);
  color: #2a1a08;
  background: linear-gradient(180deg, #fff3cf, #f6c95b 60%, #c8901c);
}
.game-hud__streak-mult[hidden] { display: none; }
/* Per-coin tick. Tiny, fast, and restarted by JS on every pickup —
   this is the drumbeat that keeps the chip feeling alive between
   milestones, which is where most of a streak is actually spent. */
.game-hud__streak.is-tick { animation: streakTick 240ms cubic-bezier(0.2, 1.6, 0.3, 1); }
@keyframes streakTick {
  0%   { transform: translateX(-50%) scale(1); }
  40%  { transform: translateX(-50%) scale(1.13); }
  100% { transform: translateX(-50%) scale(1); }
}
.game-hud__streak.is-entering { animation: streakIn 380ms cubic-bezier(0.2, 1.5, 0.3, 1); }
@keyframes streakIn {
  from { opacity: 0; transform: translateX(-50%) translateY(-10px) scale(0.8); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
}
/* Heat by tier. */
.game-hud__streak[data-tier="2"] { border-color: rgba(255, 170, 90, 0.7); }
.game-hud__streak[data-tier="3"] { border-color: rgba(255, 140, 70, 0.8); }
.game-hud__streak[data-tier="4"],
.game-hud__streak[data-tier="5"] {
  border-color: rgba(255, 120, 60, 0.9);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5), 0 0 16px rgba(255, 140, 60, 0.35);
}
.game-hud__streak[data-tier="6"] {
  border-color: rgba(255, 250, 240, 0.95);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5), 0 0 26px rgba(255, 220, 170, 0.6);
  animation: streakWhiteHot 900ms ease-in-out infinite alternate;
}
@keyframes streakWhiteHot {
  from { box-shadow: 0 4px 14px rgba(0,0,0,0.5), 0 0 18px rgba(255, 220, 170, 0.45); }
  to   { box-shadow: 0 4px 14px rgba(0,0,0,0.5), 0 0 34px rgba(255, 240, 210, 0.8); }
}
/* Break — the chip goes grey, shakes, drops. Half a second, then
   gone. Small on purpose: punishing this moment teaches players to
   stop chasing streaks, which is the opposite of the point. */
.game-hud__streak.is-breaking {
  animation: streakBreak 500ms ease-in forwards;
  filter: grayscale(0.85);
}
@keyframes streakBreak {
  0%   { transform: translateX(-50%) scale(1)    rotate(0deg);   opacity: 1; }
  18%  { transform: translateX(-50%) scale(1.05) rotate(-2deg); }
  40%  { transform: translateX(-50%) scale(0.98) rotate(2deg); }
  100% { transform: translateX(-50%) scale(0.86) translateY(16px) rotate(-4deg); opacity: 0; }
}

/* --- Tier milestone flash --------------------------------------- */
.streak-flash {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(2px, 1cqmin, 8px);
  pointer-events: none;
  z-index: 7;
  animation: streakFlashOut 1500ms cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
}
/* Punch in, hold, then shrink AND rise toward the chip — the same
   "big number resolves into the HUD element it belongs to" grammar
   the speed flash uses, so the player already knows how to read it. */
@keyframes streakFlashOut {
  0%   { opacity: 0; transform: scale(1.6); }
  10%  { opacity: 1; transform: scale(1); }
  62%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.4) translateY(-32cqb); }
}
.streak-flash__rays {
  position: absolute;
  inset: -50%;
  background: repeating-conic-gradient(from 0deg at 50% 50%,
    rgba(255, 200, 110, 0) 0deg,
    rgba(255, 200, 110, 0.22) 2.5deg,
    rgba(255, 200, 110, 0) 9deg);
  animation: streakRays 6s linear infinite;
}
@keyframes streakRays { to { transform: rotate(360deg); } }
.streak-flash__mult {
  position: relative;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(46px, 22cqmin, 150px);
  line-height: 0.95;
  background: linear-gradient(180deg, #fffdf5 0%, #ffd070 48%, #d07a12 100%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  filter: drop-shadow(0 6px 26px rgba(255, 180, 70, 0.6));
}
.streak-flash__label {
  position: relative;
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(9px, 3.4cqmin, 20px);
  letter-spacing: 0.4em;
  text-indent: 0.4em;
  color: rgba(255, 240, 210, 0.92);
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7);
}
/* Bigger multipliers burn hotter — ×7 and ×10 go white. */
.streak-flash[data-mult="7"] .streak-flash__mult,
.streak-flash[data-mult="10"] .streak-flash__mult {
  background: linear-gradient(180deg, #ffffff 0%, #fff0c0 45%, #ffae3a 100%);
  filter: drop-shadow(0 6px 34px rgba(255, 220, 150, 0.85));
}

/* Stage-wide wash behind the milestone. */
.game-stage.is-streak-pulse::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(70% 55% at 50% 50%,
    rgba(255, 200, 110, 0.30), rgba(255, 170, 60, 0.10) 55%, transparent 80%);
  animation: streakPulse 700ms ease-out forwards;
  pointer-events: none;
  z-index: 6;
}
@keyframes streakPulse {
  0%   { opacity: 0;   transform: scale(0.85); }
  22%  { opacity: 1; }
  100% { opacity: 0;   transform: scale(1.1); }
}

/* --- Streak row on the summary screen ---------------------------- */
.game-hud__over-streak {
  display: inline-flex;
  align-items: center;
  gap: var(--pad-2xs);
  margin: 0 0 var(--pad-sm);
  padding: var(--pad-2xs) var(--pad-sm);
  border-radius: 999px;
  border: 1px solid rgba(246, 201, 91, 0.35);
  background: rgba(246, 201, 91, 0.08);
}
.game-hud__over-streak[hidden] { display: none; }
.game-hud__over-streak-flame {
  width: 9px; height: 12px;
  border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
  background: linear-gradient(180deg, #fff3cf, #f6a03a 60%, #c8501a);
  box-shadow: 0 0 8px rgba(246, 160, 58, 0.7);
}
.game-hud__over-streak-value {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-md);
  color: #ffe6a8;
}
.game-hud__over-streak-label {
  font-size: var(--font-2xs);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(226, 214, 194, 0.75);
}
.game-hud__over-streak.is-record {
  border-color: rgba(255, 220, 130, 0.9);
  background: rgba(246, 201, 91, 0.18);
  animation: streakRecordPulse 1.6s ease-in-out infinite;
}
.game-hud__over-streak.is-record .game-hud__over-streak-label { color: #ffd070; }
@keyframes streakRecordPulse {
  0%, 100% { box-shadow: 0 0 0 rgba(246, 201, 91, 0); }
  50%      { box-shadow: 0 0 20px rgba(246, 201, 91, 0.55); }
}

/* ================================================================
   Home-screen leaderboards
   ================================================================
   Tabbed, because the boards answer different questions — "who
   drives furthest" and "who is greediest" reward different players,
   and someone who will never top distance might well top streak.
   Sits under the Resume list on the title screen.
   ================================================================ */
.game-hud__boards {
  width: min(420px, 92cqi);
  margin: var(--pad-sm) auto 0;
  text-align: left;
}
.game-hud__boards[hidden] { display: none; }
.game-hud__boards-tabs {
  display: flex;
  gap: var(--pad-2xs);
  margin-bottom: var(--pad-2xs);
}
.game-hud__boards-tab {
  flex: 1 1 0;
  padding: clamp(3px, 1.2cqi, 7px) clamp(6px, 2cqi, 12px);
  border-radius: 999px;
  border: 1px solid rgba(200, 205, 215, 0.18);
  background: rgba(4, 6, 8, 0.6);
  color: rgba(214, 208, 196, 0.7);
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: var(--font-2xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  pointer-events: auto;
  transition: color 140ms ease, border-color 140ms ease, background 140ms ease;
}
.game-hud__boards-tab:hover { color: var(--cream); }
.game-hud__boards-tab.is-active {
  color: #2a1a08;
  border-color: rgba(90, 60, 20, 0.9);
  background: linear-gradient(180deg, #f4ead6 0%, #d9b26b 60%, #a76a1a 100%);
}
.game-hud__boards-list {
  list-style: none;
  margin: 0;
  padding: 0;
  background: rgba(4, 6, 8, 0.45);
  border: 1px solid rgba(200, 205, 215, 0.1);
  border-radius: 6px;
  overflow: hidden;
}
/* Medal tints on the podium — the cheapest way to make a table read
   as a ranking instead of as a list. */
.game-hud__lb-place--m1 { color: #ffd970; text-shadow: 0 0 10px rgba(255, 215, 112, 0.55); }
.game-hud__lb-place--m2 { color: #d8dde6; text-shadow: 0 0 10px rgba(216, 221, 230, 0.4); }
.game-hud__lb-place--m3 { color: #e2955a; text-shadow: 0 0 10px rgba(226, 149, 90, 0.4); }
.game-hud__boards-you {
  margin: var(--pad-2xs) 0 0;
  text-align: center;
  font-size: var(--font-2xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(246, 201, 91, 0.8);
}
.game-hud__boards-you[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .game-hud__streak-flame,
  .game-hud__streak.is-tick,
  .game-hud__streak[data-tier="6"],
  .streak-flash__rays,
  .game-hud__over-streak.is-record { animation: none; }
  .streak-flash { animation-duration: 900ms; }
  .game-stage.is-streak-pulse::after { animation-duration: 300ms; }
}

/* ================================================================
   STAGE CLEARED banner
   ================================================================
   The loudest screen in the game. Rays behind, crest stamping in
   from above, CLEARED set at banner scale, and the run's numbers
   underneath as a receipt. Everything is staggered off one 120ms
   step so the whole thing assembles rather than appears.
   ================================================================ */
.stagewin__rays {
  position: absolute;
  inset: -50%;
  background: repeating-conic-gradient(from 0deg at 50% 50%,
    rgba(246, 201, 91, 0) 0deg,
    rgba(246, 201, 91, 0.16) 2deg,
    rgba(246, 201, 91, 0) 8deg);
  animation: stagewinRays 34s linear infinite, stagewinFade 900ms ease-out both;
  pointer-events: none;
}
@keyframes stagewinRays { to { transform: rotate(360deg); } }
@keyframes stagewinFade { from { opacity: 0; } to { opacity: 1; } }

.stagewin {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(2px, 1cqmin, 8px);
  padding: clamp(12px, 5cqmin, 40px);
  text-align: center;
}
.stagewin__crest {
  width: clamp(38px, 14cqmin, 92px);
  filter: drop-shadow(0 6px 18px rgba(246, 201, 91, 0.45));
  animation: stagewinStamp 620ms cubic-bezier(0.2, 1.5, 0.3, 1) both;
}
/* The crest lands first and everything else follows it in — the
   stamp is the beat the sound cue's low strike is written against. */
@keyframes stagewinStamp {
  0%   { opacity: 0; transform: translateY(-18%) scale(1.8); filter: blur(6px); }
  60%  { opacity: 1; filter: blur(0); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
/* ================================================================
   Chapter card — the seam between one game and the next
   ================================================================
   Shares the stage-win typography on purpose: same house, different
   sentence. That one looks back and carries numbers; this one looks
   forward and carries a name. */
.chapter {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(10px, 2.4cqmin, 22px);
  padding: 0 8cqi;
  text-align: center;
  z-index: 2;
  /* NO BACKGROUND. The card is a title laid over the opening frame of
     the scene it introduces — the yard, seen from outside the south
     wall — so that dismissing it doesn't cut to a different picture.
     This band is only enough to keep the words legible over whatever
     the camera happens to be looking at. */
  background: linear-gradient(to bottom,
    transparent 0%,
    rgba(8, 5, 3, 0.55) 26%,
    rgba(8, 5, 3, 0.72) 50%,
    rgba(8, 5, 3, 0.55) 74%,
    transparent 100%);
}
.chapter__rule {
  width: min(58cqi, 340px);
  height: 1px;
  background: linear-gradient(90deg,
    rgba(217, 178, 107, 0) 0%,
    rgba(217, 178, 107, 0.85) 50%,
    rgba(217, 178, 107, 0) 100%);
  animation: chapterWipe 720ms cubic-bezier(0.2, 0.9, 0.2, 1) both;
}
.chapter__eyebrow {
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(9px, 2.8cqmin, 17px);
  letter-spacing: 0.55em;
  text-indent: 0.55em;
  text-transform: uppercase;
  color: rgba(255, 236, 194, 0.78);
  animation: stagewinUp 520ms cubic-bezier(0.2, 1.2, 0.3, 1) 200ms both;
}
.chapter__title {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(26px, 11cqmin, 76px);
  line-height: 1.02;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  background: linear-gradient(180deg, #fffdf5 0%, #f6c95b 52%, #a76a1a 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: stagewinUp 620ms cubic-bezier(0.2, 1.1, 0.3, 1) 320ms both;
}
.chapter__line {
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 600;
  font-size: clamp(12px, 3.4cqmin, 20px);
  line-height: 1.45;
  max-width: 34ch;
  color: rgba(238, 228, 210, 0.9);
  animation: stagewinUp 560ms cubic-bezier(0.2, 1.1, 0.3, 1) 520ms both;
}
@keyframes chapterWipe {
  0%   { opacity: 0; transform: scaleX(0.2); }
  100% { opacity: 1; transform: scaleX(1); }
}
@media (prefers-reduced-motion: reduce) {
  .chapter__rule, .chapter__eyebrow, .chapter__title, .chapter__line {
    animation-duration: 200ms !important;
    animation-delay: 0ms !important;
  }
}

.stagewin__eyebrow {
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(9px, 3cqmin, 18px);
  letter-spacing: 0.55em;
  text-indent: 0.55em;          /* balance the tracking on the last glyph */
  text-transform: uppercase;
  color: rgba(255, 236, 194, 0.8);
  animation: stagewinUp 520ms cubic-bezier(0.2, 1.2, 0.3, 1) 180ms both;
}
.stagewin__title {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(34px, 15cqmin, 104px);
  line-height: 0.98;
  letter-spacing: 0.04em;
  background: linear-gradient(180deg, #fffdf5 0%, #f6c95b 52%, #a76a1a 100%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  filter: drop-shadow(0 4px 22px rgba(246, 201, 91, 0.45));
  animation: stagewinTitle 700ms cubic-bezier(0.16, 1.1, 0.3, 1) 300ms both;
}
@keyframes stagewinTitle {
  0%   { opacity: 0; transform: scale(1.22); letter-spacing: 0.3em; }
  100% { opacity: 1; transform: scale(1);    letter-spacing: 0.04em; }
}
.stagewin__rule {
  width: min(58cqmin, 420px);
  height: 2px;
  background: linear-gradient(90deg, transparent, rgba(246, 201, 91, 0.9), transparent);
  animation: stagewinRule 620ms cubic-bezier(0.2, 1, 0.3, 1) 480ms both;
}
@keyframes stagewinRule {
  from { opacity: 0; transform: scaleX(0.15); }
  to   { opacity: 1; transform: scaleX(1); }
}
.stagewin__stats {
  display: flex;
  gap: clamp(10px, 5cqmin, 46px);
  margin-top: clamp(4px, 2cqmin, 16px);
  animation: stagewinUp 560ms cubic-bezier(0.2, 1.2, 0.3, 1) 600ms both;
}
.stagewin__stat-value {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(15px, 5.4cqmin, 34px);
  color: #ffe6a8;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.6);
}
.stagewin__stat-label {
  font-family: "Rajdhani", system-ui, sans-serif;
  font-size: clamp(7px, 2.2cqmin, 12px);
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  color: rgba(214, 206, 190, 0.7);
}
@keyframes stagewinUp {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .stagewin__rays { animation: stagewinFade 400ms ease-out both; }
  .stagewin__crest, .stagewin__eyebrow, .stagewin__title,
  .stagewin__rule, .stagewin__stats {
    animation-duration: 260ms;
    animation-delay: 0ms;
  }
}

/* ================================================================
   Drive-into-the-workshop cinematic
   ================================================================
   Fires when the player answers PULL OVER. Four beats of 240 ms,
   the same grid the `shop_dive` audio cue is built on (and the
   DIVE_BEAT constant in games/runner/main.js):

     beat 0   wind-up — the world pulls back a hair while speed
                        streaks bloom out of the shop
     beat 0-3 dolly   — the camera pushes INTO the building. The fg
                        canvas scales far harder than the bg, so this
                        is a parallax push with real depth, not a
                        flat zoom of a flat picture.
     beat 3   impact  — gold bloom blows the frame out, two shockwave
                        rings snap outward, the stage kicks.
     beat 3-6 settle  — the camera pulls back to 1x under the
                        dissolving bloom, landing on the parked trucks
                        just as the shopkeeper conversation fades up.
                        The pull-back is part of the ANIMATION, not a
                        class removal: the scene that follows is a
                        semi-transparent dialogue overlay, so a snap
                        back to 1x would be plainly visible.

   --dive-x / --dive-y are set by the runner from the building's live
   on-screen position, so every layer here — the transform origin,
   the streak hub, the ring centre, the bloom core — converges on the
   actual doorway. Nothing is hard-coded to the middle of the screen.
   ================================================================ */
.game-stage.is-shop-dive .game-canvas {
  transform-origin: var(--dive-x, 85%) var(--dive-y, 22%);
  will-change: transform, filter;
}
/* Foreground: road, vehicles, the workshop itself. This is the layer
   the camera is actually flying into.

   Timing is expressed as percentages of 1400ms so the three phases
   land on the beat grid: wind-up bottoms out at 154ms (11%), the
   dive peaks at 720ms (51.4% — beat 3, with the bloom), and the
   pull-back finishes at 1400ms. Each segment carries its own
   timing-function, declared on the keyframe it starts from, because
   one curve can't be both a slow wind-up and a hard acceleration. */
.game-stage.is-shop-dive .game-canvas--fg {
  animation: shopDiveFg 1400ms linear forwards;
}
/* Background parallax plate — same origin, a fraction of the travel.
   The ratio between the two scales IS the depth cue. */
.game-stage.is-shop-dive .game-canvas--bg {
  animation: shopDiveBg 1400ms linear forwards;
}
@keyframes shopDiveFg {
  0%     { transform: scale(1);     filter: none;
           animation-timing-function: cubic-bezier(0.3, 0, 0.7, 1); }
  11%    { transform: scale(0.972); filter: saturate(1.05);
           animation-timing-function: cubic-bezier(0.5, 0, 0.78, 0.36); }
  51.4%  { transform: scale(2.75);  filter: saturate(1.35) brightness(1.45) contrast(1.06);
           animation-timing-function: cubic-bezier(0.16, 0.9, 0.28, 1); }
  100%   { transform: scale(1);     filter: none; }
}
@keyframes shopDiveBg {
  0%     { transform: scale(1);
           animation-timing-function: cubic-bezier(0.3, 0, 0.7, 1); }
  11%    { transform: scale(0.99);
           animation-timing-function: cubic-bezier(0.5, 0, 0.78, 0.36); }
  51.4%  { transform: scale(1.62);
           animation-timing-function: cubic-bezier(0.16, 0.9, 0.28, 1); }
  100%   { transform: scale(1); }
}

/* Impact kick. Lives on the stage (not the canvases) so it can't
   fight the dolly transform, and so the HUD shakes with the world —
   a camera knock moves everything or it reads as a glitch. */
.game-stage.is-shop-dive-impact { animation: shopDiveKick 380ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both; }
@keyframes shopDiveKick {
  0%, 100%  { transform: translate3d(0, 0, 0); }
  12%       { transform: translate3d(-6px, 3px, 0); }
  26%       { transform: translate3d(7px, -4px, 0); }
  42%       { transform: translate3d(-5px, 2px, 0); }
  58%       { transform: translate3d(4px, -2px, 0); }
  76%       { transform: translate3d(-2px, 1px, 0); }
}

.shop-dive {
  position: absolute;
  inset: 0;
  /* ABOVE the cutscene layer (4) and the shop panel (5),
     deliberately. The shopkeeper conversation fades up while the
     bloom is still white-hot, so the scene is uncovered by the light
     dissolving rather than pasted over a dark screen. pointer-events
     stay off, so taps still reach the conversation underneath. */
  z-index: 6;
  pointer-events: none;
  overflow: hidden;
}

/* Speed streaks — fine radial spokes centred on the doorway, so the
   frame reads as rushing toward it. A conic gradient gives us a few
   hundred spokes for one paint; the radial mask keeps the hub clear
   (spokes converging to a point turn into mush) and fades the ends. */
.shop-dive__streaks {
  position: absolute;
  inset: 0;
  background: repeating-conic-gradient(from 0deg at var(--dive-x, 85%) var(--dive-y, 22%),
    rgba(255, 236, 194, 0) 0deg,
    rgba(255, 236, 194, 0.55) 0.35deg,
    rgba(255, 210, 140, 0.22) 0.7deg,
    rgba(255, 236, 194, 0) 2.1deg);
  -webkit-mask-image: radial-gradient(circle at var(--dive-x, 85%) var(--dive-y, 22%),
    transparent 0%, transparent 12%, #000 34%, #000 62%, transparent 96%);
          mask-image: radial-gradient(circle at var(--dive-x, 85%) var(--dive-y, 22%),
    transparent 0%, transparent 12%, #000 34%, #000 62%, transparent 96%);
  transform-origin: var(--dive-x, 85%) var(--dive-y, 22%);
  opacity: 0;
  animation: shopDiveStreaks 720ms cubic-bezier(0.5, 0, 0.78, 0.36) forwards;
}
@keyframes shopDiveStreaks {
  0%   { opacity: 0;    transform: scale(0.55) rotate(0deg); }
  30%  { opacity: 0.85; }
  100% { opacity: 0;    transform: scale(3.4) rotate(7deg); }
}

/* Shockwave rings — a hard leading edge and a softer echo 90 ms
   behind it. Both start as a 12 px dot AT the doorway and scale out
   past the stage edge, thinning as they go. */
.shop-dive__ring {
  position: absolute;
  left: var(--dive-x, 85%);
  top:  var(--dive-y, 22%);
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  border: 5px solid rgba(255, 232, 176, 0.95);
  box-shadow:
    0 0 34px rgba(255, 198, 104, 0.85),
    inset 0 0 22px rgba(255, 226, 160, 0.6);
  opacity: 0;
  animation: shopDiveRing 640ms cubic-bezier(0.12, 0.82, 0.28, 1) 720ms forwards;
}
.shop-dive__ring--2 {
  border-color: rgba(255, 174, 92, 0.7);
  border-width: 3px;
  animation-delay: 810ms;
  animation-duration: 700ms;
}
@keyframes shopDiveRing {
  0%   { opacity: 0;    transform: scale(1);  border-width: 6px; }
  10%  { opacity: 1; }
  100% { opacity: 0;    transform: scale(78); border-width: 1px; }
}

/* The bloom. Peaks exactly on beat 3 (delay + its own 14% ramp), holds
   white-hot while the shop panel fades up underneath, then dissolves
   — so the workshop interior is revealed BY the light. */
.shop-dive__bloom {
  position: absolute;
  inset: 0;
  transform-origin: var(--dive-x, 85%) var(--dive-y, 22%);
  background: radial-gradient(circle at var(--dive-x, 85%) var(--dive-y, 22%),
    rgba(255, 255, 253, 1)    0%,
    rgba(255, 246, 224, 0.99) 16%,
    rgba(255, 216, 142, 0.94) 33%,
    rgba(228, 152, 66, 0.74)  52%,
    rgba(70, 36, 12, 0.38)    74%,
    transparent 94%);
  opacity: 0;
  animation: shopDiveBloom 1120ms cubic-bezier(0.2, 0.9, 0.3, 1) 570ms forwards;
}
@keyframes shopDiveBloom {
  0%   { opacity: 0;    transform: scale(0.22); }
  14%  { opacity: 1;    transform: scale(1.06); }
  46%  { opacity: 0.96; transform: scale(1.28); }
  100% { opacity: 0;    transform: scale(1.7); }
}

/* Reduced motion: keep the beat and the light, drop the travel. The
   dolly, streaks and camera kick are exactly the parts that provoke
   motion sickness; a bloom that fades up and out still carries the
   "we've arrived" moment. */
@media (prefers-reduced-motion: reduce) {
  .game-stage.is-shop-dive .game-canvas--fg,
  .game-stage.is-shop-dive .game-canvas--bg,
  .game-stage.is-shop-dive-impact,
  .shop-dive__streaks,
  .shop-dive__ring { animation: none; }
  .shop-dive__streaks,
  .shop-dive__ring { opacity: 0; }
  .shop-dive__bloom { animation-duration: 900ms; animation-delay: 620ms; }
}

/* ================================================================
   In-run shop — blaster upgrades
   ================================================================
   Full-stage overlay mounted inside .game-stage by shop.js. Uses the
   forge/gold aesthetic. Sized with cqi/cqmin so the 3x3 upgrade
   grid, wallet strip, and close button all fit on quarter-slice
   desktop (1920x540), full desktop (1920x1080), and portrait phone
   (400x720) without scrolling the panel itself. Long resource
   inventories scroll INSIDE the wallet strip; the card grid uses
   fixed 3 columns and shrinks its typography instead of wrapping.
   ================================================================ */
.shop {
  position: absolute;
  inset: 0;
  z-index: 5;   /* above cutscene + hud */
  color: #f4ead6;
  font-family: "Rajdhani", system-ui, sans-serif;
  animation: shopFadeIn 260ms ease-out both;
}
@keyframes shopFadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.shop__scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(70% 60% at 50% 40%, rgba(10, 6, 2, 0.82), rgba(6, 4, 2, 0.94) 65%, rgba(0, 0, 0, 0.96) 100%);
}
.shop__panel {
  position: absolute;
  inset: clamp(8px, 3cqi, 22px);
  display: flex;
  flex-direction: column;
  gap: clamp(6px, 2cqmin, 14px);
  padding: clamp(10px, 3cqmin, 20px);
  /* Room background — the weaponsShopBackground.png fills the panel
     from behind, with a dark gradient veil on top so the glass UI
     tiles stay legible against the busy interior art. */
  background:
    linear-gradient(180deg,
      rgba(6, 4, 2, 0.55) 0%,
      rgba(6, 4, 2, 0.35) 30%,
      rgba(6, 4, 2, 0.55) 70%,
      rgba(6, 4, 2, 0.80) 100%),
    url('/game/content/rooms/weaponsShopBackground.png') center / cover no-repeat,
    linear-gradient(180deg, rgba(30, 18, 8, 0.96), rgba(14, 8, 3, 0.98));
  border: 2px solid rgba(255, 220, 130, 0.85);
  border-radius: clamp(10px, 3cqmin, 18px);
  box-shadow:
    0 12px 40px rgba(0, 0, 0, 0.75),
    inset 0 0 32px rgba(255, 200, 100, 0.20),
    0 0 60px rgba(255, 200, 100, 0.25);
  overflow: hidden;
  animation: shopPanelIn 380ms cubic-bezier(0.2, 1.4, 0.3, 1) both;
}
@keyframes shopPanelIn {
  from { transform: translateY(24px) scale(0.94); opacity: 0; }
  to   { transform: translateY(0)    scale(1);    opacity: 1; }
}

.shop__header { text-align: center; }
.shop__title {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-xl);
  letter-spacing: 0.14em;
  color: rgba(255, 220, 130, 1);
  text-shadow: 0 2px 0 rgba(120, 60, 10, 0.7), 0 0 18px rgba(255, 190, 80, 0.35);
}
.shop__subtitle {
  font-size: var(--font-xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(244, 234, 214, 0.72);
  margin-top: clamp(2px, 0.6cqmin, 6px);
}

/* Wallet strip — the "YOU HAVE" row. Chips wrap on narrow stages.
   Glass tile over the workshop backdrop. */
.shop__wallet {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(6px, 1.6cqmin, 12px);
  padding: clamp(4px, 1.4cqmin, 10px) clamp(8px, 2cqmin, 14px);
  background: rgba(6, 4, 2, 0.55);
  backdrop-filter: blur(6px) saturate(1.1);
  -webkit-backdrop-filter: blur(6px) saturate(1.1);
  border: 1px solid rgba(255, 220, 130, 0.45);
  border-radius: 999px;
  font-size: var(--font-sm);
}
.shop__wallet-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-weight: 700;
  color: rgba(255, 235, 190, 0.95);
}
.shop__wallet-chip b { color: #fff5c0; font-weight: 800; }

/* ---- Paying for something -----------------------------------------
   The chip recoils as the coins leave it — a short kick away from the
   count, and a warm flash that decays. Enough to draw the eye to the
   number without pretending a purchase is damage. */
.shop__wallet-chip.is-spending {
  animation: shopSpendKick 460ms cubic-bezier(0.2, 0.9, 0.3, 1) both;
}
@keyframes shopSpendKick {
  0%   { transform: translateY(0)     scale(1);    filter: brightness(1); }
  22%  { transform: translateY(-3px)  scale(1.09); filter: brightness(1.7); }
  55%  { transform: translateY(2px)   scale(0.97); filter: brightness(1.1); }
  100% { transform: translateY(0)     scale(1);    filter: brightness(1); }
}

/* The coins themselves. A layer pinned over the panel so they can fall
   past the wallet strip and out of the frame; pointer-events off so
   they never eat a tap meant for the card underneath. */
.shop__spend-layer {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 8;
}
.shop__spend-coin {
  position: absolute;
  left: 0; top: 0;
  width: clamp(11px, 3cqi, 17px);
  height: clamp(11px, 3cqi, 17px);
  transform: translate(calc(var(--x) - 50%), calc(var(--y) - 50%));
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.6));
  animation: shopCoinFly 720ms cubic-bezier(0.25, 0.6, 0.4, 1) both;
}
.shop__spend-coin svg { width: 100%; height: 100%; display: block; }
/* Out and DOWN, tumbling. Money falling away from the pile, not
   travelling toward the thing being bought — leaving is the feeling. */
@keyframes shopCoinFly {
  0% {
    opacity: 0;
    transform: translate(calc(var(--x) - 50%), calc(var(--y) - 50%)) scale(0.5) rotate(0deg);
  }
  18% {
    opacity: 1;
    transform: translate(calc(var(--x) - 50% + var(--dx) * 0.32),
                         calc(var(--y) - 50% - 14px)) scale(1.12) rotate(calc(var(--rot) * 0.3));
  }
  100% {
    opacity: 0;
    transform: translate(calc(var(--x) - 50% + var(--dx)),
                         calc(var(--y) - 50% + var(--dy))) scale(0.82) rotate(var(--rot));
  }
}
@media (prefers-reduced-motion: reduce) {
  .shop__wallet-chip.is-spending { animation-duration: 160ms; }
  .shop__spend-coin { animation-duration: 240ms; }
}

/* Upgrade grid — 3 columns (tracks), 3 rows (levels). Fills the
   remaining vertical space and each card stretches to fit. */
.shop__grid {
  flex: 1 1 auto;
  min-height: 0;   /* let grid children shrink instead of overflowing */
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(6px, 1.8cqmin, 12px);
}
.shop__track {
  display: grid;
  grid-template-rows: auto repeat(3, 1fr);
  gap: clamp(4px, 1.4cqmin, 10px);
  min-height: 0;
}
.shop__track-name {
  text-align: center;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-md);
  letter-spacing: 0.18em;
  color: rgba(255, 220, 130, 0.98);
  text-transform: uppercase;
  padding: clamp(2px, 0.6cqmin, 6px) 0;
  border-bottom: 1px solid rgba(255, 220, 130, 0.35);
}

.shop__card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: clamp(2px, 0.8cqmin, 6px);
  padding: clamp(6px, 1.8cqmin, 12px);
  /* Glass tile — dark translucent over the workshop bg, gold rim. */
  background: linear-gradient(180deg, rgba(30, 18, 8, 0.72), rgba(10, 6, 2, 0.82));
  backdrop-filter: blur(6px) saturate(1.15);
  -webkit-backdrop-filter: blur(6px) saturate(1.15);
  border: 1.5px solid rgba(255, 220, 130, 0.55);
  border-radius: clamp(6px, 1.8cqmin, 12px);
  box-shadow: inset 0 1px 0 rgba(255, 220, 130, 0.15), 0 4px 10px rgba(0, 0, 0, 0.4);
  min-height: 0;
  overflow: hidden;
}
.shop__card--afford {
  border-color: rgba(255, 220, 130, 0.98);
  box-shadow:
    inset 0 1px 0 rgba(255, 245, 214, 0.25),
    0 4px 12px rgba(0, 0, 0, 0.4),
    0 0 22px rgba(255, 200, 100, 0.35);
}
.shop__card--owned {
  border-color: rgba(120, 210, 130, 0.85);
  box-shadow: inset 0 0 18px rgba(120, 210, 130, 0.2), 0 4px 10px rgba(0, 0, 0, 0.4);
  opacity: 0.92;
}
.shop__card--locked { opacity: 0.42; filter: grayscale(0.5); }
.shop__card--broke  { opacity: 0.78; }
.shop__card--shake  { animation: shopShake 360ms ease-out; }
@keyframes shopShake {
  0%, 100% { transform: translateX(0); }
  20%      { transform: translateX(-6px); }
  40%      { transform: translateX(6px); }
  60%      { transform: translateX(-4px); }
  80%      { transform: translateX(3px); }
}

.shop__card-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 6px;
}
.shop__card-level {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-sm);
  color: rgba(255, 245, 214, 0.95);
  letter-spacing: 0.1em;
}
.shop__badge {
  font-size: var(--font-2xs);
  font-weight: 800;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: 4px;
}
.shop__badge--owned  { background: rgba(80, 180, 90, 0.9);  color: #0e2010; }
.shop__badge--locked { background: rgba(70, 55, 40, 0.9);   color: rgba(244, 234, 214, 0.7); }

.shop__card-desc {
  font-size: var(--font-xs);
  line-height: 1.28;
  color: rgba(244, 234, 214, 0.9);
  min-height: 0;
  overflow: hidden;
}

.shop__cost {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(4px, 1cqmin, 8px);
  margin-top: auto;
  font-size: var(--font-xs);
}
.shop__chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 6px;
  background: rgba(0, 0, 0, 0.32);
  border: 1px solid rgba(255, 220, 130, 0.4);
  border-radius: 999px;
  font-weight: 700;
  color: rgba(255, 235, 190, 0.95);
}
.shop__card--broke .shop__chip {
  border-color: rgba(230, 100, 80, 0.65);
  color: rgba(255, 190, 170, 0.95);
}

.shop__buy {
  margin-top: clamp(4px, 1.2cqmin, 8px);
  padding: clamp(4px, 1.2cqmin, 8px) clamp(8px, 2cqmin, 14px);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-sm);
  letter-spacing: 0.16em;
  color: #1a0f04;
  background: linear-gradient(180deg, #ffe090, #d8a040);
  border: 1.5px solid rgba(120, 70, 15, 0.85);
  border-radius: 6px;
  cursor: pointer;
  transition: transform 120ms ease, box-shadow 120ms ease;
  box-shadow: 0 2px 0 rgba(120, 70, 15, 0.9), inset 0 1px 0 rgba(255, 245, 214, 0.7);
}
.shop__buy:hover { transform: translateY(-1px); box-shadow: 0 3px 0 rgba(120, 70, 15, 0.9), inset 0 1px 0 rgba(255, 245, 214, 0.7); }
.shop__buy:active { transform: translateY(1px); box-shadow: 0 1px 0 rgba(120, 70, 15, 0.9); }
.shop__buy--disabled {
  background: linear-gradient(180deg, #5a4a30, #362a1a);
  color: rgba(244, 234, 214, 0.55);
  cursor: not-allowed;
  box-shadow: 0 2px 0 rgba(40, 24, 10, 0.9), inset 0 1px 0 rgba(255, 245, 214, 0.1);
}
.shop__buy--disabled:hover { transform: none; }
/* Settle window (~700 ms after shop opens) — BUY buttons refuse
   clicks so an anxious tap-through from the preceding conversation
   can't silently purchase the flagship upgrade. Visually greyed so
   the state is legible. */
.shop.is-settling .shop__buy {
  pointer-events: none;
  opacity: 0.45;
  filter: grayscale(0.5);
}

.shop__foot {
  display: flex;
  justify-content: center;
  padding-top: clamp(4px, 1cqmin, 8px);
}
.shop__close {
  padding: clamp(6px, 1.8cqmin, 12px) clamp(14px, 4cqmin, 26px);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-md);
  letter-spacing: 0.18em;
  color: rgba(255, 220, 130, 0.95);
  background: rgba(0, 0, 0, 0.4);
  border: 1.5px solid rgba(255, 220, 130, 0.7);
  border-radius: 8px;
  cursor: pointer;
  transition: background 120ms ease, transform 120ms ease;
}
.shop__close:hover { background: rgba(255, 200, 100, 0.18); transform: translateY(-1px); }
.shop__close:active { transform: translateY(1px); }
/* Once the deal is taken there is nothing else to do in here, so the
   exit stops being a quiet ghost button and becomes the CTA. */
.shop__close--primary {
  color: #2a1a08;
  background: linear-gradient(180deg, #fff3cf 0%, #f6c95b 55%, #a76a1a 100%);
  border-color: rgba(90, 60, 20, 0.9);
  animation: shopClosePulse 1.8s ease-in-out infinite;
}
@keyframes shopClosePulse {
  0%, 100% { box-shadow: 0 0 0 rgba(246, 201, 91, 0); }
  50%      { box-shadow: 0 0 22px rgba(246, 201, 91, 0.65); }
}

/* ================================================================
   RIG's deal — the first-visit panel
   ================================================================
   One card instead of a 3x3 grid. Everything here is scaled to read
   as an event: the gold is brighter than the rest of the shop, the
   name is set in the display face at banner size, and the nine bolt
   glyphs animate in one at a time so the player literally watches
   the spread they're buying get wider.
   ================================================================ */
.shop__panel--deal { justify-content: center; }
.shop__deal {
  position: relative;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(3px, 1.1cqmin, 9px);
  padding: clamp(8px, 2.6cqmin, 20px) clamp(10px, 3cqmin, 24px);
  border-radius: 14px;
  border: 1.5px solid rgba(246, 201, 91, 0.55);
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(246, 201, 91, 0.16), transparent 62%),
    linear-gradient(180deg, rgba(38, 26, 14, 0.92), rgba(14, 10, 6, 0.96));
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 18px 44px rgba(0, 0, 0, 0.55);
  overflow: hidden;
  text-align: center;
}
/* Slow sweep of light across the card — it should never look static. */
.shop__deal-glow {
  position: absolute;
  inset: -40%;
  background: conic-gradient(from 0deg,
    transparent 0deg, rgba(246, 201, 91, 0.14) 30deg, transparent 70deg,
    transparent 180deg, rgba(255, 236, 194, 0.10) 210deg, transparent 250deg);
  animation: dealSweep 9s linear infinite;
  pointer-events: none;
}
@keyframes dealSweep { to { transform: rotate(360deg); } }
.shop__deal-eyebrow {
  position: relative;
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(8px, 2.4cqmin, 13px);
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: #ff9a5a;
}
.shop__deal-name {
  position: relative;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(20px, 7.4cqmin, 46px);
  line-height: 1.02;
  letter-spacing: 0.06em;
  background: linear-gradient(180deg, #fff8e2 0%, #f6c95b 55%, #b9791f 100%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  filter: drop-shadow(0 3px 10px rgba(246, 201, 91, 0.35));
}
/* The spread preview: one glyph per bolt she's about to fire, lit
   left-to-right so the width of the volley registers as a fact. */
.shop__deal-spread {
  position: relative;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: clamp(3px, 1.2cqmin, 8px);
  height: clamp(16px, 5cqmin, 30px);
  margin: clamp(2px, 1cqmin, 8px) 0;
}
.shop__deal-bolt {
  width: clamp(3px, 0.9cqmin, 5px);
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(180deg, #fff5fa 0%, #ffb0e0 45%, #ff5cd0 100%);
  box-shadow: 0 0 10px rgba(255, 92, 208, 0.8);
  opacity: 0;
  transform: scaleY(0.2);
  transform-origin: bottom;
  animation: dealBolt 420ms cubic-bezier(0.2, 1.4, 0.3, 1) forwards;
  animation-delay: calc(var(--i) * 55ms + 260ms);
}
@keyframes dealBolt {
  to { opacity: 1; transform: scaleY(1); }
}
.shop__deal-stats {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(4px, 1.6cqmin, 12px);
}
.shop__deal-stats li {
  padding: clamp(2px, 0.8cqmin, 5px) clamp(6px, 2cqmin, 12px);
  border-radius: 999px;
  border: 1px solid rgba(246, 201, 91, 0.35);
  background: rgba(246, 201, 91, 0.08);
  font-size: clamp(8px, 2.4cqmin, 14px);
  letter-spacing: 0.08em;
  color: rgba(255, 240, 210, 0.94);
  white-space: nowrap;
}
.shop__deal-from {
  position: relative;
  max-width: 34ch;
  font-size: clamp(8px, 2.3cqmin, 13px);
  font-style: italic;
  color: rgba(226, 214, 194, 0.72);
}
.shop__deal-price {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(13px, 4cqmin, 24px);
  color: #ffe6a8;
}
.shop__deal-buy {
  position: relative;
  margin-top: clamp(2px, 1cqmin, 8px);
  padding: clamp(6px, 2cqmin, 14px) clamp(16px, 6cqmin, 40px);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(11px, 3.4cqmin, 20px);
  letter-spacing: 0.16em;
  color: #2a1a08;
  background: linear-gradient(180deg, #fff3cf 0%, #f6c95b 55%, #a76a1a 100%);
  border: 1.5px solid rgba(90, 60, 20, 0.9);
  border-radius: 10px;
  cursor: pointer;
  box-shadow: 0 3px 0 rgba(70, 40, 10, 0.9), 0 10px 26px rgba(0, 0, 0, 0.5);
  transition: transform 120ms ease-out, box-shadow 120ms ease-out;
}
.shop__deal-buy:hover  { transform: translateY(-2px); box-shadow: 0 5px 0 rgba(70, 40, 10, 0.9), 0 14px 32px rgba(0, 0, 0, 0.55); }
.shop__deal-buy:active { transform: translateY(2px);  box-shadow: 0 1px 0 rgba(70, 40, 10, 0.9), 0 6px 16px rgba(0, 0, 0, 0.45); }
/* Shown in place of nothing when the player is short. Rig covers the
   rest — see dealHtml. Quiet: it is a kindness, not an alarm. */
.shop__deal-waived {
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 600;
  font-size: clamp(11px, 2.9cqi, 15px);
  color: rgba(255, 228, 178, 0.82);
  text-align: center;
  margin-bottom: 0.5em;
}

.shop__deal-buy--disabled {
  filter: grayscale(0.75) brightness(0.7);
  cursor: not-allowed;
}
.shop__deal-taken {
  position: relative;
  margin-top: clamp(2px, 1cqmin, 8px);
  padding: clamp(6px, 2cqmin, 14px) clamp(16px, 6cqmin, 40px);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(11px, 3.4cqmin, 20px);
  letter-spacing: 0.16em;
  color: #7ee08a;
  border: 1.5px solid rgba(126, 224, 138, 0.5);
  border-radius: 10px;
  background: rgba(126, 224, 138, 0.08);
}
.shop__deal--flash {
  animation: dealFlash 620ms cubic-bezier(0.2, 1.3, 0.3, 1);
}
@keyframes dealFlash {
  0%   { box-shadow: 0 0 0 rgba(255, 236, 194, 0); transform: scale(1); }
  30%  { box-shadow: 0 0 70px rgba(255, 236, 194, 0.95); transform: scale(1.03); }
  100% { box-shadow: 0 0 0 rgba(255, 236, 194, 0); transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .shop__deal-glow { animation: none; }
  .shop__deal-bolt { animation-duration: 1ms; }
  .shop__close--primary { animation: none; }
}

/* ================================================================
   Hero equip modal — click a hero card, this opens with the hero at
   center and mount-point squares laid over the pixel offsets from
   the catalog. Inventory drawer at the bottom lists owned items.
   Full-stage overlay, same mount pattern as .shop.
   ================================================================ */
.hero-equip {
  position: absolute;
  inset: 0;
  z-index: 5;
  color: #f4ead6;
  font-family: "Rajdhani", system-ui, sans-serif;
  animation: heroEquipFadeIn 260ms ease-out both;
}
@keyframes heroEquipFadeIn { from { opacity: 0 } to { opacity: 1 } }
.hero-equip__scrim {
  position: absolute; inset: 0;
  background: radial-gradient(70% 60% at 50% 45%,
              rgba(10, 6, 2, 0.85), rgba(6, 4, 2, 0.95) 65%, rgba(0, 0, 0, 0.97) 100%);
}
.hero-equip__panel {
  position: absolute;
  inset: clamp(8px, 3cqi, 22px);
  display: flex;
  flex-direction: column;
  gap: clamp(6px, 2cqmin, 14px);
  padding: clamp(10px, 3cqmin, 20px);
  background: linear-gradient(180deg, rgba(30, 18, 8, 0.94), rgba(14, 8, 3, 0.98));
  border: 2px solid rgba(255, 220, 130, 0.85);
  border-radius: clamp(10px, 3cqmin, 18px);
  box-shadow:
    0 12px 40px rgba(0, 0, 0, 0.75),
    inset 0 0 32px rgba(255, 200, 100, 0.14),
    0 0 60px rgba(255, 200, 100, 0.22);
  overflow: hidden;
  animation: heroEquipPanelIn 380ms cubic-bezier(0.2, 1.4, 0.3, 1) both;
}
@keyframes heroEquipPanelIn {
  from { transform: translateY(24px) scale(0.94); opacity: 0 }
  to   { transform: translateY(0)    scale(1);    opacity: 1 }
}
.hero-equip__header {
  display: flex; align-items: center; justify-content: space-between;
  gap: clamp(6px, 2cqmin, 12px);
}
.hero-equip__title {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-lg);
  letter-spacing: 0.14em;
  color: rgba(255, 220, 130, 1);
  text-shadow: 0 2px 0 rgba(120, 60, 10, 0.7), 0 0 18px rgba(255, 190, 80, 0.35);
}
.hero-equip__x {
  width: clamp(28px, 6cqmin, 40px);
  height: clamp(28px, 6cqmin, 40px);
  border: 1.5px solid rgba(255, 220, 130, 0.7);
  background: rgba(0, 0, 0, 0.35);
  border-radius: 999px;
  color: rgba(255, 220, 130, 0.9);
  font-size: var(--font-lg);
  font-weight: 700;
  cursor: pointer;
  line-height: 1;
}
.hero-equip__x:hover { background: rgba(255, 200, 100, 0.18); }
/* Settle window (~800 ms after open) — close controls refuse taps so
   an anxious tap-through can't dismiss the modal before the player
   sees it. Visually dimmed so the state is legible. */
.hero-equip.is-settling .hero-equip__x,
.hero-equip.is-settling .hero-equip__close-btn {
  pointer-events: none;
  opacity: 0.4;
  filter: grayscale(0.4);
}

.hero-equip__stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: clamp(6px, 1.8cqmin, 12px);
  background:
    radial-gradient(60% 60% at 50% 50%, rgba(255, 200, 100, 0.10), transparent 70%),
    linear-gradient(180deg, rgba(0, 0, 0, 0.30), rgba(0, 0, 0, 0.55));
  /* Reserve space above the bike-frame for mount squares that
     project past its top edge (front-slot on motorbike/car). Without
     this, on short stages the header pushes the bike up and the
     mount collides with the "EQUIP …" title. clamp caps growth on
     huge stages while ensuring at least ~90 px of clearance on
     phone-sized ones. */
  padding-top: clamp(90px, 14cqmin, 140px);
}
/* Bike frame — sizes to the image (inline-block wrapper) so the
   mount-layer that inset:0's on top of it hugs the bike exactly.
   overflow: visible lets mount squares that live above the top of
   the image (like the FRONT slot at slot_offset y=-70) render past
   the frame bounds into the stage's empty space. Without the frame
   wrapper, the mount layer inset:0'd on the whole stage, so on tall
   stages the mount tracked to the stage top-edge and collided with
   the modal header. */
.hero-equip__frame {
  position: relative;
  display: inline-block;
  overflow: visible;
  /* Allow inner-shadow'd stage background to bleed under the image
     — no bg here, purely a positioning wrapper. */
}
.hero-equip__hero-img {
  display: block;
  filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.7));
}
.hero-equip__mount-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: visible;
}
.hero-equip__mount {
  position: absolute;
  /* width + height set inline by placeMounts() so the square
     matches the equipped weapon's in-game footprint (or a sensible
     empty-slot default). No CSS clamp — inline wins deterministically. */
  transform: translate(-50%, -50%);
  border-radius: clamp(8px, 2.4cqmin, 14px);
  background: rgba(6, 4, 2, 0.60);
  backdrop-filter: blur(4px);
  border: 2px dashed rgba(255, 220, 130, 0.75);
  color: rgba(255, 220, 130, 0.9);
  cursor: pointer;
  pointer-events: auto;
  transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.hero-equip__mount.is-filled {
  border-style: solid;
  background: rgba(30, 18, 8, 0.85);
  box-shadow: 0 6px 14px rgba(0, 0, 0, 0.5), inset 0 0 12px rgba(255, 200, 100, 0.2);
}
.hero-equip__mount.is-selected {
  animation: heroEquipMountPulse 1.6s ease-in-out infinite;
  border-color: rgba(255, 245, 214, 1);
  transform: translate(-50%, -50%) scale(1.06);
}
@keyframes heroEquipMountPulse {
  0%, 100% { box-shadow: 0 6px 14px rgba(0,0,0,0.5), 0 0 0 rgba(255, 200, 100, 0.7); }
  50%      { box-shadow: 0 6px 14px rgba(0,0,0,0.5), 0 0 24px rgba(255, 200, 100, 0.9); }
}
.hero-equip__mount:hover { transform: translate(-50%, -50%) scale(1.08); }
.hero-equip__mount-icon {
  width: 78%;
  height: 78%;
  object-fit: contain;
  pointer-events: none;
}
.hero-equip__mount-plus {
  font-family: "Cinzel", serif;
  font-size: var(--font-lg);
  font-weight: 900;
  opacity: 0.7;
}
.hero-equip__mount-label {
  position: absolute;
  bottom: -20px;
  left: 50%;
  transform: translateX(-50%);
  font-family: "Cinzel", serif;
  font-size: var(--font-2xs);
  font-weight: 900;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(255, 220, 130, 0.85);
  white-space: nowrap;
  padding: 2px 8px;
  background: rgba(6, 4, 2, 0.65);
  border-radius: 999px;
  pointer-events: none;
}

.hero-equip__hint {
  text-align: center;
  font-size: var(--font-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(244, 234, 214, 0.65);
}

.hero-equip__drawer {
  flex: 0 0 auto;
  display: flex;
  gap: clamp(6px, 1.8cqmin, 12px);
  padding: clamp(6px, 1.8cqmin, 12px);
  background: rgba(6, 4, 2, 0.55);
  backdrop-filter: blur(6px);
  border: 1.5px solid rgba(255, 220, 130, 0.45);
  border-radius: clamp(8px, 2cqmin, 14px);
  overflow-x: auto;
  scrollbar-width: thin;
}
.hero-equip__empty {
  padding: clamp(8px, 2cqmin, 14px);
  text-align: center;
  font-size: var(--font-sm);
  color: rgba(244, 234, 214, 0.65);
  font-style: italic;
}

.hero-equip__tile {
  position: relative;
  flex: 0 0 auto;
  width: clamp(70px, 18cqmin, 110px);
  padding: clamp(6px, 1.6cqmin, 10px);
  background: linear-gradient(180deg, rgba(30, 18, 8, 0.9), rgba(14, 8, 3, 0.95));
  border: 1.5px solid rgba(255, 220, 130, 0.55);
  border-radius: clamp(6px, 1.6cqmin, 10px);
  color: #f5efe0;
  cursor: pointer;
  transition: transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.hero-equip__tile:hover { transform: translateY(-2px); border-color: rgba(255, 220, 130, 0.9); }
.hero-equip__tile.is-suggested {
  border-color: rgba(255, 245, 214, 1);
  box-shadow: 0 0 22px rgba(255, 200, 100, 0.6);
  animation: heroEquipTileBob 1.4s ease-in-out infinite;
}
@keyframes heroEquipTileBob {
  0%, 100% { transform: translateY(0) }
  50%      { transform: translateY(-4px) }
}
.hero-equip__tile.is-compat {
  border-color: rgba(120, 210, 130, 0.85);
  box-shadow: 0 0 18px rgba(120, 210, 130, 0.35);
}
.hero-equip__tile.is-incompat { opacity: 0.35; cursor: not-allowed; filter: grayscale(0.6); }
.hero-equip__tile.is-equipped { opacity: 0.75; }
/* Prevent the browser's built-in touch gestures (scroll, pinch) from
   claiming pointer events on a tile — we need pointerdown/move to
   drive the drag. `none` disables all default gestures on the tile
   itself; the drawer can still be scrolled by touching between tiles. */
.hero-equip__tile {
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  /* Belt-and-suspenders against the browser's native drag-image
     behaviour — even with draggable="false" on the img, some
     browsers still initiate an image drag on pointerdown, hijacking
     the pointer stream and preventing pointermove/up from ever
     reaching our handler. -webkit-user-drag is the older escape
     hatch that still applies in Chromium touch stacks. */
  -webkit-user-drag: none;
}
/* Anything INSIDE the tile — icon image, name label, equipped
   badge — must not accept pointer events. Otherwise pointerdown on
   the img spawns a native browser image drag that overrides our
   Pointer Events pipeline, and in some cases fires a "drop" that
   the OS turns into a navigation (which reads as a page reload). */
.hero-equip__tile > * { pointer-events: none; }
.hero-equip__tile.is-dragging-source {
  opacity: 0.35;
  filter: grayscale(0.5);
  cursor: grabbing;
}
/* The ghost is a floating clone of the tile that follows the cursor
   during a drag. position:fixed uses viewport coords so the JS
   pointer values map 1:1. pointer-events:none lets elementFromPoint
   see THROUGH the ghost to find the mount underneath. */
.hero-equip__drag-ghost {
  position: fixed !important;
  left: 0;  /* set inline */
  top: 0;   /* set inline */
  /* !important and animation:none together are what keep the ghost
     ON the pointer. A state class carrying a keyframe animation
     overrides a plain transform no matter the specificity, which is
     how `is-suggested`'s bob once left the ghost hanging by its
     corner. The clone is stripped of those classes in JS as well —
     this is the belt to that pair of braces. */
  animation: none !important;
  transform: translate(-50%, -50%) scale(0.9) rotate(-2deg) !important;
  pointer-events: none;
  z-index: 9999;
  opacity: 0.94;
  box-shadow: 0 12px 26px rgba(0, 0, 0, 0.7), 0 0 22px rgba(255, 200, 100, 0.55);
  filter: drop-shadow(0 4px 8px rgba(0,0,0,0.5));
  /* Suppress badges + labels that don't belong on the ghost. */
}
.hero-equip__drag-ghost .hero-equip__tile-badge { display: none; }

/* Mount visual states during a drop hover. Set by the drag handlers
   as the cursor passes over each mount. */
.hero-equip__mount.is-drop-target {
  border-color: rgba(120, 210, 130, 1);
  background: rgba(60, 120, 70, 0.4);
  box-shadow: 0 0 30px rgba(120, 210, 130, 0.75), inset 0 0 20px rgba(120, 210, 130, 0.35);
  transform: translate(-50%, -50%) scale(1.15);
}
.hero-equip__mount.is-drop-incompat {
  border-color: rgba(230, 100, 90, 0.95);
  background: rgba(120, 40, 40, 0.4);
  box-shadow: 0 0 22px rgba(230, 100, 90, 0.7), inset 0 0 16px rgba(230, 100, 90, 0.3);
  animation: heroEquipMountNo 0.5s ease-in-out;
}
@keyframes heroEquipMountNo {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  25%      { transform: translate(-54%, -50%) scale(1); }
  75%      { transform: translate(-46%, -50%) scale(1); }
}

/* Cursor affordance on tiles — grab on desktop, native mobile
   handling on touch. */
.hero-equip__tile { cursor: grab; }
.hero-equip__tile:active { cursor: grabbing; }
.hero-equip__tile-icon {
  width: clamp(36px, 10cqmin, 60px);
  height: clamp(36px, 10cqmin, 60px);
  object-fit: contain;
}
.hero-equip__tile-name {
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  font-size: var(--font-2xs);
  letter-spacing: 0.06em;
  text-align: center;
  color: rgba(255, 235, 190, 0.95);
}
.hero-equip__tile-badge {
  position: absolute;
  top: -6px;
  right: -6px;
  padding: 2px 6px;
  background: linear-gradient(180deg, #ffe090, #d8a040);
  color: #1a0f04;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-2xs);
  letter-spacing: 0.1em;
  border-radius: 999px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}

.hero-equip__foot {
  display: flex;
  justify-content: center;
  padding-top: clamp(2px, 0.8cqmin, 6px);
}
.hero-equip__close-btn {
  padding: clamp(6px, 1.8cqmin, 12px) clamp(14px, 4cqmin, 26px);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-md);
  letter-spacing: 0.18em;
  color: rgba(255, 220, 130, 0.95);
  background: rgba(0, 0, 0, 0.4);
  border: 1.5px solid rgba(255, 220, 130, 0.7);
  border-radius: 8px;
  cursor: pointer;
}
.hero-equip__close-btn:hover { background: rgba(255, 200, 100, 0.18); }

/* THE TEACHING LOCK — the exit is dead until the weapon is fitted.
   Deliberately still legible rather than nearly invisible: the player
   has to be able to read the words "Save & Close" to understand that
   the thing they want is being withheld until they do something. A
   greyed-out button you can't read is just a smudge, and a smudge
   reads as broken rather than as locked. */
.hero-equip.is-equip-locked .hero-equip__x,
.hero-equip.is-equip-locked .hero-equip__close-btn {
  opacity: 0.4;
  filter: grayscale(0.85);
  color: rgba(230, 220, 200, 0.7);
  border-color: rgba(200, 190, 170, 0.35);
  cursor: not-allowed;
  pointer-events: none;
}
/* The hint carries the reason, so it gets the emphasis the button
   just lost. */
/* A REFUSED EQUIP, SAID IN THE PANEL.

   The hint line is the one piece of chrome that is always on screen
   while this modal is open, which is exactly why the failure goes here:
   the HUD's toast host only exists during a run, so a rejected drop
   opened from the launcher used to report itself to nothing at all.
   See equipNotice in hero-equip.js. */
.hero-equip__hint.is-error {
  color: #ff9a86;
  font-weight: 700;
  animation: equipNoticeIn 220ms ease-out both;
}
@keyframes equipNoticeIn {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* And the slot flinches, so the eye goes to the thing that said no
   rather than hunting for a line of text. Short, and it does not move
   the layout — a mount square that jumped would drag the hero image
   with it. */
.hero-equip__mount.is-refused {
  animation: equipRefuse 420ms cubic-bezier(.36,.07,.19,.97) both;
  border-color: rgba(255, 120, 96, 0.9);
}
@keyframes equipRefuse {
  10%, 90% { transform: translate(calc(-50% - 2px), -50%); }
  30%, 70% { transform: translate(calc(-50% + 3px), -50%); }
  50%      { transform: translate(calc(-50% - 3px), -50%); }
  0%, 100% { transform: translate(-50%, -50%); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-equip__hint.is-error,
  .hero-equip__mount.is-refused { animation: none; }
}

.hero-equip.is-equip-locked .hero-equip__hint {
  color: rgba(255, 226, 150, 0.98);
  font-weight: 700;
}

/* GHOST HAND — a looping demonstration of the drag. A 0×0 anchor moved
   by one transform; both children hang off its centre, so the whole rig
   travels as one thing and the payload can never drift off the hand.
   position:fixed + body-level, matching the drag ghost, so the
   viewport coordinates measured in JS map 1:1. */
.hero-equip__demo {
  position: fixed;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
  z-index: 10000;
  pointer-events: none;
}
.hero-equip__demo-item {
  position: absolute;
  left: 0;
  top: 0;
  width: clamp(30px, 8cqmin, 46px);
  height: clamp(30px, 8cqmin, 46px);
  object-fit: contain;
  transform: translate(-50%, -50%);
  opacity: 0.9;
  filter: drop-shadow(0 0 10px rgba(255, 200, 100, 0.85))
          drop-shadow(0 4px 8px rgba(0, 0, 0, 0.55));
}
/* The hand sits below-right of the anchor the way a real cursor does,
   so the item reads as held in the fingers rather than skewered by
   them. */
.hero-equip__demo-hand {
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(-14%, 6%);
  filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.6));
}
.hero-equip__demo-hand path {
  fill: rgba(255, 242, 214, 0.97);
  stroke: rgba(46, 26, 10, 0.9);
  stroke-width: 1.1;
  stroke-linejoin: round;
}

@media (prefers-reduced-motion: reduce) {
  .hero-equip, .hero-equip__panel, .hero-equip__mount.is-selected,
  .hero-equip__tile.is-suggested {
    animation-duration: 200ms !important;
  }
  /* The hand is not built at all under reduced motion (see
     syncHandDemo); this is the belt to that brace. */
  .hero-equip__demo { display: none; }
}

/* Hero-card clickability — the roster cards need a cursor + hover
   affordance now that clicking them opens the equip modal. */
/* Hotkey badge, top-left of the card. Small and high contrast, and
   tucked into the corner so it never competes with the portrait. */
.game-hud__team-key {
  position: absolute;
  top: 0; left: 0;
  min-width: clamp(10px, 3.4cqi, 17px);
  padding: 0 clamp(1px, 0.5cqi, 3px);
  border-bottom-right-radius: clamp(3px, 1.2cqi, 7px);
  background: rgba(6, 5, 4, 0.86);
  border-right: 1px solid rgba(217, 178, 107, 0.55);
  border-bottom: 1px solid rgba(217, 178, 107, 0.55);
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(7px, 2.4cqi, 12px);
  line-height: 1.5;
  text-align: center;
  color: #ffdc82;
  pointer-events: none;
  z-index: 2;
}

/* ================================================================
   Next-stage loading screen
   ================================================================
   Covers everything, including the summary panel it was launched
   from — the previous stage is over and should not be visible
   underneath. Only appears when the warm-up didn't beat the player. */
.game-hud__stageload {
  position: absolute;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(120% 90% at 50% 40%, #2a1b0f 0%, #140c06 70%, #0b0704 100%);
  animation: stageloadIn 260ms ease-out both;
}
.game-hud__stageload[hidden] { display: none; }
@keyframes stageloadIn { from { opacity: 0 } to { opacity: 1 } }
.game-hud__stageload-inner {
  width: min(86cqi, 520px);
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(8px, 2cqmin, 16px);
}
.game-hud__stageload-eyebrow {
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(9px, 2.6cqmin, 15px);
  letter-spacing: 0.5em;
  text-indent: 0.5em;
  text-transform: uppercase;
  color: rgba(255, 236, 194, 0.7);
}
.game-hud__stageload-title {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(22px, 8cqmin, 52px);
  line-height: 1.05;
  background: linear-gradient(180deg, #fffdf5 0%, #f6c95b 55%, #a76a1a 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Indeterminate: we don't know how many bytes are left, and a fake
   percentage that jumps to 100 is worse than an honest sweep. */
.game-hud__stageload-bar {
  width: min(60cqi, 300px);
  height: 3px;
  background: rgba(255, 236, 194, 0.14);
  overflow: hidden;
  border-radius: 2px;
}
.game-hud__stageload-bar > i {
  display: block;
  width: 38%;
  height: 100%;
  background: linear-gradient(90deg, rgba(246,201,91,0) 0%, #f6c95b 50%, rgba(246,201,91,0) 100%);
  animation: stageloadSweep 1.15s ease-in-out infinite;
}
@keyframes stageloadSweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(363%); }
}
.game-hud__stageload-tipwrap {
  margin-top: clamp(10px, 3cqmin, 26px);
  min-height: 5.2em;              /* reserve the space so rotating tips don't jump the layout */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5em;
}
.game-hud__stageload-tiplabel {
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(8px, 2.2cqmin, 12px);
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  text-transform: uppercase;
  color: rgba(217, 178, 107, 0.8);
}
.game-hud__stageload-tip {
  margin: 0;
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 500;
  font-size: clamp(12px, 3.4cqmin, 18px);
  line-height: 1.5;
  max-width: 40ch;
  color: rgba(238, 228, 210, 0.94);
  opacity: 0;
}
.game-hud__stageload-tip.is-in { animation: stageloadTip 520ms ease-out both; }
@keyframes stageloadTip {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .game-hud__stageload-bar > i { animation-duration: 2.4s; }
  .game-hud__stageload-tip.is-in { animation-duration: 120ms; }
}

/* Health, on the card — the same bar in the runner and the bastion,
   because the card is the one piece of chrome both games share. Sits
   flush along the bottom edge so it reads as part of the frame rather
   than as something laid on top of the portrait. */
.game-hud__team-hp {
  position: absolute;
  /* ABOVE the card, not across the bottom of it. Laid over the
     portrait it covered the art and read as part of the frame; sat
     above, it reads as a gauge belonging to the card — and the row of
     them across the team scans as one status line. */
  left: 0; right: 0;
  bottom: calc(100% + clamp(2px, 0.8cqi, 4px));
  height: clamp(3px, 1.1cqi, 6px);
  background: rgba(6, 5, 4, 0.82);
  border: 1px solid rgba(0, 0, 0, 0.55);
  border-radius: 2px;
  overflow: hidden;
  pointer-events: none;
  z-index: 2;
}
/* FOUR BANDS. Green is deliberately the most saturated of the set —
   "healthy" should be the colour that reads instantly as fine so the
   eye can skip it, and the warning colours earn attention by being
   the ones that CHANGED. */
.game-hud__team-hp > i {
  display: block;
  height: 100%;
  width: 100%;
  background: #5fe07a;
  transition: width 160ms ease-out, background-color 220ms linear;
}
.game-hud__team-hp[data-band="warn"] > i { background: #f7d74a; }
.game-hud__team-hp[data-band="low"]  > i { background: #f5891f; }
.game-hud__team-hp[data-band="crit"] > i { background: #ec2f2f; }

/* WHAT THEY ARE DOING, ACROSS THE FOOT OF THE CARD.
   The same words as the pill over the crewman's head — see the note
   above setSlotStatus in hud.js. Inside the card rather than under it:
   the row already sits on the bottom edge of the stage, and a label
   hung below it would either be off screen or push the whole row up.

   Two lines, and no ellipsis. The longest thing it ever says is
   WAITING ON MATERIALS, and half of that word truncated is worse than
   a small second line — the point of the strip is that the answer is
   readable at a glance, from the row, without going to find anybody. */
.game-hud__team-status {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: clamp(1px, 0.4cqi, 3px) clamp(1px, 0.5cqi, 3px);
  border-radius: 0 0 clamp(3px, 1.4cqi, 8px) clamp(3px, 1.4cqi, 8px);
  background: linear-gradient(180deg, rgba(6, 5, 4, 0) 0%, rgba(6, 5, 4, 0.88) 42%, rgba(6, 5, 4, 0.95) 100%);
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(6px, 1.85cqi, 10px);
  line-height: 1.04;
  letter-spacing: 0.02em;
  text-align: center;
  text-transform: uppercase;
  color: rgba(226, 216, 198, 0.92);
  pointer-events: none;
  z-index: 3;
  /* Two lines, then stop. A third would eat the portrait. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
/* The same three words the world pill uses, so the colour is learnt
   once: gold for working, grey for idle, red for stuck-and-it-is-your-
   problem. */
.game-hud__team-status[data-variant="work"]    { color: #ffd98a; }
.game-hud__team-status[data-variant="idle"]    { color: rgba(206, 198, 184, 0.8); }
.game-hud__team-status[data-variant="blocked"] {
  color: #ff9a86;
  background: linear-gradient(180deg, rgba(60, 10, 6, 0) 0%, rgba(70, 12, 8, 0.9) 42%, rgba(70, 12, 8, 0.96) 100%);
}

/* THE CARD FOLLOWS THE BAR. A four-pixel gauge is the right amount of
   detail and the wrong amount of signal in a fight — the rim is what
   gets noticed peripherally, the bar is what gets read once they look.
   Green is left alone on purpose: a healthy card should be quiet. */
.game-hud__team-slot[data-hp-band="warn"] {
  border-color: rgba(247, 215, 74, 0.85);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45), 0 0 10px rgba(247, 215, 74, 0.3);
}
.game-hud__team-slot[data-hp-band="low"] {
  border-color: rgba(245, 137, 31, 0.9);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45), 0 0 13px rgba(245, 137, 31, 0.42);
}
.game-hud__team-slot[data-hp-band="crit"] {
  border-color: rgba(236, 47, 47, 1);
  animation: hudCardCrit 760ms ease-in-out infinite;
}
/* The pulse lives on the CARD, not the bar. Pulsing a sliver a few
   pixels tall is a pulse nobody sees; pulsing the frame around the
   portrait is unmissable without covering anything. */
@keyframes hudCardCrit {
  0%, 100% {
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45), 0 0 6px rgba(236, 47, 47, 0.45);
  }
  50% {
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45), 0 0 20px 3px rgba(236, 47, 47, 0.9);
  }
}
.game-hud__team-hp[data-band="crit"] > i { animation: hudHpCrit 760ms ease-in-out infinite; }
@keyframes hudHpCrit {
  0%, 100% { filter: brightness(1); }
  50%      { filter: brightness(1.6); }
}
/* Knocked out beats health: the card is already greyed with a countdown
   over it, and a red pulse underneath that is two alarms for one event.
   hp is 0 while down, so without this every knockout would strobe. */
.game-hud__team-slot--down[data-hp-band] {
  animation: none;
  border-color: rgba(217, 178, 107, 0.6);   /* back to the resting rim */
}
@media (prefers-reduced-motion: reduce) {
  .game-hud__team-slot[data-hp-band="crit"],
  .game-hud__team-hp[data-band="crit"] > i { animation: none; }
  .game-hud__team-hp > i { transition: none; }
}

/* Knocked out. The portrait greys out and the seconds left sit over
   it, so a player who presses 2 and gets nothing is looking straight
   at the reason. Not `--locked`: that means "you don't have them",
   this means "not right now". */
.game-hud__team-slot--down > img {
  filter: grayscale(1) brightness(0.45);
}
.game-hud__team-slot--down {
  box-shadow: inset 0 0 0 1px rgba(255, 106, 74, 0.75);
}
.game-hud__team-down {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 800;
  font-size: clamp(9px, 3.4cqi, 16px);
  color: #ff9576;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.95);
  pointer-events: none;
  z-index: 3;
}

.game-hud__team-slot:not(.game-hud__team-slot--locked) {
  cursor: pointer;
  transition: transform 120ms ease, box-shadow 120ms ease;
}
.game-hud__team-slot:not(.game-hud__team-slot--locked):hover {
  transform: translateY(-2px);
  box-shadow: 0 0 18px rgba(255, 200, 100, 0.55);
}

/* ================================================================
   In-shop tutorial tips bar
   ================================================================
   Dedicated flex row inside .shop__panel that reserves its own
   vertical space (the grid uses flex: 1 above it) so cards never
   overlap the tip text. The moto rider — the player's companion —
   coaches the player through their first purchase. RIG (the
   shopkeeper) never speaks *inside* the shop overlay; his voice
   lives in the pre-shop conversation cutscene.
   Copy swaps: firstVisit → point at RATE L1; after purchase →
   congratulate + close-shop hint; return visit → gentle browse.
   ================================================================ */
.shop__tips {
  display: flex;
  align-items: center;
  gap: clamp(6px, 1.8cqmin, 14px);
  padding: clamp(6px, 1.8cqmin, 12px) clamp(10px, 2.4cqmin, 16px);
  background: rgba(6, 4, 2, 0.68);
  backdrop-filter: blur(6px) saturate(1.1);
  -webkit-backdrop-filter: blur(6px) saturate(1.1);
  border: 1.5px solid rgba(255, 220, 130, 0.55);
  border-radius: clamp(8px, 2cqmin, 14px);
  animation: shopTipsIn 420ms cubic-bezier(0.2, 1.4, 0.3, 1) 200ms both;
}
@keyframes shopTipsIn {
  from { transform: translateY(16px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
.shop__tips-portrait {
  flex: 0 0 auto;
  width: clamp(40px, 10cqmin, 68px);
  height: clamp(40px, 10cqmin, 68px);
  border-radius: 999px;
  overflow: hidden;
  background: radial-gradient(circle at 50% 40%, rgba(70, 45, 15, 0.85), rgba(10, 6, 2, 0.95));
  box-shadow:
    0 0 0 2px rgba(255, 220, 130, 0.85),
    0 4px 10px rgba(0, 0, 0, 0.55),
    0 0 18px rgba(255, 200, 100, 0.35);
}
.shop__tips-portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 25%;
  display: block;
}
.shop__tips-bubble {
  flex: 1 1 auto;
  min-width: 0;
}
.shop__tips-name {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-2xs);
  color: rgba(255, 220, 130, 1);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  margin-bottom: 2px;
}
.shop__tips-line {
  font-family: "Rajdhani", sans-serif;
  font-weight: 500;
  font-size: var(--font-sm);
  line-height: 1.32;
  color: #f5efe0;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
}

/* Coach mark — dashed gold ring wrapping the recommended card + a
   pulsing chevron pointing at it. Only rendered on first visit. */
/* The "look here" ring is NOT drawn here. It used to be — a dashed
   outline plus its own pulse keyframes, a private fourth copy of an
   affordance the game already had three of — and it ringed the whole
   card while the copy told the player to tap its BUY button. The
   shared tutorial ring (platform/ui/coach.js) now lands on the button
   itself; this class survives only as the marker the shop uses to
   find it. Keep it: removing it silently unhooks the ring. */
.shop__card--recommended { position: relative; }
.shop__card-recommend-badge {
  position: absolute;
  top: clamp(-6px, -1.8cqmin, -4px);
  right: clamp(-6px, -1.8cqmin, -4px);
  padding: 2px 8px;
  background: linear-gradient(180deg, #ffe090, #d8a040);
  color: #1a0f04;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-2xs);
  letter-spacing: 0.14em;
  border-radius: 999px;
  border: 1px solid rgba(120, 70, 15, 0.9);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.55);
  animation: shopBadgeBob 1600ms ease-in-out infinite;
}
@keyframes shopBadgeBob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-3px); }
}

/* Purchase reward — spawned briefly on a card the moment it's bought.
   Gold spark burst + flash halo. Removed after ~900ms by shop.js. */
.shop__card--flash {
  animation: shopCardFlash 700ms ease-out;
}
@keyframes shopCardFlash {
  0%   { box-shadow: 0 0 0 rgba(255, 220, 130, 0), inset 0 0 0 rgba(255, 240, 200, 0); background-color: transparent; }
  20%  { box-shadow: 0 0 60px rgba(255, 220, 130, 0.95), inset 0 0 40px rgba(255, 240, 200, 0.7); }
  100% { box-shadow: 0 0 0 rgba(255, 220, 130, 0), inset 0 0 0 rgba(255, 240, 200, 0); }
}
.shop__reward-burst {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: visible;
  z-index: 4;
}
.shop__reward-burst span {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: radial-gradient(circle at 40% 40%, #fff5c0, #ffb040 60%, transparent 100%);
  transform: translate(-50%, -50%) scale(0);
  animation: shopSpark 780ms ease-out forwards;
}
@keyframes shopSpark {
  0%   { transform: translate(-50%, -50%) scale(0.5); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--dx, 0px)), calc(-50% + var(--dy, 0px))) scale(0.2); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .shop, .shop__panel, .shop__keeper { animation-duration: 200ms !important; }
  .shop__card--shake, .shop__card--recommended, .shop__card--flash,
  .shop__reward-burst span, .shop__card-recommend-badge {
    animation: none !important;
  }
}

/* ================================================================
   First-run tutorial overlay — interactive practice ("STEER TO...")
   ================================================================
   Layout (v2): header up top (title + progress dots + short hint),
   two thin instruction strips pinned to the LEFT + RIGHT edges of
   the stage (chevron + tap ripple + keycaps), and a "Don't show this
   tip again" checkbox pinned to the bottom. The MIDDLE is empty on
   purpose — the runner spawns practice-target rings on the game
   canvas underneath, and the player must actually steer to them.
   No scrim: the play surface must stay visible for target reading.
   Sits inside [data-game-cutscene], which the launcher uses in GATED
   mode (waitFor promise) — the scene doesn't exit until the runner
   resolves the promise (i.e. all N targets collected).
   ================================================================ */
/* ============================================================
   TUTORIAL BANNER — title pill, progress dots, hint line.
   Emitted only by platform/ui/coach.js, the one template every
   tutorial in every game renders through, so a change here moves
   all of them at once.

   These used to carry a second set of .tut__* selectors for the
   runner's steer scene, which built its own copy of the same
   chrome. That scene is gone; the class names stayed so the
   styling didn't have to be rewritten.
   ============================================================ */
.practice-overlay__title {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--tut-title-font);
  letter-spacing: 0.1em;
  color: #ffdc82;
  text-shadow:
    0 2px 0 rgba(60, 30, 5, 0.9),
    0 0 22px rgba(246, 201, 91, 0.7);
  text-transform: uppercase;
  line-height: 1.05;
}

.practice-overlay__dots {
  display: inline-flex;
  gap: var(--tut-dot-gap);
  padding: var(--pad-2xs) 0;
}
.practice-overlay__dot {
  width:  var(--tut-dot-size);
  height: var(--tut-dot-size);
  border-radius: 50%;
  border: 1.5px solid rgba(255, 220, 130, 0.55);
  background: rgba(10, 7, 5, 0.45);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4);
  transition: background 220ms ease-out,
              border-color 220ms ease-out,
              transform 220ms cubic-bezier(0.2, 1.4, 0.3, 1),
              box-shadow 220ms ease-out;
}
.practice-overlay__dot--filled {
  background: radial-gradient(circle, #fff8e2 0%, #ffd06a 55%, #f6a03a 100%);
  border-color: rgba(255, 220, 130, 0.95);
  box-shadow:
    0 0 10px rgba(246, 160, 58, 0.85),
    inset 0 0 6px rgba(255, 248, 226, 0.5);
  transform: scale(1.15);
}

.practice-overlay__hint {
  font-size: var(--tut-hint-font);
  letter-spacing: 0.06em;
  color: rgba(244, 234, 214, 0.7);
  padding: 0 var(--pad-sm);
  text-align: center;
}

/* THE STEERING PANEL.
   Was two strips pinned to the left and right edges, each with a tap
   ripple — correct when the runner steered by absolute position, and
   actively misleading now that it steers with a floating drag stick.
   One centred panel: a knob sliding in its circle, DRAG TO STEER, then
   the keycaps. Centred because the control now lives wherever the
   thumb lands, which is the whole point of the change — pointing at
   the edges would point at the one place a player never needs to go.

   Sits low rather than mid-stage: the demo mirrors a real control the
   player is about to use, so it belongs near where their thumb is, and
   the middle of the stage stays clear for the practice targets. */
.tut__steer {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--above-heroes, 90px) + clamp(46px, 15cqi, 96px));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(2px, 1.2cqi, 8px);
  padding: clamp(6px, 2.4cqi, 16px) clamp(10px, 4cqi, 26px);
  border-radius: clamp(6px, 2.4cqi, 14px);
  background:
    linear-gradient(180deg, rgba(255,225,150,0.06) 0%, rgba(0,0,0,0.55) 100%),
    rgba(10, 7, 5, 0.72);
  border: 1.5px solid rgba(217, 178, 107, 0.55);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.5),
    0 10px 24px rgba(0, 0, 0, 0.45),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
  color: rgba(244, 234, 214, 0.92);
  animation: tutSteerIn 480ms cubic-bezier(0.2, 1.2, 0.3, 1) both;
}
@keyframes tutSteerIn {
  from { opacity: 0; transform: translateX(-50%) scale(0.94); }
  to   { opacity: 1; transform: translateX(-50%) scale(1); }
}

/* The demo. A dashed circle with a knob that slides between the two
   chevrons — the gesture performed, rather than described. The knob
   travel is deliberately the same shape as the real control's: out to
   the rim and back, never further. */
.tut__steer-demo {
  position: relative;
  width:  clamp(92px, 34cqi, 150px);
  height: clamp(38px, 13cqi, 58px);
  display: grid;
  place-items: center;
  color: #ffdc82;
}
.tut__steer-ring {
  width:  clamp(34px, 12cqi, 52px);
  height: clamp(34px, 12cqi, 52px);
  border-radius: 50%;
  border: 2px dashed rgba(246, 201, 91, 0.45);
}
.tut__steer-knob {
  position: absolute;
  left: 50%; top: 50%;
  width:  clamp(16px, 5.6cqi, 24px);
  height: clamp(16px, 5.6cqi, 24px);
  margin: calc(clamp(16px, 5.6cqi, 24px) / -2) 0 0 calc(clamp(16px, 5.6cqi, 24px) / -2);
  border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, #fff3cf, #f6c95b 55%, #a76a1a);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
  animation: tutSteerKnob 2.6s ease-in-out infinite;
}
@keyframes tutSteerKnob {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(clamp(-26px, -9cqi, -16px)); }
  50%      { transform: translateX(0); }
  75%      { transform: translateX(clamp(16px, 9cqi, 26px)); }
}
.tut__steer-chev {
  position: absolute;
  top: 50%;
  width:  clamp(11px, 4cqi, 17px);
  height: clamp(15px, 5cqi, 22px);
  margin-top: clamp(-11px, -2.5cqi, -7px);
  filter: drop-shadow(0 0 6px rgba(255, 208, 106, 0.5));
  animation: tutSteerChev 2.6s ease-in-out infinite;
}
.tut__steer-chev--l { left: 0; }
/* Offset by a quarter cycle so each chevron lights as the knob
   arrives at it, not on some rhythm of its own. */
.tut__steer-chev--r { right: 0; animation-delay: 1.3s; }
@keyframes tutSteerChev {
  0%, 100% { opacity: 0.3; }
  25%      { opacity: 1; }
  50%      { opacity: 0.3; }
}
@media (prefers-reduced-motion: reduce) {
  .tut__steer-knob,
  .tut__steer-chev { animation: none; }
}

.tut__tap-label {
  font-family: "Cinzel", serif;
  font-weight: 700;
  font-size: clamp(7px, 2cqi, 14px);
  letter-spacing: 0.16em;
  color: #ffdc82;
  text-transform: uppercase;
}
.tut__or {
  font-size: clamp(6px, 1.7cqi, 12px);
  letter-spacing: 0.14em;
  color: rgba(244, 234, 214, 0.55);
  text-transform: uppercase;
  margin-top: 2px;
}

/* Physical-looking key pills — flat top-lit face + darker rim + inset
   shadow. Reads as an actual key cap. Grouped in a row so A + ← sit
   next to each other under the "or press" separator. */
.tut__keys {
  display: inline-flex;
  gap: 5px;
  margin-top: 2px;
}
.tut__key {
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(7px, 2.8cqi, 16px);
  line-height: 1;
  min-width: clamp(12px, 4.2cqi, 28px);
  padding: clamp(2px, 1cqi, 6px) clamp(3px, 1.4cqi, 8px);
  border-radius: 6px;
  background: linear-gradient(180deg, #f4ead6 0%, #d9b26b 100%);
  color: #2a1a08;
  border: 1px solid rgba(90, 60, 20, 0.9);
  box-shadow:
    0 2px 0 rgba(70, 40, 10, 0.9),
    inset 0 1px 0 rgba(255, 255, 255, 0.5),
    0 0 6px rgba(0, 0, 0, 0.3);
  text-align: center;
}


/* Bottom action bar — the opt-out checkbox + SKIP button sit in
   one horizontal row pinned DIRECTLY ABOVE the hero-card row.
   Anchored via the shared --above-heroes CSS variable so on ANY
   stage size the bar stays right on top of the cards instead of
   floating up into the play area. Zero magic numbers. */
/* Sits inside .coach__stack, which owns the positioning. `pointer-
   events: auto` re-enables input on just this row — the coach root and
   the stack are both transparent to it, so the game underneath stays
   playable while a non-blocking tutorial is up. */
/* THE WAY OUT, PINNED TO THE TOP.
   It used to be the last row of the instruction stack. Once the stack
   started anchoring to its target that put it over the control the step
   was telling the player to press — and because it arrives on a delay,
   the stack had already been measured and placed without it, so it grew
   downward onto the target after the fact.
   Its own fixed home now: top of the stage, centred, as far from the
   directed attention as this layout goes, and in the same place on
   every step so a player who wants it always knows where to look. */
.tut__actions {
  position: absolute;
  top: clamp(6px, 1.6cqb, 14px);
  left: 50%;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
  max-width: 92cqi;
  pointer-events: auto;
  z-index: 5;
}
/* A banner owns the top when there is one, so step below it. */
.coach.has-banner .tut__actions { top: clamp(74px, 18cqb, 150px); }

/* Opt-out row — now an inline pill inside .tut__actions. `pointer-
   events` stays auto so the checkbox is clickable through the
   overlay. Custom-styled box (native checkboxes look inconsistent
   across mobile browsers and don't match the forge palette). */
.tut__optout {
  display: inline-flex;
  align-items: center;
  gap: clamp(4px, 1.6cqi, 10px);
  padding: clamp(3px, 1.6cqi, 10px) clamp(7px, 2.8cqi, 16px);
  border-radius: 999px;
  background: rgba(10, 7, 5, 0.72);
  border: 1px solid rgba(217, 178, 107, 0.45);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  /* Keep the label on ONE line even at small sizes — the checkbox
     pill would otherwise wrap the label into 3-4 vertical lines on
     narrow stages, blowing up the action bar's height. */
  white-space: nowrap;
}

/* SKIP button — same pill silhouette as the opt-out, brighter
   forge fill so it reads as the primary action. Meaning: "yes I
   want out of this," while the checkbox next to it decides
   permanence. Uppercase Cinzel + tight letter-spacing echoes the
   game-over "DRIVE AGAIN" button. */
/* THE WAY OUT OF THE TUTORIAL IS NOT THE POINT OF IT.
   This was a gold gradient pill with a drop shadow — the loudest
   control on a screen whose whole job is to get the player to read one
   line and do one thing. A tutorial whose most eye-catching element is
   the skip button is a tutorial that has been designed to be skipped.
   It is a quiet text link now: no fill, no shadow, muted, underlined
   so it still reads as something you can press. */
.tut__skip-btn {
  position: relative;
  overflow: hidden;
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 600;
  font-size: clamp(9px, 2.4cqi, 13px);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: clamp(3px, 1.2cqi, 7px) clamp(6px, 2.2cqi, 12px);
  border-radius: 5px;
  color: rgba(238, 228, 210, 0.5);
  background: transparent;
  border: 0;
  box-shadow: none;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: rgba(238, 228, 210, 0.28);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color 140ms ease-out;
  touch-action: none;          /* the hold is ours, not the browser's */
}
.tut__skip-btn:hover,
.tut__skip-btn:focus-visible {
  color: rgba(255, 236, 194, 0.92);
  text-decoration-color: rgba(255, 236, 194, 0.6);
  outline: none;
}
.tut__skip-btn:focus-visible { box-shadow: 0 0 0 2px rgba(255, 236, 194, 0.45); }
.tut__skip-label { position: relative; z-index: 1; }

/* HOLD TO SKIP. The fill is the progress bar and the affordance at
   once — pressing does something visible immediately, so the control
   never feels dead, and letting go before it completes visibly undoes
   it. A button-pusher gets a flicker; someone who means it gets out. */
.tut__skip-fill {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 0;
  background: linear-gradient(90deg, rgba(217, 178, 107, 0.25), rgba(217, 178, 107, 0.5));
  border-radius: 5px;
  pointer-events: none;
  transition: width 180ms ease-out;
}
.tut__skip-btn.is-holding .tut__skip-fill {
  width: 100%;
  /* Matches SKIP_HOLD_MS in platform/ui/coach.js. */
  transition: width 750ms linear;
}
.tut__skip-btn.is-holding { color: rgba(255, 236, 194, 0.95); }
/* Tapped instead of held: the label swaps to "Hold to skip" and the
   control nudges, so a failed press explains itself rather than
   reading as a button that did nothing. */
.tut__skip-btn.is-hinting {
  color: rgba(255, 236, 194, 0.95);
  animation: tutSkipHint 420ms ease-out;
}
@keyframes tutSkipHint {
  0%, 100% { transform: translateX(0); }
  30%      { transform: translateX(-2px); }
  65%      { transform: translateX(2px); }
}

/* The row arrives a few seconds in, and says so by fading up rather
   than appearing — a control that pops into existence pulls the eye,
   which is the opposite of what the delay is for. */
.tut__actions.is-arriving { animation: tutActionsIn 600ms ease-out both; }
@keyframes tutActionsIn {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .tut__actions.is-arriving { animation-duration: 150ms; }
  .tut__skip-btn.is-hinting { animation: none; }
}
.tut__optout input {
  /* visually-hidden but focusable — real checkbox drives state */
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  border: 0;
}
.tut__optout-box {
  width:  clamp(9px, 3.4cqi, 20px);
  height: clamp(9px, 3.4cqi, 20px);
  border-radius: 4px;
  background: linear-gradient(180deg, #221610 0%, #100a06 100%);
  border: 1.5px solid rgba(217, 178, 107, 0.7);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  transition: border-color 160ms ease-out, background 160ms ease-out;
}
.tut__optout-box::after {
  content: "";
  width:  clamp(5px, 2.2cqi, 12px);
  height: clamp(5px, 2.2cqi, 12px);
  border-radius: 2px;
  background: radial-gradient(circle at 40% 35%, #fff8e2 0%, #ffd06a 60%, #c88a2c 100%);
  box-shadow: 0 0 8px rgba(246, 160, 58, 0.7);
  transform: scale(0);
  transition: transform 180ms cubic-bezier(0.2, 1.5, 0.3, 1);
}
.tut__optout input:checked + .tut__optout-box {
  border-color: rgba(255, 220, 130, 0.95);
  background: linear-gradient(180deg, #2a1c10 0%, #180e08 100%);
}
.tut__optout input:checked + .tut__optout-box::after { transform: scale(1); }
.tut__optout input:focus-visible + .tut__optout-box {
  outline: 2px solid rgba(246, 201, 91, 0.9);
  outline-offset: 2px;
}
.tut__optout-label {
  font-family: "Rajdhani", system-ui, sans-serif;
  font-size: clamp(7px, 2.4cqi, 15px);
  letter-spacing: 0.05em;
  color: rgba(244, 234, 214, 0.88);
  text-transform: none;
}

/* Narrow-container override kept intentionally minimal — base
   sizes above are already fluid via cqi clamps, so most of the
   old fixed-px overrides here are now redundant. Kept only the
   things that need a *layout* shift (not just a size change)
   on genuinely narrow stages. */

/* ================================================================
   Practice overlay ("AVOID OBSTACLES" / "COLLECT COINS")
   ================================================================
   Non-modal HUD banner used by tutorial phases 2 + 3. Unlike the
   modal steer scene, the game world keeps ticking underneath — the
   overlay is intentionally slim (icon + title at the top, progress
   dots below, hint at the bottom) so the play area stays wide open
   for the player to actually dodge / grab. Slides in from the top,
   pulses when the phase completes, then fades out.

   Positioning: banner sits BELOW the top HUD scoreboard so it
   never overlaps the distance/km-h/coins/best card.
   ================================================================ */
/* Enter/exit/complete animations for the shared tutorial banner.
   The .practice-overlay container that used to own them is gone —
   .coach__top and .coach.is-exiting / .is-complete drive them now. */
@keyframes practiceEnter {
  from { opacity: 0; transform: translateY(-14px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes practiceExit {
  to { opacity: 0; transform: translateY(-8px); }
}

.practice-overlay__banner {
  display: inline-flex;
  align-items: center;
  gap: clamp(6px, 2cqi, 10px);
  padding: clamp(5px, 1.8cqi, 8px) clamp(10px, 3.4cqi, 16px);
  border-radius: 999px;
  background:
    linear-gradient(180deg, rgba(255, 225, 150, 0.06) 0%, rgba(0, 0, 0, 0.55) 100%),
    rgba(10, 7, 5, 0.82);
  border: 1.5px solid rgba(217, 178, 107, 0.55);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.55),
    0 10px 24px rgba(0, 0, 0, 0.5);
}
.practice-overlay__icon  { display: grid; place-items: center; line-height: 0; }
.practice-overlay__icon svg {
  width:  var(--tut-icon-size);
  height: var(--tut-icon-size);
}
/* Per-phase title accent — the base title styles come from the
   shared .practice-overlay__title rule near the top.
   Only the color changes per phase. */
.practice-overlay__banner--avoid   .practice-overlay__title { color: #ff9a5a; }
.practice-overlay__banner--collect .practice-overlay__title { color: #ffd070; }

/* Hint sits under the dots, narrow enough not to run the width of a
   wide stage. */
.practice-overlay__hint {
  margin-top: 6px;
  max-width: min(320px, 90cqi);
}

/* Phase-complete pulse — one gentle bounce on the banner when all
   required successes are in, before it fades out. Reads as a small
   celebration ("done!") without stealing focus. Triggered by
   .coach.is-complete, defined with the rest of the coach below. */
@keyframes practiceCompletePulse {
  0%   { transform: scale(1);    box-shadow: 0 0 0 rgba(255, 208, 106, 0); }
  50%  { transform: scale(1.06); box-shadow: 0 0 26px rgba(255, 208, 106, 0.65); }
  100% { transform: scale(1);    box-shadow: 0 0 0 rgba(255, 208, 106, 0); }
}

/* Tutorial hit flash — brief full-stage red tint the moment the
   player crashes into a tutorial-mode hazard. Non-fatal (the
   collision handler suppresses gameOver during tutorial phases);
   this is the "you hit it, try again" visual beat. */
.game-stage.is-tut-hit-flash::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(60% 45% at 50% 78%, rgba(255, 60, 60, 0.5), rgba(255, 30, 30, 0.1) 70%, transparent 100%);
  animation: tutHitFlash 320ms ease-out;
  pointer-events: none;
  z-index: 6;
}
@keyframes tutHitFlash {
  0%   { opacity: 1; }
  100% { opacity: 0; }
}

/* Practice-overlay base padding is already fluid via clamp() —
   no narrow-container override needed. */

/* ================================================================
   Team roster — bottom-center 5-slot card row
   ================================================================
   Pinned to the bottom of the play stage, below the driving area.
   Slot 0 is always the player's equipped vehicle; slot 1 fills with
   the companion motorbike when she arrives at the 100 m mark; the
   remaining slots are locked placeholders that tease future unlocks.
   Slots share the forge-palette idiom used by the top scoreboard +
   sidebar so the whole HUD feels authored from one source. */
.game-hud__team {
  position: absolute;
  bottom: var(--hero-card-bottom);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: clamp(3px, 1.4cqi, 6px);
  pointer-events: none;
  z-index: 4;
}
.game-hud__team-slot {
  /* Card width scales with stage; height is width × 1.08 (a slight
     portrait cast, matching the vehicle portraits the cards frame).
     Aspect stays constant on any stage; --hero-card-* variables
     let the tutorial action bar anchor right above the row. */
  width:  var(--hero-card-width);
  height: var(--hero-card-height);
  border-radius: clamp(4px, 1.6cqi, 10px);
  /* VISIBLE, so the health bar can sit above the card. The portrait is
     still clipped — it inherits the radius directly (rule below) —
     which is what the overflow was actually for. */
  overflow: visible;
  position: relative;
  background:
    linear-gradient(180deg, rgba(255, 225, 150, 0.05) 0%, rgba(4, 6, 8, 0.7) 100%),
    rgba(10, 7, 5, 0.78);
  border: 1.5px solid rgba(217, 178, 107, 0.6);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.45),
    0 6px 14px rgba(0, 0, 0, 0.45),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
  display: grid;
  place-items: center;
  transition: box-shadow 240ms ease-out, border-color 240ms ease-out;
}
.game-hud__team-slot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  /* Clips itself now that the slot's overflow is visible — that is
     what the slot's overflow:hidden was doing, and the health bar
     needs to escape the card to sit above it. */
  border-radius: inherit;
  user-select: none;
  -webkit-user-drag: none;
}
/* Same reason: the knocked-out veil is a full-bleed overlay and would
   spill past the rounded corners without its own clip. */
.game-hud__team-down { border-radius: inherit; overflow: hidden; }

/* Locked / empty slot — dimmed rim, muted background, faint '?' cue
   that reads as "future hero." Kept quiet so the two active slots
   dominate the row visually. */
.game-hud__team-slot--locked {
  background: rgba(4, 6, 8, 0.55);
  border-color: rgba(184, 188, 196, 0.22);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.35),
    inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
.game-hud__team-slot--locked::after {
  content: '?';
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(11px, 5cqi, 28px);
  letter-spacing: 0.04em;
  color: rgba(200, 205, 215, 0.35);
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
}

/* Unlock flash — plays once when a locked slot receives a hero.
   Scale + gold-glow pop that reads as "welcome to the team." Same
   burst timing (cubic-bezier(0.2, 1.6, 0.3, 1)) as the sidebar
   count-bump so team beats + resource beats share a heartbeat. */
.game-hud__team-slot.is-unlocking {
  animation: teamUnlock 520ms cubic-bezier(0.2, 1.6, 0.3, 1) both;
}
@keyframes teamUnlock {
  0%   { transform: scale(0.6);  opacity: 0;
         box-shadow: 0 0 0 rgba(255, 220, 130, 0); }
  35%  { transform: scale(1.18); opacity: 1;
         box-shadow: 0 0 24px rgba(255, 220, 130, 0.9),
                     0 0 0 1px rgba(0, 0, 0, 0.5); }
  100% { transform: scale(1);    opacity: 1;
         box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45),
                     0 6px 14px rgba(0, 0, 0, 0.45),
                     inset 0 1px 0 rgba(255, 255, 255, 0.06); }
}

/* Base team-card sizes above are already fluid via clamp() — no
   additional narrow-container override needed. */

/* ================================================================
   Resource pop text ("+1 SCRAP" / "+1 WOOD" ...)
   ================================================================
   Rises out of the pickup site and fades to zero over 900 ms. A
   momentary "salvage grabbed" beat that reinforces the earn-loop
   without cluttering the persistent HUD. Colour is driven per-
   resource via the --color CSS variable set by the runner. */
.resource-pop {
  position: absolute;
  left: 0;
  top:  0;
  transform: translate(calc(var(--x) - 50%), calc(var(--y) - 100%));
  pointer-events: none;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(12px, 3cqi, 15px);
  letter-spacing: 0.08em;
  color: var(--color, #ffd070);
  text-shadow:
    0 1px 0 rgba(20, 10, 4, 0.9),
    0 0 10px rgba(0, 0, 0, 0.6);
  z-index: 4;
  animation: resourcePopRise 900ms cubic-bezier(0.15, 0.75, 0.25, 1) forwards;
  white-space: nowrap;
}
/* Damage taken. Same rise, bigger and blunter — a cost should not be
   styled like a reward. */
.damage-pop {
  font-size: clamp(15px, 4.2cqi, 21px);
  letter-spacing: 0.04em;
}
@keyframes resourcePopRise {
  0%   { opacity: 0;   transform: translate(calc(var(--x) - 50%), calc(var(--y) - 100%)) scale(0.7); }
  20%  { opacity: 1;   transform: translate(calc(var(--x) - 50%), calc(var(--y) - 130%)) scale(1.05); }
  70%  { opacity: 0.9; transform: translate(calc(var(--x) - 50%), calc(var(--y) - 220%)) scale(1);    }
  100% { opacity: 0;   transform: translate(calc(var(--x) - 50%), calc(var(--y) - 300%)) scale(0.9); }
}

/* ================================================================
   Game-over resource summary
   ================================================================
   Compact row showing what was salvaged this run. Renders inline
   SVG icons (mirroring the in-game sprite palettes) + counts, with
   zero-count resources filtered out. Sits between the "new best"
   line and the leaderboard so it reads as run-metadata, not as a
   competing leaderboard. */
.game-hud__resources {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin: 8px 0 12px;
  padding: 8px 14px;
  border-radius: 8px;
  background: rgba(10, 7, 5, 0.55);
  border: 1px solid rgba(217, 178, 107, 0.35);
}
.game-hud__resources[hidden] { display: none; }
.game-hud__resources-label {
  font-family: "Rajdhani", system-ui, sans-serif;
  font-size: 10px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(184, 188, 196, 0.75);
}
.game-hud__resources-list {
  display: flex;
  gap: 14px;
  align-items: center;
  list-style: none;
  margin: 0;
  padding: 0;
  flex-wrap: wrap;
  justify-content: center;
}
.game-hud__resources-row {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.game-hud__resources-icon {
  display: grid;
  place-items: center;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
  line-height: 0;
}
.game-hud__resources-count {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: 15px;
  letter-spacing: 0.03em;
  color: var(--gold-0, #ffdc82);
  font-variant-numeric: tabular-nums;
}

/* ================================================================
   Vertical resource sidebar (live)
   ================================================================
   Sits on the LEFT edge of the play stage, vertically centered.
   Five rows (scrap / wood / stone / fuel / herbs), always visible
   from run start so the player watches totals tick up from 0 as
   drops are grabbed. A brief scale bump on any row that just
   ticked draws the eye without stealing focus. Compact so it
   doesn't crowd the driving lane. */
.game-hud__side-resources {
  position: absolute;
  left: clamp(6px, 1.8cqi, 12px);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: clamp(3px, 1.4cqi, 6px);
  padding: clamp(5px, 1.8cqi, 8px) clamp(4px, 1.4cqi, 6px);
  border-radius: 10px;
  background: linear-gradient(180deg, rgba(4, 6, 8, 0.72), rgba(4, 6, 8, 0.42));
  border: 1px solid rgba(200, 205, 215, 0.16);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.35),
    0 6px 18px rgba(0, 0, 0, 0.4);
  pointer-events: none;
  z-index: 4;
}
.game-hud__side-row {
  /* Hidden rows use display:none so the sidebar container shrinks
     to hug ONLY the visible rows. First unlock (coins) renders as
     a single compact square in the sidebar's slot; each additional
     unlock extends the bar downward. */
  display: none;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: clamp(22px, 8cqi, 34px);
  pointer-events: none;
}
.game-hud__side-row.is-visible {
  display: flex;
  animation: sideRowReveal 420ms cubic-bezier(0.2, 1.6, 0.3, 1) both;
}
@keyframes sideRowReveal {
  0%   { opacity: 0; transform: translateX(-8px) scale(0.7); }
  55%  { opacity: 1; transform: translateX(0)    scale(1.12); }
  100% { opacity: 1; transform: translateX(0)    scale(1); }
}
/* Sidebar container also hidden until at least one row is unlocked
   — see hud.js unlockResource which removes the [hidden] attribute
   the moment the first row appears. Keeps the play screen clean for
   fresh players before any resource is introduced. */
.game-hud__side-resources[hidden] { display: none; }
.game-hud__side-icon {
  display: grid;
  place-items: center;
  line-height: 0;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
}
.game-hud__side-icon svg {
  width:  clamp(14px, 5cqi, 22px);
  height: clamp(14px, 5cqi, 22px);
}
.game-hud__side-count {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(10px, 3.4cqi, 14px);
  letter-spacing: 0.02em;
  color: var(--gold-0, #ffdc82);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  transition: color 200ms ease-out;
}
/* Bump animation — the runner-driven setResources adds .is-bumped
   whenever a count changes, we play a one-shot scale+glow, then the
   class is removed on the next update. Reuses the same "pulse" idiom
   as the progress dots in the tutorial to keep the visual language
   consistent. */
.game-hud__side-count.is-bumped {
  animation: sideCountBump 380ms cubic-bezier(0.2, 1.6, 0.3, 1) both;
  color: #fff5d0;
}
@keyframes sideCountBump {
  0%   { transform: scale(1);    text-shadow: 0 0 0 rgba(246, 201, 91, 0); }
  40%  { transform: scale(1.35); text-shadow: 0 0 12px rgba(246, 201, 91, 0.85); }
  100% { transform: scale(1);    text-shadow: 0 0 0 rgba(246, 201, 91, 0); }
}

/* Base sidebar sizes above are already fluid via clamp() — no
   narrow-container override needed. */

/* ================================================================
   Tutorial-mode indicator
   ================================================================
   When the runner enters any tutorial (day-1 chain OR the mid-run
   blaster pickup intro), it flips .is-tutorial-mode on the game
   stage. We render two distinct signals so the player can never
   confuse a tutorial with real gameplay:

     1. A small "TUTORIAL" pill badge in the top-left corner
     2. A subtle cyan inner border pulsing around the whole stage

   Cool teal-cyan is deliberately picked to contrast the warm-gold
   forge palette everywhere else — that's the universal "training /
   instructional" color language, unmistakably distinct from the
   game's fantasy chrome.

   Both signals are additive (only visible during tutorials) and
   non-intrusive (no scrim, no darkening of the play area). */
.game-stage.is-tutorial-mode {
  box-shadow:
    inset 0 0 0 2px rgba(124, 224, 255, 0.32),
    inset 0 0 40px rgba(124, 224, 255, 0.18),
    0 0 0 1px rgba(0, 0, 0, 0.35),
    0 0 24px rgba(0, 0, 0, 0.55);
  animation: tutModeFrame 3.6s ease-in-out infinite;
}
@keyframes tutModeFrame {
  0%, 100% {
    box-shadow:
      inset 0 0 0 2px rgba(124, 224, 255, 0.28),
      inset 0 0 40px rgba(124, 224, 255, 0.12),
      0 0 0 1px rgba(0, 0, 0, 0.35),
      0 0 24px rgba(0, 0, 0, 0.55);
  }
  50% {
    box-shadow:
      inset 0 0 0 2px rgba(124, 224, 255, 0.5),
      inset 0 0 44px rgba(124, 224, 255, 0.24),
      0 0 0 1px rgba(0, 0, 0, 0.35),
      0 0 24px rgba(0, 0, 0, 0.55);
  }
}
.game-stage.is-tutorial-mode::before {
  content: 'TUTORIAL';
  position: absolute;
  top: 14px;
  left: 14px;
  padding: 4px 10px 3px;
  border-radius: 5px;
  background:
    linear-gradient(180deg, rgba(30, 60, 78, 0.92) 0%, rgba(10, 22, 32, 0.92) 100%);
  border: 1.5px solid rgba(124, 224, 255, 0.75);
  color: #b8f0ff;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: 10px;
  letter-spacing: 0.24em;
  text-shadow: 0 0 10px rgba(124, 224, 255, 0.6);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.45),
    0 0 14px rgba(124, 224, 255, 0.35),
    inset 0 1px 0 rgba(255, 255, 255, 0.08);
  pointer-events: none;
  z-index: 7;
  animation: tutBadgeIn 320ms cubic-bezier(0.2, 1.4, 0.3, 1) both;
}
@keyframes tutBadgeIn {
  from { opacity: 0; transform: translateY(-6px) scale(0.92); }
  to   { opacity: 1; transform: translateY(0)    scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .game-stage.is-tutorial-mode { animation: none; }
  .game-stage.is-tutorial-mode::before { animation: none; }
}

/* During tutorial: hide the top play scoreboard (distance / km-h /
   best). It occupies the same y-band as the tutorial header title
   and would overlap the "reach every target to begin" hint. The
   score is frozen during tutorial anyway, so hiding it costs
   nothing informational. It fades back in at tutorial exit. */
.game-stage.is-tutorial-mode .game-hud__scoreboard {
  visibility: hidden;
}

/* ================================================================
   BASE DEFENSE — in-stage UI
   ================================================================
   Owned by games/defense/ui.js, not by the platform HUD: this is
   furniture specific to defending something. Laid out as a top status
   strip and a bottom build bar, leaving the middle of the stage — the
   board — completely clear, because the board is the thing the player
   is actually reading.
   ================================================================ */
.td { position: absolute; inset: 0; pointer-events: none; z-index: 4; }

/* --- Top status strip -------------------------------------------- */
.td-status {
  position: absolute;
  top: clamp(4px, 1.4cqi, 10px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: clamp(8px, 3cqi, 20px);
  padding: clamp(3px, 1.2cqi, 7px) clamp(8px, 3cqi, 18px);
  border-radius: 6px;
  background: linear-gradient(180deg, rgba(4, 6, 8, 0.85), rgba(4, 6, 8, 0.55));
  border: 1px solid rgba(200, 205, 215, 0.18);
  font-family: "Rajdhani", system-ui, sans-serif;
  white-space: nowrap;
}
.td-status__block { display: flex; flex-direction: column; align-items: center; }
.td-status__block--core { min-width: clamp(64px, 22cqi, 130px); }
.td-status__label {
  font-size: clamp(6px, 1.9cqi, 10px);
  letter-spacing: 0.24em;
  color: rgba(200, 205, 215, 0.6);
}
.td-status__value {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(11px, 3.4cqi, 19px);
  color: var(--cream, #f4ead6);
  font-variant-numeric: tabular-nums;
}
.td-status__value--scrap { color: #f6c95b; }
.td-status__value.is-bumped { animation: tdBump 260ms cubic-bezier(0.2, 1.6, 0.3, 1); }
@keyframes tdBump { 40% { transform: scale(1.22); } }

/* Core integrity. A bar rather than a number: mid-wave the player
   needs to read "how much trouble am I in" in peripheral vision, and
   a bar answers that without being read. */
.td-core-bar {
  width: 100%;
  height: clamp(5px, 1.6cqi, 9px);
  margin-top: 2px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(200, 205, 215, 0.2);
  overflow: hidden;
}
.td-core-bar > span {
  display: block; height: 100%; width: 100%;
  background: linear-gradient(180deg, #9fe8a8, #4fae5d);
  transition: width 220ms ease-out, background 320ms ease-out;
}
.td-core-bar > span[data-level="warn"] { background: linear-gradient(180deg, #ffd97a, #d8912a); }
.td-core-bar > span[data-level="crit"] {
  background: linear-gradient(180deg, #ff8a6a, #c33a24);
  animation: tdCrit 900ms ease-in-out infinite;
}
@keyframes tdCrit { 50% { filter: brightness(1.5); } }

/* --- Callout ------------------------------------------------------ */
.td-callout {
  position: absolute;
  top: 22%;
  left: 50%;
  transform: translateX(-50%);
  padding: clamp(4px, 1.6cqi, 10px) clamp(10px, 4cqi, 26px);
  border-radius: 999px;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(12px, 4.4cqi, 26px);
  letter-spacing: 0.12em;
  white-space: nowrap;
  background: rgba(6, 5, 4, 0.82);
  border: 1.5px solid rgba(246, 201, 91, 0.6);
  color: #ffe6a8;
}
.td-callout[hidden] { display: none; }
.td-callout[data-variant="good"] { border-color: rgba(126, 224, 138, 0.8); color: #b7f0bf; }
.td-callout[data-variant="bad"]  { border-color: rgba(226, 96, 60, 0.85);  color: #ffb9a5; }
.td-callout[data-variant="warn"] { border-color: rgba(255, 154, 90, 0.85); color: #ffd0a8; }
.td-callout.is-in { animation: tdCallout 2200ms cubic-bezier(0.2, 1.3, 0.3, 1) forwards; }
@keyframes tdCallout {
  0%   { opacity: 0; transform: translateX(-50%) scale(0.7); }
  10%  { opacity: 1; transform: translateX(-50%) scale(1); }
  78%  { opacity: 1; transform: translateX(-50%) scale(1); }
  100% { opacity: 0; transform: translateX(-50%) scale(1.04) translateY(-10px); }
}

/* --- Build bar ---------------------------------------------------- */
.td-build {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(3px, 1.2cqi, 7px);
  padding: clamp(4px, 1.6cqi, 10px) clamp(6px, 2cqi, 14px) clamp(6px, 2.2cqi, 14px);
  background: linear-gradient(180deg, rgba(4, 6, 8, 0), rgba(4, 6, 8, 0.82) 38%);
  pointer-events: auto;
}
.td-build__cards { display: flex; gap: clamp(4px, 1.6cqi, 10px); }
.td-build__card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  min-width: clamp(52px, 19cqi, 96px);
  padding: clamp(3px, 1.2cqi, 7px) clamp(5px, 2cqi, 12px);
  border-radius: 8px;
  border: 1.5px solid rgba(200, 205, 215, 0.22);
  background: rgba(10, 12, 16, 0.85);
  color: var(--cream, #f4ead6);
  font-family: "Rajdhani", system-ui, sans-serif;
  cursor: pointer;
  transition: border-color 140ms ease, background 140ms ease, transform 120ms ease;
}
.td-build__card:hover { transform: translateY(-2px); }
.td-build__card.is-armed {
  border-color: rgba(246, 201, 91, 0.95);
  background: rgba(246, 201, 91, 0.16);
  box-shadow: 0 0 18px rgba(246, 201, 91, 0.4);
}
/* Unaffordable is dimmed, never hidden — a player should be able to
   see what they are saving toward. */
.td-build__card.is-broke { opacity: 0.42; }
.td-build__glyph { width: clamp(18px, 6cqi, 30px); line-height: 0; }
.td-build__glyph svg { width: 100%; height: auto; }
.td-build__name {
  font-size: clamp(6px, 1.9cqi, 11px);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(226, 220, 208, 0.85);
}
.td-build__cost {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(9px, 2.8cqi, 15px);
  color: #f6c95b;
}
.td-build__go { min-width: clamp(120px, 44cqi, 240px); }

/* --- Buttons ------------------------------------------------------ */
.td-btn {
  padding: clamp(4px, 1.5cqi, 9px) clamp(10px, 3.6cqi, 22px);
  border-radius: 8px;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(9px, 2.8cqi, 15px);
  letter-spacing: 0.14em;
  cursor: pointer;
  pointer-events: auto;
  transition: transform 120ms ease-out, filter 140ms ease;
}
.td-btn--gold {
  color: #2a1a08;
  background: linear-gradient(180deg, #fff3cf 0%, #f6c95b 60%, #a76a1a 100%);
  border: 1.5px solid rgba(90, 60, 20, 0.9);
}
.td-btn--ghost {
  color: rgba(255, 220, 130, 0.95);
  background: rgba(0, 0, 0, 0.45);
  border: 1.5px solid rgba(255, 220, 130, 0.55);
}
.td-btn:hover:not(:disabled) { transform: translateY(-1px); }
.td-btn:disabled { cursor: default; filter: grayscale(0.7) brightness(0.62); }

/* --- Selected-tower panel ----------------------------------------- */
.td-panel {
  position: absolute;
  left: 50%;
  bottom: clamp(74px, 22cqb, 150px);
  transform: translateX(-50%);
  width: min(300px, 88cqi);
  padding: clamp(6px, 2.2cqi, 13px) clamp(8px, 3cqi, 16px);
  border-radius: 10px;
  background: linear-gradient(180deg, rgba(14, 12, 10, 0.96), rgba(6, 5, 4, 0.94));
  border: 1.5px solid rgba(246, 201, 91, 0.5);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.6);
  pointer-events: auto;
  font-family: "Rajdhani", system-ui, sans-serif;
}
.td-panel[hidden] { display: none; }
.td-panel__head { display: flex; align-items: baseline; justify-content: space-between; }
.td-panel__name {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(11px, 3.4cqi, 18px);
  color: #ffe6a8;
}
.td-panel__lvl {
  font-family: "Cinzel", serif;
  font-size: clamp(9px, 2.6cqi, 14px);
  color: rgba(246, 201, 91, 0.8);
}
.td-panel__stats {
  margin: 3px 0 6px;
  font-size: clamp(7px, 2.2cqi, 12px);
  letter-spacing: 0.06em;
  color: rgba(226, 220, 208, 0.8);
}
.td-panel__actions { display: flex; gap: clamp(3px, 1.2cqi, 7px); }
.td-panel__actions .td-btn { flex: 1 1 0; padding-inline: 4px; }

/* Core hit — a red pulse over the whole stage. The player's eyes are
   on the lane, not on the bar, so a leak has to announce itself
   globally or it goes unnoticed until the run is over. */
.game-stage.is-td-hit::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(80% 60% at 50% 100%, rgba(226, 60, 40, 0.5), transparent 72%);
  animation: tdHit 420ms ease-out forwards;
  pointer-events: none;
  z-index: 6;
}
@keyframes tdHit { 0% { opacity: 0; } 14% { opacity: 1; } 100% { opacity: 0; } }

/* Chrome a game has switched off via hud.setPlayChrome(). */
.is-hidden-chrome { display: none !important; }

@media (prefers-reduced-motion: reduce) {
  .td-callout.is-in { animation-duration: 1600ms; }
  .td-core-bar > span[data-level="crit"],
  .td-status__value.is-bumped { animation: none; }
  .game-stage.is-td-hit::after { animation-duration: 220ms; }
}

/* ================================================================
   THE RUNNER — floating drag stick (touch only)
   ================================================================
   Steering is RELATIVE: the circle appears under the thumb and reads
   the delta from there, so full lock either way is reachable from the
   middle of the glass. The old control drove the car toward the
   finger's absolute position, which meant a hard left needed a thumb
   on the far left — in the strip Safari, Android and Discord's own
   mobile drawer all claim for themselves. See platform/ui/stick.js.

   Z-INDEX 2, AND THE NUMBER MATTERS.
   This was 3, chosen from a comment elsewhere that said the HUD sat at
   5. It does not — `.game-hud` is z-index 3 as well, and the pad is
   appended to the stage AFTER it, so equal z-index meant DOM order
   decided and the pad won every hit test. While it was
   pointer-events:none on desktop that was invisible; the moment a mouse
   could drive it, an invisible sheet lay over every HUD button — the
   game-over panel's Continue among them.

   2 puts it level with `.game-canvas--fg` and later in the DOM, so the
   knob still draws over the road, while the HUD at 3 wins any overlap
   outright rather than by accident of sibling order. Nothing that wants
   a click can lose one to this.
   ================================================================ */
.gstick { position: absolute; inset: 0; pointer-events: none; z-index: 2; }

/* The pad is the full width of the stage — a floating stick has no
   reason to restrict where a thumb may land, and catching an edge
   touch here is strictly better than letting it reach the host client.
   It stops above the hero-card row (the shared --above-heroes rail) so
   reaching for the stick can never switch hero by accident. */
.gstick__pad {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: var(--above-heroes, 90px);
  /* Live for EVERY input device. This was gated to `(pointer: coarse)`
     on the reasoning that desktop has the keyboard — which is true, and
     is not a reason to make a control that works fine with a mouse
     refuse to answer one. Dragging steers on a desktop too; the
     keyboard is unchanged.

     Safe to be hit-testable everywhere because nothing under it wants a
     click. The runner reads no canvas pointer input any more, and every
     layer that does take clicks — HUD (5), coach (7), cutscenes, the
     shop — is above this one's z-index 3. */
  pointer-events: auto;
  touch-action: none;
  /* A drag control should not select the text it drags across, and on a
     desktop with a mouse that is suddenly reachable. */
  user-select: none;
  -webkit-user-select: none;
}
/* Dead during cutscenes, the shop, and after the run ends. setLive(false)
   stops the pad RESPONDING; this stops it existing as far as hit-testing
   is concerned. Belt and braces with the z-index above: a control that
   is switched off should not be able to swallow a click even if
   something later gets the stacking wrong. */
.gstick__pad[data-dead] { pointer-events: none; }

.gstick__base {
  position: absolute;
  width: 96px; height: 96px;
  margin: -48px 0 0 -48px;
  border-radius: 50%;
  border: 2px solid rgba(246, 201, 91, 0.35);
  background: radial-gradient(circle, rgba(246, 201, 91, 0.12), rgba(0, 0, 0, 0.25) 70%);
  pointer-events: none;
}
.gstick__base[hidden] { display: none; }
.gstick__knob {
  position: absolute;
  left: 50%; top: 50%;
  width: 40px; height: 40px;
  margin: -20px 0 0 -20px;
  border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, #fff3cf, #f6c95b 55%, #a76a1a);
  box-shadow: 0 3px 12px rgba(0, 0, 0, 0.6);
}

/* Resting hint — bottom centre, gone for good the first time a thumb
   lands. The control it replaced advertised itself (tapping the screen
   WAS the control); this one has to be found, and it is the first
   thing a new player meets. */
.gstick__hint {
  position: absolute;
  left: 50%;
  bottom: clamp(12px, 5cqi, 40px);
  transform: translateX(-50%);
  width:  clamp(84px, 30cqi, 132px);
  height: clamp(40px, 14cqi, 60px);
  display: grid;
  place-items: center;
  color: rgba(255, 220, 130, 0.5);
  pointer-events: none;
}
.gstick__hint[hidden] { display: none; }
.gstick__hint-ring {
  width:  clamp(30px, 11cqi, 46px);
  height: clamp(30px, 11cqi, 46px);
  border-radius: 50%;
  border: 2px dashed rgba(246, 201, 91, 0.4);
  animation: gstickHintPulse 2.2s ease-in-out infinite;
}
.gstick__hint-chev {
  position: absolute;
  top: 50%;
  width:  clamp(11px, 4cqi, 17px);
  height: clamp(15px, 5cqi, 22px);
  margin-top: clamp(-11px, -2.5cqi, -7px);
  animation: gstickHintSlide 2.2s ease-in-out infinite;
}
.gstick__hint-chev--l { left: 0; }
.gstick__hint-chev--r { right: 0; animation-delay: 1.1s; }
@keyframes gstickHintPulse {
  0%, 100% { opacity: 0.45; transform: scale(1); }
  50%      { opacity: 0.9;  transform: scale(1.06); }
}
@keyframes gstickHintSlide {
  0%, 100% { opacity: 0.3; }
  50%      { opacity: 0.95; }
}
@media (prefers-reduced-motion: reduce) {
  .gstick__hint-ring,
  .gstick__hint-chev { animation: none; }
}

/* ================================================================
   THE BASTION — in-stage controls
   ================================================================
   First engine here with free 2D movement, so it needs a stick. The
   stick is FLOATING: it appears wherever the thumb lands in the left
   half and tracks from there. Fixed-position sticks make the player
   look down to find them; floating ones don't, which is why every
   modern mobile action game uses them.
   ================================================================ */
.bx { position: absolute; inset: 0; pointer-events: none; z-index: 4; }
/* THE YARD'S HUD STANDS DOWN FOR THE DUEL. One class, set by
   ui.setDuelMode, rather than six hide() calls — so a control added to
   this file later is covered without anybody remembering to come back.
   Everything here describes a match in the compound, and during the
   standoff there is nobody in the compound. */
.bx.is-duel > * { display: none !important; }

.bx-status {
  position: absolute;
  top: clamp(4px, 1.4cqi, 10px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: clamp(6px, 2.4cqi, 16px);
  padding: clamp(3px, 1.2cqi, 7px) clamp(7px, 2.6cqi, 16px);
  border-radius: 6px;
  background: linear-gradient(180deg, rgba(4, 6, 8, 0.86), rgba(4, 6, 8, 0.56));
  border: 1px solid rgba(200, 205, 215, 0.18);
  font-family: "Rajdhani", system-ui, sans-serif;
  white-space: nowrap;
}
.bx-status__block { display: flex; flex-direction: column; align-items: center; }
.bx-status__block--core { min-width: clamp(52px, 18cqi, 110px); }
.bx-status__label {
  font-size: clamp(5px, 1.7cqi, 9px);
  letter-spacing: 0.22em;
  color: rgba(200, 205, 215, 0.6);
}
.bx-status__value {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(10px, 3.1cqi, 18px);
  color: var(--cream, #f4ead6);
  font-variant-numeric: tabular-nums;
}
.bx-status__value--scrap { color: #f6c95b; }
.bx-status__value.is-bad { color: #ff8a6a; }
.bx-status__value.is-bumped { animation: bxBump 260ms cubic-bezier(0.2, 1.6, 0.3, 1); }
@keyframes bxBump { 40% { transform: scale(1.2); } }

.bx-bar {
  width: 100%;
  height: clamp(5px, 1.6cqi, 9px);
  margin-top: 2px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(200, 205, 215, 0.2);
  overflow: hidden;
}
.bx-bar > span {
  display: block; height: 100%; width: 100%;
  background: linear-gradient(180deg, #9fe8a8, #4fae5d);
  transition: width 220ms ease-out, background 320ms ease-out;
}
.bx-bar > span[data-level="warn"] { background: linear-gradient(180deg, #ffd97a, #d8912a); }
.bx-bar > span[data-level="crit"] {
  background: linear-gradient(180deg, #ff8a6a, #c33a24);
  animation: bxCrit 900ms ease-in-out infinite;
}
@keyframes bxCrit { 50% { filter: brightness(1.5); } }

.bx-callout {
  position: absolute;
  top: 20%;
  left: 50%;
  transform: translateX(-50%);
  padding: clamp(4px, 1.6cqi, 10px) clamp(10px, 4cqi, 26px);
  border-radius: 999px;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(11px, 4cqi, 24px);
  letter-spacing: 0.12em;
  white-space: nowrap;
  background: rgba(6, 5, 4, 0.82);
  border: 1.5px solid rgba(246, 201, 91, 0.6);
  color: #ffe6a8;
}
.bx-callout[hidden] { display: none; }
.bx-callout[data-variant="good"] { border-color: rgba(126, 224, 138, 0.8); color: #b7f0bf; }
.bx-callout[data-variant="bad"]  { border-color: rgba(226, 96, 60, 0.85);  color: #ffb9a5; }
.bx-callout[data-variant="warn"] { border-color: rgba(255, 154, 90, 0.85); color: #ffd0a8; }
.bx-callout.is-in { animation: bxCallout 2400ms cubic-bezier(0.2, 1.3, 0.3, 1) forwards; }
@keyframes bxCallout {
  0%   { opacity: 0; transform: translateX(-50%) scale(0.72); }
  9%   { opacity: 1; transform: translateX(-50%) scale(1); }
  80%  { opacity: 1; transform: translateX(-50%) scale(1); }
  100% { opacity: 0; transform: translateX(-50%) translateY(-10px); }
}

/* --- Floating stick ------------------------------------------------
   The pad is the left half of the stage ABOVE the hero cards, so a
   thumb reaching for the stick can never accidentally switch heroes. */
.bx-stick {
  position: absolute;
  left: 0;
  top: 0;
  width: 52%;
  bottom: var(--above-heroes, 90px);
  pointer-events: auto;
  touch-action: none;
}
.bx-stick__base {
  position: absolute;
  width: 92px; height: 92px;
  margin: -46px 0 0 -46px;
  border-radius: 50%;
  border: 2px solid rgba(246, 201, 91, 0.35);
  background: radial-gradient(circle, rgba(246, 201, 91, 0.12), rgba(0, 0, 0, 0.25) 70%);
  pointer-events: none;
}
.bx-stick__base[hidden] { display: none; }
.bx-stick__knob {
  position: absolute;
  left: 50%; top: 50%;
  width: 40px; height: 40px;
  margin: -20px 0 0 -20px;
  border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, #fff3cf, #f6c95b 55%, #a76a1a);
  box-shadow: 0 3px 12px rgba(0, 0, 0, 0.6);
}

/* --- Context action ------------------------------------------------
   One button, bottom-right, above the hero cards. Hidden entirely when
   there is nothing to do, so its presence alone means "you can act". */
.bx-actions {
  position: absolute;
  right: clamp(8px, 3cqi, 20px);
  bottom: calc(var(--above-heroes, 90px) + clamp(4px, 1.5cqi, 10px));
  pointer-events: none;
}
/* ===================================================================
   THE JOB RING — what a hero is doing, drawn on the thing they are
   doing it to.

   Centred exactly on the objective (the anchor point IS the centre,
   unlike the action button which lifts clear of the sprite), so the
   dial reads as belonging to that pile, that panel, that plot.
   =================================================================== */
.bx-focus {
  position: absolute;
  z-index: 5;
  display: grid;
  place-items: center;
  width:  clamp(54px, 17cqi, 92px);
  height: clamp(54px, 17cqi, 92px);
  pointer-events: none;
  transform: translate(-50%, -50%);
  animation: bxFocusIn 220ms cubic-bezier(0.2, 1.4, 0.4, 1) both;
}
.bx-focus[hidden] { display: none; }
.bx-focus.is-anchored { left: 0; top: 0; }
@keyframes bxFocusIn {
  from { opacity: 0; transform: translate(-50%, -50%) scale(0.7); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

/* Every child stacks in the same grid cell — the dial behind, the
   glyph in the middle, the words below it. */
.bx-focus > * { grid-area: 1 / 1; }

.bx-focus__dial {
  width: 100%;
  height: 100%;
  /* -90deg so the fill starts at twelve o'clock, which is the only
     place a person expects a dial to start. */
  transform: rotate(-90deg);
  overflow: visible;
}
.bx-focus__track {
  fill: rgba(10, 7, 4, 0.55);
  stroke: rgba(255, 220, 150, 0.18);
  stroke-width: 5;
}
.bx-focus__fill {
  fill: none;
  stroke: #f6c95b;
  stroke-width: 5;
  stroke-linecap: round;
  filter: drop-shadow(0 0 5px rgba(246, 201, 91, 0.75));
  /* The dash is written by JS every frame; the transition smooths the
     gap between frames rather than the value itself, so a 60 Hz fill
     looks continuous and a 20 Hz one still does. */
  transition: stroke-dashoffset 90ms linear;
}
.bx-focus__glyph {
  display: grid;
  place-items: center;
  width:  clamp(18px, 6cqi, 30px);
  height: clamp(18px, 6cqi, 30px);
  color: #ffe6b4;
  pointer-events: none;
}
.bx-focus__glyph svg { width: 100%; height: 100%; }
.bx-focus__label,
.bx-focus__note {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.95);
}
.bx-focus__label {
  margin-top: 2px;
  font-size: clamp(8px, 2.4cqi, 12px);
  color: rgba(255, 235, 195, 0.95);
}
.bx-focus__note {
  margin-top: clamp(12px, 3.6cqi, 18px);
  font-size: clamp(7px, 2cqi, 10px);
  color: #ff9a8a;
}

/* WAITING ON MATERIALS is not progress. The dial stops claiming to
   fill and the whole thing cools to a warning colour, because a ring
   that keeps sweeping while nothing happens is a lie told sixty times
   a second. */
.bx-focus[data-state="stalled"] .bx-focus__fill { stroke: #ff9a8a; filter: none; }
.bx-focus[data-state="stalled"] .bx-focus__glyph { color: #ffc9bd; }

/* THE TURRET ASKS. There is no duration to show — it is waiting for a
   press — so the dial pulses as a ring instead of filling, and the
   middle carries a keycap rather than an icon. */
.bx-focus.is-prompt .bx-focus__fill {
  stroke-dasharray: 6 10;
  animation: bxFocusSpin 3.2s linear infinite;
  transition: none;
}
.bx-focus.is-prompt .bx-focus__track { stroke: rgba(255, 220, 150, 0.4); }
@keyframes bxFocusSpin { to { stroke-dashoffset: -64; } }
.bx-focus__key {
  padding: 2px clamp(4px, 1.4cqi, 8px);
  border-radius: 4px;
  border: 1px solid rgba(255, 220, 150, 0.7);
  border-bottom-width: 2.5px;
  background: rgba(30, 20, 10, 0.9);
  color: #ffe6b4;
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  font-size: clamp(7px, 2cqi, 10px);
  letter-spacing: 0.08em;
  animation: bxFocusKey 1.4s ease-in-out infinite;
}
@keyframes bxFocusKey {
  0%, 100% { transform: translateY(0);    opacity: 0.85; }
  50%      { transform: translateY(-2px); opacity: 1; }
}

/* Finished. One flourish — the ring flashes white and blooms outward —
   and then it is gone. Half a second, because the payoff for having
   stopped is knowing that stopping worked. */
.bx-focus[data-state="done"] .bx-focus__fill {
  stroke: #fff6d2;
  animation: bxFocusDone 520ms ease-out both;
}
.bx-focus[data-state="done"] .bx-focus__track { stroke: rgba(255, 246, 210, 0.5); }
@keyframes bxFocusDone {
  0%   { transform: scale(1);    opacity: 1; }
  60%  { transform: scale(1.18); opacity: 1; }
  100% { transform: scale(1.35); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .bx-focus,
  .bx-focus__key,
  .bx-focus.is-prompt .bx-focus__fill,
  .bx-focus[data-state="done"] .bx-focus__fill { animation: none; }
}

.bx-action {
  padding: clamp(8px, 2.6cqi, 16px) clamp(12px, 4cqi, 26px);
  border-radius: 999px;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(9px, 2.8cqi, 15px);
  letter-spacing: 0.12em;
  color: #2a1a08;
  background: linear-gradient(180deg, #fff3cf 0%, #f6c95b 60%, #a76a1a 100%);
  border: 1.5px solid rgba(90, 60, 20, 0.9);
  box-shadow: 0 3px 0 rgba(70, 40, 10, 0.9), 0 8px 20px rgba(0, 0, 0, 0.5);
  cursor: pointer;
  pointer-events: auto;
  touch-action: none;
  animation: bxAction 1.8s ease-in-out infinite;
}
.bx-action[hidden] { display: none; }
/* ANCHORED TO A WORLD OBJECT. coach.js computes the exact screen point
   and writes left/top; escaping the container's right/bottom pinning is
   what `position: fixed`-like behaviour needs here, so the button is
   taken out of the corner's flow entirely.

   The label belongs ON the thing it acts on — "TAKE THE GUN" beside the
   turret, "HOLD TO REBUILD" on the panel being rebuilt. In a corner it
   is a button talking about something somewhere else, and because the
   tutorial ring targets this button, the instruction went to the corner
   with it. */
.bx-action.is-anchored {
  position: absolute;
  transform: translateX(-50%);
  /* Smoothed so it glides with the camera instead of juddering
     frame-to-frame as the projection lands on sub-pixels. */
  transition: left 90ms linear, top 90ms linear;
}
.bx-action.is-anchored:active { transform: translateX(-50%) translateY(2px); }
/* The container stops being a positioning corner once its child has
   left it — otherwise the anchored coordinates would be measured from
   the bottom-right box rather than the stage. */
.bx-actions:has(> .bx-action.is-anchored) {
  right: 0; bottom: 0; left: 0; top: 0;
}
@media (prefers-reduced-motion: reduce) {
  .bx-action.is-anchored { transition: none; }
}
.bx-action[data-variant="ghost"] {
  color: rgba(255, 220, 130, 0.95);
  background: rgba(0, 0, 0, 0.55);
  border-color: rgba(255, 220, 130, 0.6);
  box-shadow: none;
  animation: none;
}
.bx-action:active { transform: translateY(2px); }
@keyframes bxAction {
  0%, 100% { box-shadow: 0 3px 0 rgba(70,40,10,0.9), 0 8px 20px rgba(0,0,0,0.5); }
  50%      { box-shadow: 0 3px 0 rgba(70,40,10,0.9), 0 8px 26px rgba(246,201,91,0.55); }
}

/* The team cards become the hero switcher here, so they need to look
   pressable and show which hero is being driven. */
.game-stage .game-hud__team { pointer-events: auto; }

@media (prefers-reduced-motion: reduce) {
  .bx-action { animation: none; }
  .bx-callout.is-in { animation-duration: 1800ms; }
  .bx-bar > span[data-level="crit"],
  .bx-status__value.is-bumped { animation: none; }
}


/* --- Waypoint ------------------------------------------------------
   Waypoint. A ring when the target is on screen, an arrow pinned to
   the edge when it isn't — on a map four times the size of the view,
   "go there" is unanswerable without one. */
.bx-way {
  position: absolute;
  width: 34px; height: 34px;
  margin: -17px 0 0 -17px;
  pointer-events: none;
  z-index: 6;
}
.bx-way[hidden] { display: none; }
.bx-way__arrow {
  position: absolute;
  inset: 0;
  transform: rotate(var(--way-rot, 0rad));
}
.bx-way__arrow::before {
  content: '';
  position: absolute;
  left: 50%; top: 0;
  width: 0; height: 0;
  margin-left: -9px;
  border-left: 9px solid transparent;
  border-right: 9px solid transparent;
  border-bottom: 15px solid rgba(246, 201, 91, 0.95);
  filter: drop-shadow(0 0 8px rgba(246, 201, 91, 0.6));
}
/* On-screen: swap the arrow for a target ring sitting on the spot. */
.bx-way[data-mode="here"] .bx-way__arrow::before { display: none; }
.bx-way[data-mode="here"]::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 3px solid rgba(246, 201, 91, 0.9);
  box-shadow: 0 0 18px rgba(246, 201, 91, 0.5);
  animation: bxCoach 1.4s ease-in-out infinite;
}
.bx-way[data-mode="far"] { animation: bxWayBob 1.2s ease-in-out infinite; }
@keyframes bxWayBob { 50% { opacity: 0.55; } }

.bx-status__value--load { color: #c8ccd0; }
.bx-status__value--load.is-carrying { color: #7ee08a; }

@media (prefers-reduced-motion: reduce) {
  .bx-coach__ring,
  .bx-way[data-mode="here"]::after,
  .bx-way[data-mode="far"] { animation: none; }
}


/* Edge chips for blocked crew who are off screen. Pinned to the side
   of the view nearest them, so the chip's position IS the direction —
   no arrow needed, and the label answers "why" in the same glance. */
.bx-markers { position: absolute; inset: 0; pointer-events: none; z-index: 5; }
.bx-marker {
  position: absolute;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 3px clamp(5px, 2cqi, 10px);
  border-radius: 999px;
  background: rgba(6, 8, 10, 0.9);
  border: 1.5px solid rgba(200, 205, 215, 0.4);
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(7px, 2.1cqi, 11px);
  letter-spacing: 0.08em;
  white-space: nowrap;
  color: rgba(226, 220, 208, 0.9);
  max-width: 74cqi;
  overflow: hidden;
}
/* Direction arrow. Rotates the FULL 360 degrees to point from the
   hero you are driving at whatever is off screen — the base art
   points right (0rad = due east) so the rotation can be used raw. */
.bx-marker__arrow {
  width: 0; height: 0;
  flex: 0 0 auto;
  border-left: 8px solid currentColor;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  transform: rotate(var(--bx-arrow-rot, 0rad));
}
/* What is out there, not just which way. With scrap AND crew both
   able to raise a chip, the arrow alone stopped being an answer. */
.bx-marker__icon {
  width: 14px; height: 14px;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  line-height: 0;
}
.bx-marker__icon[hidden] { display: none; }
.bx-marker__icon img,
.bx-marker__icon svg {
  width: 100%; height: 100%;
  object-fit: cover;
  border-radius: 3px;
}
/* Floating over a visible target rather than pinned at the screen
   edge. The arrow goes — there is no direction to point in — and so
   does the label: the icon already says what it is, and three full
   pills hovering over three raiders would obscure more of the fight
   than they explain. Off-screen chips keep their words, because there
   the word is all the player gets. */
.bx-marker[data-near="1"] .bx-marker__arrow,
.bx-marker[data-near="1"] .bx-marker__label { display: none; }
.bx-marker[data-near="1"] {
  opacity: 0.95;
  padding: 3px;
  border-radius: 50%;
}

.bx-marker[data-variant="scrap"] {
  border-color: rgba(246, 201, 91, 0.9);
  color: #f6c95b;
}
.bx-marker[data-variant="blocked"] {
  border-color: rgba(255, 138, 106, 0.95);
  color: #ff8a6a;
  animation: bxMarkerPulse 1.3s ease-in-out infinite;
}
@keyframes bxMarkerPulse {
  0%, 100% { box-shadow: 0 0 0 rgba(255, 138, 106, 0); }
  50%      { box-shadow: 0 0 16px rgba(255, 138, 106, 0.55); }
}
@media (prefers-reduced-motion: reduce) {
  .bx-marker[data-variant="blocked"] { animation: none; }
}

/* --- Gunner view ----------------------------------------------------
   While Chibi is in the chair the whole stage is an aiming surface:
   the stick is removed (she can't drive) and pointer events fall
   through to the canvas, where the engine reads them. */
.bx.is-gunner .bx-stick { display: none; }

.bx-reticle {
  position: absolute;
  width: 42px; height: 42px;
  margin: -21px 0 0 -21px;
  pointer-events: none;
  z-index: 5;
}
.bx-reticle[hidden] { display: none; }
.bx-reticle::before,
.bx-reticle::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid rgba(255, 120, 210, 0.85);
}
.bx-reticle::after {
  inset: 13px;
  border-width: 1.5px;
  border-color: rgba(255, 240, 250, 0.9);
}
.bx-reticle[data-firing] {
  animation: bxRetFire 140ms steps(2) infinite;
}
@keyframes bxRetFire {
  0%   { transform: scale(1); }
  100% { transform: scale(1.14); }
}
@media (prefers-reduced-motion: reduce) {
  .bx-reticle[data-firing] { animation: none; }
}

/* ================================================================
   TUTORIAL COACH — shared across every game
   ================================================================
   platform/ui/coach.js. Built after a player pressed LEAVE GUN when
   the tutorial said "Leave her there — switch to HAUL": prose is the
   least reliable pointer available, so the screen dims, a hole opens
   over the exact control, and everything else stops accepting input.
   You cannot press the wrong button if the wrong button isn't there.

   The dim is four panels around the hole rather than one scrim with a
   box-shadow — a shadow spotlight looks identical but has no real
   hole, so the highlighted control would be lit and dead.
   ================================================================ */
.coach { position: absolute; inset: 0; z-index: 7; pointer-events: none; }
.coach[hidden] { display: none; }
.coach.is-exiting { animation: practiceExit 300ms ease-in forwards; }

/* Banner column — title pill, progress dots, hint. Same layout the
   practice overlay used before it was folded into the shared coach, so
   the .practice-overlay__* rules further up still style the contents
   unchanged. Padding clears the TUTORIAL badge top-left, the resource
   sidebar left, and the audio button right, at every stage size. */
.coach__top {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: none;
  padding: clamp(40px, 8cqb, 90px) clamp(48px, 14cqi, 90px) 0;
  animation: practiceEnter 340ms cubic-bezier(0.2, 1.2, 0.3, 1) both;
}
.coach__top[hidden] { display: none; }
.coach.is-complete .practice-overlay__banner {
  animation: practiceCompletePulse 480ms cubic-bezier(0.2, 1.6, 0.3, 1) both;
}

/* The steering panel positions itself (.tut__steer), so this is only a
   mounting point that doesn't eat pointer events — the whole point of
   the steer tutorial is that you can reach the stick through it and
   actually steer while it is up. */
.coach__zones { position: absolute; inset: 0; pointer-events: none; }
.coach__zones[hidden] { display: none; }
.coach__bubble[hidden] { display: none; }
.tut__actions[hidden] { display: none; }
.coach__panel {
  position: absolute;
  background: rgba(2, 4, 6, 0.74);
  opacity: 0;
  transition: opacity 200ms ease-out;
  pointer-events: none;
}
/* Panels only exist — and only swallow input — while spotlighting. */
.coach.is-spotlit .coach__panel { opacity: 1; pointer-events: auto; }

.coach__ring {
  position: absolute;
  border-radius: 12px;
  border: 2.5px solid rgba(246, 201, 91, 0.95);
  box-shadow: 0 0 24px rgba(246, 201, 91, 0.5),
              inset 0 0 20px rgba(246, 201, 91, 0.18);
  animation: coachPulse 1.5s ease-in-out infinite;
  pointer-events: none;
}
.coach__ring[hidden] { display: none; }
/* Stronger than it was. The old pulse moved 3.5% and dropped to 0.6
   opacity — at a glance, on a moving field, that is a static box. This
   breathes wider and stays bright, because the ring's whole job is to
   be the thing the eye lands on first. */
@keyframes coachPulse {
  0%, 100% { opacity: 1;    transform: scale(1); }
  50%      { opacity: 0.78; transform: scale(1.075); }
}
/* POINT AT IT. A ring says "this thing is special"; a caret bouncing on
   top of it says "press this one, now". Every mobile game that teaches
   a tap uses one, because a highlight alone is ambiguous between "look"
   and "touch" — and the report that prompted this was exactly that: the
   player could not tell they were meant to pick a hero. */
.coach__ring::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: calc(100% + clamp(6px, 1.8cqi, 12px));
  width: 0; height: 0;
  margin-left: clamp(-11px, -2.6cqi, -7px);
  border-left:  clamp(7px, 2.6cqi, 11px) solid transparent;
  border-right: clamp(7px, 2.6cqi, 11px) solid transparent;
  border-top:   clamp(9px, 3.2cqi, 14px) solid rgba(246, 201, 91, 0.98);
  filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.7));
  animation: coachPoint 1.1s cubic-bezier(0.4, 0, 0.5, 1) infinite;
}
@keyframes coachPoint {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(clamp(5px, 1.6cqi, 9px)); }
}
@media (prefers-reduced-motion: reduce) {
  .coach__ring { animation: none; }
  .coach__ring::after { animation: none; }
}

/* The instruction block: bubble + skip row, stacked, moved as ONE
   thing so a single decision keeps both clear of the control the step
   is highlighting. They used to be positioned independently, which put
   the skip row on top of whatever button the step was pointing at. */
.coach__stack {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: min(340px, 90cqi);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(6px, 2cqb, 12px);
  pointer-events: none;
}
/* Default is the BOTTOM, within reach of the thumb. `above` is the
   escape hatch the placement code picks when the bottom would cover
   the highlighted control — see placeStack in platform/ui/coach.js. */
/* ================================================================
   THE TASK CHIP — the folded form of an instruction.
   ================================================================
   Shown once the open form has had its dwell. Same icon and the same
   progress as the banner it replaces, so it reads as the instruction
   minimised rather than as a new piece of UI. Tapping it restores the
   full text.

   Deliberately in the SHARED template: a player who learns what this
   chip means in the runner already knows it in the bastion. That is
   most of what makes several engines feel like one game. */
.coach__task {
  position: absolute;
  top: clamp(44px, 12cqb, 92px);
  left: clamp(6px, 2cqi, 14px);
  display: none;
  align-items: center;
  gap: clamp(3px, 1.2cqi, 7px);
  padding: clamp(3px, 1.2cqi, 7px) clamp(6px, 2.2cqi, 11px);
  border-radius: 999px;
  background: rgba(10, 7, 5, 0.72);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 1.5px solid rgba(246, 201, 91, 0.5);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
  color: #ffdc82;
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(8px, 2.4cqi, 12px);
  letter-spacing: 0.06em;
  pointer-events: auto;
  cursor: pointer;
  /* One slow breath so the eye can find it after it appears, then
     nothing — a chip that pulses forever is a chip you learn to
     ignore. */
  animation: coachTaskIn 420ms cubic-bezier(0.2, 1.3, 0.3, 1) both;
}
.coach.is-collapsed .coach__task { display: inline-flex; }
.coach__task:hover  { border-color: rgba(246, 201, 91, 0.9); }
.coach__task:active { transform: translateY(1px); }
.coach__task-icon   { display: grid; place-items: center; line-height: 0; }
.coach__task-icon svg { width: clamp(13px, 4cqi, 20px); height: clamp(13px, 4cqi, 20px); }
.coach__task-dots[hidden] { display: none; }
@keyframes coachTaskIn {
  from { opacity: 0; transform: scale(0.7) translateY(-6px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

/* Folding away. The stack keeps its layout so re-expanding doesn't
   re-measure from scratch; it just stops being visible or clickable. */
.coach.is-collapsed .coach__stack,
.coach.is-collapsed .coach__top {
  opacity: 0;
  pointer-events: none;
  transform: translateX(-50%) scale(0.94);
  transition: opacity 260ms ease, transform 260ms ease;
}
.coach.is-collapsed .coach__top { transform: scale(0.94); }
.coach__stack, .coach__top { transition: opacity 220ms ease, transform 220ms ease; }

@media (prefers-reduced-motion: reduce) {
  .coach__task { animation: none; }
  .coach.is-collapsed .coach__stack,
  .coach.is-collapsed .coach__top { transition: none; }
}

.coach__stack[data-pos="below"] { bottom: var(--above-heroes); }
.coach__stack[data-pos="above"] { top: clamp(44px, 12cqb, 92px); }
/* Anchored to a target: coach.js computes the exact top so the
   instruction sits against the control it is about. The two slots
   above still define the safe band it is clamped into. */
.coach__stack[data-pos="anchored"] { bottom: auto; }
/* The banner owns the top while it's up. */
.coach.has-banner .coach__stack[data-pos="above"] { top: clamp(112px, 26cqb, 210px); }

.coach__bubble {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(3px, 1.2cqi, 7px);
  padding: clamp(5px, 2cqi, 12px) clamp(8px, 3cqi, 16px);
  border-radius: 10px;
  /* Translucent on purpose: this sits over a live field and the player
     should be able to see what is happening THROUGH it. Backed by a
     blur so the text stays legible over busy ground rather than being
     bought legibility with opacity. */
  background: linear-gradient(180deg, rgba(14, 12, 10, 0.82), rgba(6, 5, 4, 0.76));
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 1.5px solid rgba(246, 201, 91, 0.42);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
  font-family: "Rajdhani", system-ui, sans-serif;
  text-align: center;
  pointer-events: none;
}
/* THE INSTRUCTION IS THE LOUDEST TEXT ON THE SCREEN, or it isn't an
   instruction. This was clamp(9px, 2.9cqi, 15px) — about ELEVEN pixels
   on a phone-width stage, smaller than the status strip it was
   competing with and well under the ~16px floor mobile games use for
   copy the player is expected to act on. A tutorial line nobody reads
   is a tutorial that isn't running. */
.coach__line {
  font-size: clamp(15px, 4.2cqi, 22px);
  font-weight: 600;
  line-height: 1.32;
  letter-spacing: 0.01em;
  color: #fff1c9;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}
.coach__line[hidden] { display: none; }
.coach__keys {
  display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 4px;
}
.coach__keys[hidden] { display: none; }
.coach__or {
  margin-right: 2px;
  font-size: clamp(7px, 2.1cqi, 11px);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(200, 205, 215, 0.6);
}
.coach__key {
  display: inline-block;
  min-width: clamp(15px, 4.6cqi, 24px);
  padding: 1px clamp(3px, 1.2cqi, 7px);
  border-radius: 4px;
  border: 1px solid rgba(217, 178, 107, 0.55);
  border-bottom-width: 2px;
  background: linear-gradient(180deg, #2a2118 0%, #16100a 100%);
  color: #ffe6a8;
  font-weight: 700;
  font-size: clamp(8px, 2.4cqi, 12px);
  line-height: 1.5;
  text-align: center;
}
.coach__actions {
  display: flex; align-items: center; justify-content: center;
  gap: clamp(4px, 1.6cqi, 10px);
  pointer-events: auto;
}
.coach__optout { padding: 2px 8px; }
.coach__actions .tut__skip-btn {
  padding: 3px 12px;
  font-size: clamp(8px, 2.4cqi, 12px);
}

@media (prefers-reduced-motion: reduce) {
  .coach__ring { animation: none; }
  .coach__panel { transition: none; }
}

/* ================================================================
   THE CALLING SCREEN — platform/ui/choice.js
   ================================================================
   A permanent decision, so it is given the whole screen and two
   actions. Everything here is in service of one idea: the player
   should finish reading before they finish choosing. */
.choose {
  position: absolute;
  inset: 0;
  z-index: 9;
  display: grid;
  place-items: center;
  pointer-events: auto;
  animation: chooseIn 380ms ease-out both;
}
.choose.is-leaving { animation: chooseOut 400ms ease-in forwards; }
@keyframes chooseIn  { from { opacity: 0; } to { opacity: 1; } }
@keyframes chooseOut { to { opacity: 0; transform: scale(1.03); } }

.choose__scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(70% 55% at 50% 42%, rgba(10, 7, 4, 0.86), rgba(2, 3, 4, 0.96) 70%);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}

.choose__panel {
  position: relative;
  width: min(98cqi, 620px);
  max-height: 94cqb;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(4px, 1.4cqb, 10px);
  padding: clamp(10px, 3cqb, 22px) clamp(8px, 3cqi, 18px);
  text-align: center;
  font-family: "Rajdhani", system-ui, sans-serif;
}

.choose__eyebrow {
  font-size: clamp(7px, 2.2cqi, 11px);
  letter-spacing: 0.28em;
  color: rgba(246, 201, 91, 0.75);
}
.choose__title {
  margin: 0;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(16px, 6cqi, 34px);
  letter-spacing: 0.08em;
  color: #ffdc82;
  text-shadow: 0 2px 0 rgba(60, 30, 5, 0.9), 0 0 26px rgba(246, 201, 91, 0.55);
}

.choose__cards {
  display: flex;
  gap: clamp(4px, 1.4cqi, 9px);
  width: 100%;
  margin: clamp(4px, 1.5cqb, 12px) 0;
}

/* One card per path. The accent is the only thing that differs
   between them, so the row reads as three of the same kind of thing
   rather than three unrelated buttons. */
.choose__card {
  flex: 1 1 0;
  min-width: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(2px, 0.8cqb, 5px);
  padding: clamp(6px, 2cqi, 12px) clamp(3px, 1cqi, 7px);
  border-radius: clamp(8px, 2.4cqi, 14px);
  background: linear-gradient(180deg, rgba(26, 22, 18, 0.95), rgba(8, 7, 6, 0.95));
  border: 1.5px solid rgba(217, 178, 107, 0.4);
  color: #f4ead6;
  cursor: pointer;
  text-align: center;
  transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
  animation: chooseCardIn 420ms cubic-bezier(0.2, 1.2, 0.3, 1) both;
  animation-delay: calc(var(--i, 0) * 80ms + 120ms);
}
@keyframes chooseCardIn {
  from { opacity: 0; transform: translateY(14px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.choose__card:hover,
.choose__card:focus-visible {
  transform: translateY(-3px);
  border-color: var(--accent);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.55), 0 0 22px color-mix(in srgb, var(--accent) 40%, transparent);
}
/* Once something is picked, the others recede — the screen shows the
   decision that has been made, not the ones that haven't. */
.choose.has-pick .choose__card { opacity: 0.42; }
.choose__card.is-picked {
  opacity: 1;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent), 0 12px 30px rgba(0, 0, 0, 0.6);
  transform: translateY(-4px);
}

.choose__key {
  position: absolute;
  top: 0; left: 0;
  min-width: clamp(11px, 3.4cqi, 18px);
  padding: 0 3px;
  border-bottom-right-radius: clamp(4px, 1.4cqi, 8px);
  background: rgba(6, 5, 4, 0.9);
  border-right: 1px solid rgba(217, 178, 107, 0.5);
  border-bottom: 1px solid rgba(217, 178, 107, 0.5);
  font-weight: 700;
  font-size: clamp(7px, 2.2cqi, 11px);
  color: #ffdc82;
}
.choose__art {
  width: clamp(34px, 13cqi, 74px);
  height: clamp(34px, 13cqi, 74px);
  display: grid;
  place-items: center;
  border-radius: 50%;
  overflow: hidden;
  background: radial-gradient(circle at 50% 35%, rgba(80, 60, 30, 0.7), rgba(8, 6, 4, 0.95));
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent);
}
.choose__art img { width: 100%; height: 100%; object-fit: cover; }
.choose__name {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(9px, 3cqi, 16px);
  letter-spacing: 0.06em;
  color: var(--accent);
}
.choose__role {
  font-size: clamp(6px, 2cqi, 10px);
  letter-spacing: 0.18em;
  color: rgba(244, 234, 214, 0.6);
}
/* The reward, stated where the decision is made — on two lines,
   because "Gathering Speed" and "Construction Speed" are the terms
   that matter and abbreviating them to fit a pill is exactly the kind
   of saving that makes a permanent choice harder to reason about.
   The block is full width and wraps; nothing here is truncated. */
.choose__boost {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  width: 100%;
  margin-top: 3px;
  padding: clamp(3px, 1cqi, 6px) clamp(2px, 0.8cqi, 5px);
  border-radius: clamp(5px, 1.6cqi, 9px);
  background: color-mix(in srgb, var(--accent) 16%, rgba(0, 0, 0, 0.6));
  border: 1px solid color-mix(in srgb, var(--accent) 50%, transparent);
}
.choose__boost-value {
  font-weight: 700;
  font-size: clamp(7px, 2.15cqi, 11px);
  line-height: 1.15;
  letter-spacing: 0.01em;
  color: #fff6e2;
  text-wrap: balance;
}
.choose__boost-kind {
  font-size: clamp(5.5px, 1.65cqi, 9px);
  line-height: 1.2;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--accent) 80%, #fff);
}
.choose__blurb {
  font-size: clamp(6px, 1.9cqi, 10px);
  line-height: 1.35;
  color: rgba(244, 234, 214, 0.62);
}

.choose__note {
  margin: 0;
  font-size: clamp(7px, 2.1cqi, 11px);
  letter-spacing: 0.1em;
  color: rgba(255, 170, 120, 0.9);
}

.choose__confirm {
  margin-top: clamp(4px, 1.4cqb, 10px);
  padding: clamp(6px, 2cqb, 12px) clamp(14px, 5cqi, 30px);
  border-radius: 999px;
  border: 2px solid rgba(255, 220, 130, 0.9);
  background: linear-gradient(180deg, #ffdc82, #d8a040);
  color: #2a1c06;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(9px, 3cqi, 15px);
  letter-spacing: 0.1em;
  cursor: pointer;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  transition: transform 140ms ease, filter 140ms ease;
}
.choose__confirm[disabled] {
  filter: grayscale(0.8) brightness(0.55);
  cursor: default;
  box-shadow: none;
}
.choose__confirm:not([disabled]):hover { transform: translateY(-2px); }
.choose__confirm:not([disabled]):active { transform: translateY(1px); }

@media (prefers-reduced-motion: reduce) {
  .choose, .choose__card { animation: none; }
  .choose__card, .choose__confirm { transition: none; }
}

/* ================================================================
   TASKS — the ever-present button and its panel
   ================================================================
   Sits under the audio control, top-right, on every screen. The badge
   is the only part seen without asking, so it means exactly one thing:
   there is a reward here you have not taken. */
.tasks { position: absolute; inset: 0; pointer-events: none; z-index: 8; }

.tasks__btn {
  position: absolute;
  top: clamp(44px, 13cqb, 92px);
  right: clamp(6px, 2.4cqi, 16px);
  width: clamp(26px, 8.5cqi, 42px);
  height: clamp(26px, 8.5cqi, 42px);
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgba(10, 7, 5, 0.72);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  border: 1.5px solid rgba(217, 178, 107, 0.55);
  color: #ffdc82;
  cursor: pointer;
  pointer-events: auto;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
  transition: transform 140ms ease, border-color 140ms ease;
}
.tasks__btn svg { width: 58%; height: 58%; }
.tasks__btn:hover { transform: translateY(-2px); border-color: rgba(246, 201, 91, 0.95); }
.tasks__btn.has-claim {
  border-color: #7ee08a;
  animation: tasksNudge 2.4s ease-in-out infinite;
}
@keyframes tasksNudge {
  0%, 88%, 100% { box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45); }
  94%           { box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45), 0 0 20px rgba(126, 224, 138, 0.75); }
}
.tasks__badge {
  position: absolute;
  top: -3px; right: -3px;
  min-width: clamp(12px, 4cqi, 18px);
  padding: 0 3px;
  border-radius: 999px;
  background: #7ee08a;
  color: #06210c;
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(7px, 2.4cqi, 11px);
  line-height: clamp(12px, 4cqi, 18px);
  text-align: center;
}
.tasks__badge[hidden] { display: none; }

.tasks__panel {
  position: absolute;
  top: clamp(76px, 23cqb, 142px);
  right: clamp(6px, 2.4cqi, 16px);
  width: min(88cqi, 340px);
  max-height: 62cqb;
  overflow-y: auto;
  padding: clamp(6px, 2cqi, 12px);
  border-radius: clamp(8px, 2.4cqi, 14px);
  background: linear-gradient(180deg, rgba(16, 13, 10, 0.97), rgba(6, 5, 4, 0.97));
  border: 1.5px solid rgba(217, 178, 107, 0.5);
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.7);
  font-family: "Rajdhani", system-ui, sans-serif;
  color: #f4ead6;
  pointer-events: auto;
  animation: tasksPanelIn 220ms cubic-bezier(0.2, 1.2, 0.3, 1) both;
}
.tasks__panel[hidden] { display: none; }
@keyframes tasksPanelIn {
  from { opacity: 0; transform: translateY(-8px) scale(0.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

.tasks__head {
  display: flex; align-items: center; gap: 6px;
  margin-bottom: clamp(4px, 1.4cqi, 8px);
}
.tasks__title {
  flex: 1 1 auto;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(10px, 3.2cqi, 15px);
  letter-spacing: 0.1em;
  color: #ffdc82;
}
.tasks__purse {
  padding: 1px clamp(4px, 1.6cqi, 9px);
  border-radius: 999px;
  background: rgba(126, 224, 138, 0.16);
  border: 1px solid rgba(126, 224, 138, 0.55);
  font-weight: 700;
  font-size: clamp(7px, 2.2cqi, 10px);
  color: #b9f0c4;
}
.tasks__purse[hidden] { display: none; }
.tasks__close {
  width: 20px; height: 20px;
  border: 0; border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
  color: #f4ead6;
  font-size: 15px; line-height: 1;
  cursor: pointer;
}

.tasks__section + .tasks__section { margin-top: clamp(6px, 2cqi, 12px); }
.tasks__section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  padding-bottom: 3px;
  border-bottom: 1px solid rgba(217, 178, 107, 0.28);
  font-weight: 700;
  font-size: clamp(7px, 2.3cqi, 11px);
  letter-spacing: 0.16em;
  color: rgba(246, 201, 91, 0.85);
}
.tasks__hint {
  letter-spacing: 0.04em;
  font-weight: 400;
  font-size: clamp(6px, 1.9cqi, 9px);
  color: rgba(244, 234, 214, 0.45);
}

.tasks__row {
  display: flex; align-items: center; gap: clamp(4px, 1.6cqi, 10px);
  padding: clamp(4px, 1.4cqi, 8px) 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.tasks__row-main { flex: 1 1 auto; min-width: 0; }
.tasks__row-side {
  flex: 0 0 auto;
  display: flex; flex-direction: column; align-items: flex-end; gap: 2px;
}
.tasks__name {
  display: block;
  font-weight: 700;
  font-size: clamp(8px, 2.6cqi, 12px);
  color: #ffe6a8;
}
.tasks__blurb {
  display: block;
  font-size: clamp(6.5px, 2cqi, 10px);
  line-height: 1.25;
  color: rgba(244, 234, 214, 0.55);
}
/* Bar for the glance, numbers for the decision. */
.tasks__bar {
  display: block;
  height: 4px;
  margin-top: 4px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
  overflow: hidden;
}
.tasks__bar i {
  display: block; height: 100%;
  background: linear-gradient(90deg, #d8a040, #ffdc82);
  transition: width 320ms ease;
}
.tasks__row.is-done .tasks__bar i { background: linear-gradient(90deg, #4fae63, #7ee08a); }
.tasks__count {
  font-weight: 700;
  font-size: clamp(8px, 2.5cqi, 12px);
  color: #f4ead6;
}
.tasks__count span { font-weight: 400; color: rgba(244, 234, 214, 0.45); }

.tasks__claim {
  padding: 2px clamp(5px, 2cqi, 10px);
  border-radius: 999px;
  border: 1.5px solid #7ee08a;
  background: linear-gradient(180deg, #7ee08a, #4fae63);
  color: #06210c;
  font-weight: 700;
  font-size: clamp(7px, 2.3cqi, 11px);
  cursor: pointer;
  animation: tasksClaimPulse 1.6s ease-in-out infinite;
}
@keyframes tasksClaimPulse {
  0%, 100% { box-shadow: 0 0 0 rgba(126, 224, 138, 0); }
  50%      { box-shadow: 0 0 14px rgba(126, 224, 138, 0.7); }
}
.tasks__reward {
  font-weight: 700;
  font-size: clamp(7px, 2.2cqi, 10px);
  color: rgba(246, 201, 91, 0.75);
}
.tasks__reward.is-taken { color: rgba(126, 224, 138, 0.6); }
.tasks__row.is-claimed { opacity: 0.55; }
.tasks__row.is-paid { animation: tasksPaid 520ms ease-out; }
@keyframes tasksPaid {
  0%   { background: rgba(126, 224, 138, 0.28); }
  100% { background: transparent; }
}

@media (prefers-reduced-motion: reduce) {
  .tasks__btn.has-claim, .tasks__claim, .tasks__panel, .tasks__row.is-paid { animation: none; }
  .tasks__bar i { transition: none; }
}

/* --- Active boosts -------------------------------------------------
   A row under the status strip, hard left. One chip per boost: icon,
   percentage, and the term spelled out — "GATHER SPEED", not "GTH".
   Invisible until something has been earned, so a player who has not
   chosen a calling never sees an empty shelf. */
.bx-boosts {
  position: absolute;
  top: calc(clamp(30px, 9cqb, 62px) + clamp(4px, 1.2cqb, 9px));
  left: clamp(6px, 2.4cqi, 16px);
  display: flex;
  gap: clamp(3px, 1.2cqi, 7px);
  pointer-events: none;
  z-index: 4;
}
.bx-boosts[hidden] { display: none; }
.bx-boost {
  display: inline-flex;
  align-items: center;
  gap: clamp(2px, 0.9cqi, 5px);
  padding: clamp(2px, 0.9cqi, 4px) clamp(4px, 1.7cqi, 9px);
  border-radius: 999px;
  background: rgba(6, 8, 10, 0.82);
  border: 1.5px solid rgba(126, 224, 138, 0.6);
  color: #b9f0c4;
  font-family: "Rajdhani", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(6.5px, 2cqi, 10px);
  letter-spacing: 0.08em;
  white-space: nowrap;
}
.bx-boost[data-boost="attack"] { border-color: rgba(255, 92, 208, 0.6); color: #ffb4e6; }
.bx-boost[data-boost="gather"] { border-color: rgba(143, 182, 201, 0.7); color: #cfe6f2; }
.bx-boost__icon { display: grid; place-items: center; line-height: 0; }
.bx-boost__icon svg {
  width: clamp(8px, 2.6cqi, 13px);
  height: clamp(8px, 2.6cqi, 13px);
}
.bx-boost__pct { color: #fff; }
.bx-boost__label { opacity: 0.85; }

/* ===================================================================
   THE STANDOFF - the wave-ten duel (games/bastion/boss.js)

   THIS IS A FRAME, NOT A PANEL.

   The fight itself is on the canvas underneath: the warlord across the
   top, the three heroes in formation across the bottom, and everything
   that crosses between them. What lives here is the state a turn is
   decided on - his health, his tell, the clock, what you can press.

   So the column is deliberately hollow in the middle. Chrome banks at
   the top and the bottom, the band between them draws nothing, and the
   arena shows through it. That split is the whole layout rule: canvas
   owns motion, DOM owns text, and neither draws in the other's space.

   Everything sizes in cq units - this has to be legible on a phone
   held one-handed, which is where the tap lands.
   =================================================================== */
.bx-duel {
  position: absolute;
  inset: 0;
  z-index: 60;
  container-type: inline-size;
  animation: bxDuelIn 320ms ease-out both;
}
@keyframes bxDuelIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.bx-duel__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: clamp(3px, 1.2cqmin, 10px);
  padding: clamp(6px, 2.4cqmin, 18px);
  height: 100%;
  /* BORDER-BOX. There is no global reset in this stylesheet, so
     `height: 100%` plus padding was taller than the stage by the
     padding — and the overflow landed on the bottom row of ability
     cards, which is the row the player is reaching for. Same bug as
     the summary panel had, one screen over. */
  box-sizing: border-box;
}

/* --- The top bar --------------------------------------------------
   Sits over the arena, so it carries its own gradient backing: text
   this important cannot depend on whatever the floor happens to be
   doing behind it. Fades to nothing at the bottom edge rather than
   ending on a line, because a hard edge here reads as a letterbox and
   the arena is supposed to continue underneath. */
.bx-duel__top {
  display: flex;
  align-items: flex-start;
  gap: clamp(6px, 2cqmin, 14px);
  padding-bottom: clamp(4px, 1.6cqmin, 12px);
  /* ROOM FOR THE SESSION CONTROLS. Pause and audio sit in the stage's
     top-right corner and are drawn above this screen — correctly,
     since a modal you cannot pause is a trap. The whole header steps
     aside rather than just the name: the health bar is the largest
     object on the screen and its right end was disappearing under the
     speaker icon, which is exactly where the last 10% of a boss's
     health gets read. */
  padding-right: calc(clamp(22px, 8cqi, 40px) * 2 + clamp(18px, 5cqi, 30px));
  background: linear-gradient(180deg,
              rgba(12, 7, 4, 0.86) 0%,
              rgba(12, 7, 4, 0.55) 62%,
              rgba(12, 7, 4, 0) 100%);
}
.bx-duel__boss { flex: 1 1 auto; min-width: 0; }
.bx-duel__boss-name {
  font-family: "Cinzel", serif;
  font-weight: 900;
  /* One step down from the old size, and it buys the whole name. THE
     NUMB SKULL WARLORD is twenty characters of Cinzel at 0.14em
     tracking; at --font-md on a phone it ran under the pause button
     and ellipsised to "THE NUMB SKULL WARL…", which is the one string
     on this screen that has to survive intact — he is who you are
     fighting. */
  font-size: var(--font-sm);
  letter-spacing: 0.1em;
  color: #ffd7d7;
  text-shadow: 0 0 14px rgba(255, 70, 70, 0.6), 0 2px 4px rgba(0, 0, 0, 0.9);
  /* WRAPS RATHER THAN TRUNCATES. On a narrow stage — a Discord panel
     dragged in, a phone in landscape — the name still did not fit and
     ellipsised to "THE NUMB SKULL …", which drops the only word in it
     that says what he is. Two lines cost fourteen pixels of header,
     and layoutStage measures the header, so the arena moves down to
     suit rather than being drawn over. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* --- Health -------------------------------------------------------
   His bar is the biggest single element on the screen, which is
   correct: it is the score, the timer and the goal all at once. */
.bx-duel__bar {
  position: relative;
  height: clamp(10px, 3cqmin, 20px);
  margin-top: clamp(2px, 0.8cqmin, 6px);
  background: rgba(0, 0, 0, 0.72);
  border: 1.5px solid rgba(255, 200, 170, 0.5);
  border-radius: 999px;
  overflow: hidden;
}
.bx-duel__bar-fill {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, #ff7b6b, #b8241c);
  box-shadow: 0 0 16px rgba(255, 80, 60, 0.55);
  /* Deliberately NOT transitioned. The bar is read while a blow is
     landing, and an eased fill would still be travelling when the
     damage number arrives - which reads as the picture disagreeing
     with the number. */
}
/* The number, over the bar rather than beside it: one object to look
   at, and it cannot be separated from the bar by a line wrap. */
.bx-duel__bar-num {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-2xs);
  letter-spacing: 0.08em;
  color: #fff2e2;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.95);
  pointer-events: none;
}

/* The telegraph. Its whole job is to be impossible to miss for one
   turn, so it pulses - the only thing on this frame that moves while
   nothing is happening. */
.bx-duel__tell {
  margin-top: clamp(2px, 0.8cqmin, 6px);
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  font-size: var(--font-xs);
  letter-spacing: 0.2em;
  color: #ffb4b4;
  animation: bxTell 700ms ease-in-out infinite alternate;
}
.bx-duel__tell[hidden] { display: none; }
@keyframes bxTell {
  from { opacity: 0.4; }
  to   { opacity: 1; }
}

/* The clock. Small and cornered, not the centrepiece it used to be:
   it matters for about two seconds a turn and the arena needs the
   room the rest of the time. */
/* THE TURN CLOCK, in the control panel.
   It used to sit in the header's top-right corner — the same corner
   the pause and audio buttons are drawn in, over this screen — so the
   number counting down the player's turn was half behind a speaker
   icon. It now sits at the left of the state strip, next to the
   buttons it is timing rather than in the corner furthest from them,
   and it is a chip rather than a floating number so it holds its width
   as it counts 3 → 2 → 1 instead of shuffling the row beside it. */
.bx-duel__clock {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  min-width: clamp(24px, 7cqmin, 40px);
  padding: 1px clamp(4px, 1.4cqmin, 9px);
  border: 1px solid rgba(255, 210, 140, 0.35);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.4);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: clamp(13px, 4cqmin, 22px);
  line-height: 1.15;
  color: rgba(255, 226, 160, 0.95);
  text-shadow: 0 0 12px rgba(255, 180, 80, 0.45), 0 2px 4px rgba(0,0,0,0.9);
}
.bx-duel__clock.is-urgent {
  color: #ff9a8a;
  border-color: rgba(255, 120, 100, 0.8);
  background: rgba(60, 10, 6, 0.6);
  animation: bxClock 500ms ease-in-out infinite alternate;
}

/* The state strip: clock hard left, economy centred in what is left,
   so the two never trade places as their contents change length. */
.bx-duel__strip {
  display: flex;
  align-items: center;
  gap: clamp(6px, 2cqmin, 14px);
}
.bx-duel__strip .bx-duel__parts { flex: 1 1 auto; }
@keyframes bxClock {
  from { transform: scale(1); }
  to   { transform: scale(1.16); }
}

/* --- The arena band -----------------------------------------------
   Draws nothing. It exists to take the space the actors occupy so the
   chrome above and below it cannot creep into the fight. */
.bx-duel__field {
  flex: 1 1 auto;
  min-height: 0;
  pointer-events: none;
}

/* --- The log ------------------------------------------------------
   One line of plain language under the fight, saying what just
   happened. The animation says it too; this is what it said for
   anyone who blinked, and what a screen reader gets. */
.bx-duel__log {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 1px;
  text-align: center;
  /* A FIXED BAND. Two lines' worth, whether there are two, one or
     none. The log used to size to its content, which meant the control
     panel changed height every time the fight said something — and a
     panel that grows moves four buttons under a thumb already reaching
     for them. Reserved space is also what stops the crew having to
     dodge: the arena is laid out against this panel's top edge, and an
     edge that moves is not an edge. */
  min-height: calc(var(--font-xs) * 2.6);
}
/* The meter takes this band while it runs — see render() in boss.js.
   Hidden rather than stacked, so the height is the same either way. */
.bx-duel__log.is-swapped { display: none; }
.bx-duel__log-line {
  font-family: "Rajdhani", sans-serif;
  font-weight: 600;
  font-size: var(--font-xs);
  letter-spacing: 0.04em;
  color: rgba(255, 238, 210, 0.95);
  text-shadow: 0 2px 5px rgba(0, 0, 0, 1);
}
/* Older lines recede, so the newest is the one the eye lands on
   without anybody having to read three. */
.bx-duel__log-line:not(:last-child) { opacity: 0.4; font-size: var(--font-2xs); }

/* --- The timing meter ---------------------------------------------
   The one place skill enters the duel. It replaces the log band while
   it is up, so it arrives where the eye already is rather than
   somewhere the player has to find under time pressure. */
.bx-duel__meter { padding: clamp(2px, 1cqmin, 8px) 0; }
.bx-duel__meter[hidden] { display: none; }
.bx-duel__meter-track {
  position: relative;
  height: clamp(18px, 5cqmin, 30px);
  background: rgba(0, 0, 0, 0.78);
  border: 1.5px solid rgba(255, 220, 150, 0.55);
  border-radius: 6px;
  overflow: hidden;
}
/* The bands are DRAWN, not implied. A timing test whose target is
   invisible is a coin toss with extra steps. These percentages must
   match BAND_GOOD and BAND_PERFECT in boss.js. */
.bx-duel__meter-good {
  position: absolute;
  left: 20%;
  width: 60%;
  top: 0;
  bottom: 0;
  background: rgba(120, 200, 130, 0.3);
}
.bx-duel__meter-perfect {
  position: absolute;
  left: 44%;
  width: 12%;
  top: 0;
  bottom: 0;
  background: rgba(255, 216, 120, 0.58);
  box-shadow: 0 0 18px rgba(255, 200, 90, 0.7);
}
.bx-duel__meter-head {
  position: absolute;
  top: -2px;
  bottom: -2px;
  width: 4px;
  margin-left: -2px;
  background: #fff6e0;
  box-shadow: 0 0 12px rgba(255, 255, 255, 0.9);
}
.bx-duel__meter-hint {
  margin-top: clamp(2px, 0.8cqmin, 6px);
  text-align: center;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-xs);
  letter-spacing: 0.22em;
  color: rgba(255, 232, 175, 0.98);
  text-shadow: 0 2px 5px rgba(0, 0, 0, 1);
}

/* --- The control panel --------------------------------------------
   THE ONE STRUCTURAL CHANGE, and everything else here follows from it.

   Parts and the four buttons used to be two loose rows laid straight
   over the arena. The crew are drawn on that arena, each carrying a
   health bar under their feet, so the bottom third of the screen was
   three unrelated things sharing the same pixels: hero art, hero
   numbers, and the controls. Nothing was aligned to anything, because
   there was nothing to align to.

   Grouped and backed, it is a panel — the arena ends at a line and the
   controls begin at it. The canvas layout measures this element's top
   edge and keeps the crew above it (see layoutStage in boss.js), so
   the two halves of the screen cannot overlap however short the phone
   or however long the translated labels. */
.bx-duel__foot {
  display: flex;
  flex-direction: column;
  gap: clamp(3px, 1.2cqmin, 8px);
  padding: clamp(5px, 1.8cqmin, 12px) clamp(4px, 1.4cqmin, 10px)
           clamp(4px, 1.4cqmin, 10px);
  border-top: 1px solid rgba(255, 200, 130, 0.22);
  border-radius: clamp(8px, 2.4cqmin, 16px) clamp(8px, 2.4cqmin, 16px) 0 0;
  background: linear-gradient(180deg,
              rgba(14, 9, 5, 0) 0%,
              rgba(14, 9, 5, 0.82) 22%,
              rgba(10, 6, 3, 0.95) 100%);
  box-shadow: 0 -10px 26px rgba(0, 0, 0, 0.45);
}

/* --- Parts --------------------------------------------------------
   The duel's only economy, on one line: what Haul has swept up, and
   what is still lying on the floor waiting for him. */
.bx-duel__parts {
  display: flex;
  justify-content: center;
  gap: clamp(6px, 2cqmin, 16px);
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  font-size: var(--font-2xs);
  letter-spacing: 0.12em;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 1);
}
.bx-duel__parts-held  { color: #ffe2a0; }
.bx-duel__parts-loose { color: #9fd6a8; }

/* --- The four buttons ---------------------------------------------
   Bottom of the frame, under the thumb, big enough to hit without
   looking. Two columns rather than four: four across a phone gives
   every one of them a label nobody can read.

   They carry their own dark backing for the same reason the top bar
   does - they sit over a lit arena floor. */
.bx-duel__acts {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(4px, 1.6cqmin, 10px);
}
.bx-duel__act {
  display: grid;
  /* A FIXED SHAPE PER CARD, so four cards read as one control panel
     rather than as four different-sized boxes: an accent stripe that
     says whose it is, then who / what / why in three rows of settled
     height. Two of these labels are translated, and a grid that sizes
     to its content lets the longest of them decide how tall every card
     on the screen is. */
  grid-template-columns: clamp(3px, 1cqmin, 5px) 1fr;
  grid-template-rows: auto auto auto;
  align-content: center;
  gap: 0 clamp(6px, 2cqmin, 12px);
  position: relative;
  padding: clamp(5px, 1.8cqmin, 12px) clamp(7px, 2.2cqmin, 14px);
  min-height: clamp(46px, 11cqmin, 76px);
  background: linear-gradient(180deg, rgba(32, 21, 9, 0.95), rgba(18, 11, 4, 0.95));
  border: 1px solid rgba(255, 210, 130, 0.32);
  border-radius: 9px;
  cursor: pointer;
  text-align: left;
  overflow: hidden;
}
/* The stripe. Colour is the hero — the same faction colour their
   sprite carries in the yard — and it runs the full height of the card
   so the four cards can be told apart peripherally, without reading. */
.bx-duel__act::before {
  content: "";
  grid-column: 1;
  grid-row: 1 / -1;
  border-radius: 999px;
  background: currentColor;
  opacity: 0.9;
}
.bx-duel__act > * { grid-column: 2; }
.bx-duel__act:disabled { opacity: 0.3; cursor: not-allowed; filter: grayscale(0.75); }
.bx-duel__act:not(:disabled):hover { background: rgba(255, 200, 100, 0.2); }
.bx-duel__act:not(:disabled):active { transform: translateY(1px); }
/* Colour is the hero, matching the faction rule the sprites follow -
   you learn which button is whose from the yard, not from this frame.
   Set as `color` so the accent stripe (currentColor, above) and the
   border are one decision rather than two that can drift apart. */
.bx-duel__act--moto  { color: #ff78d2; border-color: rgba(255, 120, 210, 0.5); }
.bx-duel__act--truck { color: #96bed7; border-color: rgba(150, 190, 215, 0.5); }
.bx-duel__act--mason { color: #ffc369; border-color: rgba(255, 195, 105, 0.5); }
/* The pressable state belongs to the card, not to the palette: a hover
   that recolours the stripe would say the hero changed. */
.bx-duel__act:not(:disabled):hover {
  border-color: rgba(255, 230, 170, 0.9);
  background: linear-gradient(180deg, rgba(52, 34, 14, 0.97), rgba(28, 18, 7, 0.97));
}
.bx-duel__act:not(:disabled):focus-visible {
  outline: 2px solid rgba(255, 235, 190, 0.9);
  outline-offset: 2px;
}
/* Three sizes, three weights, three opacities — whose it is, what it
   does, and why you would. A player reads the middle line under
   pressure and the other two once, at leisure, so only the middle line
   is allowed to be loud. */
.bx-duel__act-who {
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  font-size: var(--font-2xs);
  letter-spacing: 0.18em;
  line-height: 1.15;
  opacity: 0.7;
  color: currentColor;
}
.bx-duel__act-label {
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-sm);
  letter-spacing: 0.08em;
  line-height: 1.15;
  color: #fff2d4;
  /* Never wraps to a second line: a card that grows sets the height of
     the row and the grid stops being a grid. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bx-duel__act-desc {
  font-family: "Rajdhani", sans-serif;
  font-size: var(--font-2xs);
  line-height: 1.2;
  color: rgba(255, 235, 205, 0.6);
  /* One line, clipped. The description is flavour and a phone is
     narrow; two lines of it would push the label off the card. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bx-duel__act-cd {
  position: absolute;
  top: 4px;
  right: 6px;
  padding: 0 6px;
  background: rgba(0, 0, 0, 0.75);
  border-radius: 999px;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-2xs);
  color: #ffc9a0;
}

@media (prefers-reduced-motion: reduce) {
  .bx-duel { animation: none; }
  .bx-duel__tell, .bx-duel__clock.is-urgent { animation: none; }
  .bx-duel__clock.is-urgent { transform: none; }
}

/* ===================================================================
   RUN SUMMARY - platform/ui/summary.js

   A reading screen, so it is built like one: a single column, a clear
   hierarchy, and nothing competing with the numbers. It scrolls rather
   than shrinking - a summary that fits every phone by making the text
   smaller is a summary nobody reads on a phone.

   Rows are label-left / value-right with a dotted leader between them,
   because that is how a receipt is read and this is a receipt.
   =================================================================== */
.gsum {
  position: absolute;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  /* SIZE, not inline-size. The panel below is capped in `cqb`, and a
     container that only contains the inline axis cannot answer a
     block-axis query — so that cap was resolved somewhere else
     entirely (the viewport, in the browsers that fall back rather than
     walk further up). On a stage that is short inside a tall window —
     which is every Discord panel — 92cqb then meant 92% of the WINDOW,
     and the summary hung off the bottom of the game with the CONTINUE
     button below the fold. Sizing this container fixes the cap at the
     overlay's own height, which is the stage's height. */
  container-type: size;
  animation: gsumIn 260ms ease-out both;
}
@keyframes gsumIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.gsum__scrim {
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 40%,
              rgba(18, 10, 4, 0.9) 0%, rgba(8, 4, 2, 0.97) 75%);
  backdrop-filter: blur(3px);
}
.gsum__panel {
  position: relative;
  /* BORDER-BOX, and this was the whole bug the second time round.
     There is no global box-sizing reset in this stylesheet, so a
     560px panel was 560 of CONTENT plus 28px of padding a side plus a
     2px border — 620px laid out inside a phone about 556px wide, with
     the overflow landing exactly on the right-hand column where the
     numbers are. The height fix earlier was real and fixed a different
     axis; this is the one that was cutting "1,489" in half. */
  box-sizing: border-box;
  /* Three caps, because they answer three different questions and any
     one of them can be the binding one: the container's width, the
     VIEWPORT's width (for a stage that is somehow wider than the glass
     it is being drawn on), and a readable maximum on a desktop. */
  width: min(96cqi, 94vw, 560px);
  max-width: 100%;
  /* Belt and braces: the percentage resolves against the grid area —
     the overlay box — with no container-query machinery involved at
     all, so the panel is bounded even if the unit above is ever
     mis-resolved again. */
  max-height: min(92cqb, 92%);
  /* The PANEL no longer scrolls; its body does (below). Scrolling the
     whole panel put the title and the CONTINUE button on the same
     scroll track as the content, so a long summary — which is what a
     full resource haul now makes — could park the only way out of the
     screen below the fold. Title and button are furniture; they stay
     where they are and the middle moves. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: clamp(5px, 1.6cqb, 12px);
  padding: clamp(12px, 3.4cqb, 26px) clamp(12px, 4cqi, 28px);
  background: linear-gradient(180deg, rgba(38, 24, 12, 0.97), rgba(22, 13, 6, 0.98));
  border: 2px solid rgba(255, 200, 110, 0.45);
  border-radius: 14px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(255, 160, 60, 0.06);
  font-family: "Rajdhani", system-ui, sans-serif;
}
/* ===================================================================
   THE PARTY LAYER — confetti, streamers, coins and a crest, for a run
   that was actually won.

   Sits BETWEEN the scrim and the panel: the pieces fall behind the
   results rather than over them, because a number a player cannot read
   is not a celebration, it is a nuisance. Pointer-events off
   throughout — nothing here is ever the thing a tap lands on.
   =================================================================== */
.gsum__party {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.gsum__panel { z-index: 1; }
/* The near layer. Over the panel and still untouchable — a tap goes to
   the button underneath, always. */
.gsum__party--front {
  z-index: 2;
  overflow: visible;   /* paper may fall past the edges of the stage */
}

/* A slow sweep of gold light behind everything, so the confetti is
   falling through something rather than over a flat brown. */
.gsum__rays {
  position: absolute;
  inset: -30%;
  background: conic-gradient(from 0deg,
    rgba(246, 201, 91, 0.16) 0deg 12deg, transparent 12deg 30deg,
    rgba(246, 201, 91, 0.12) 30deg 40deg, transparent 40deg 62deg,
    rgba(255, 220, 130, 0.14) 62deg 74deg, transparent 74deg 100deg,
    rgba(246, 201, 91, 0.16) 100deg 112deg, transparent 112deg 140deg,
    rgba(255, 220, 130, 0.10) 140deg 152deg, transparent 152deg 180deg,
    rgba(246, 201, 91, 0.16) 180deg 192deg, transparent 192deg 220deg,
    rgba(255, 220, 130, 0.12) 220deg 232deg, transparent 232deg 260deg,
    rgba(246, 201, 91, 0.14) 260deg 272deg, transparent 272deg 300deg,
    rgba(255, 220, 130, 0.16) 300deg 312deg, transparent 312deg 360deg);
  animation: gsumRays 26s linear infinite;
}
@keyframes gsumRays { to { transform: rotate(360deg); } }

/* Streamers: paper spirals taped to the four corners. Drawn as
   repeating diagonal gradients on a rotated bar, which is enough at
   this size and costs one element each. */
.gsum__streamer {
  position: absolute;
  width: clamp(80px, 26cqi, 190px);
  height: clamp(10px, 3cqi, 18px);
  background: repeating-linear-gradient(115deg,
    #f6c95b 0 10px, #fff3cf 10px 20px, #e08a3c 20px 30px, #9fe6a8 30px 40px);
  opacity: 0.75;
  border-radius: 999px;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4));
  transform-origin: 0 50%;
  animation: gsumStreamer 5.5s ease-in-out infinite alternate;
}
.gsum__streamer--tl { top: 6%;    left: -3%;  transform: rotate(24deg); }
.gsum__streamer--tr { top: 6%;    right: -3%; transform: rotate(156deg); transform-origin: 100% 50%; }
.gsum__streamer--bl { bottom: 8%; left: -3%;  transform: rotate(-24deg); }
.gsum__streamer--br { bottom: 8%; right: -3%; transform: rotate(-156deg); transform-origin: 100% 50%; }
@keyframes gsumStreamer {
  from { transform: rotate(var(--a, 24deg)) scaleX(0.94); }
  to   { transform: rotate(var(--a, 24deg)) scaleX(1.04); }
}

/* Confetti. Two motions at once — a fall and a tumble — because paper
   that falls straight down reads as rain. The horizontal drift and the
   spin are per-piece custom properties set inline. */
.gsum__confetti {
  position: absolute;
  top: -8%;
  display: block;
  opacity: 0;
  animation-name: gsumConfetti;
  animation-timing-function: cubic-bezier(0.35, 0.1, 0.55, 1);
  animation-iteration-count: infinite;
}
@keyframes gsumConfetti {
  0%   { opacity: 0; transform: translate3d(0, 0, 0) rotate(0deg); }
  8%   { opacity: 1; }
  85%  { opacity: 1; }
  100% { opacity: 0;
         transform: translate3d(var(--drift, 0), 115cqb, 0) rotate(var(--spin, 360deg)); }
}

/* Gold, falling. Slower than the paper and set spinning on its own
   axis — heavier things fall differently, and the eye knows it. */
.gsum__coin {
  position: absolute;
  top: -10%;
  font-size: clamp(14px, 4cqi, 24px);
  line-height: 1;
  opacity: 0;
  filter: drop-shadow(0 0 6px rgba(255, 200, 90, 0.55));
  animation-name: gsumCoinFall;
  animation-timing-function: cubic-bezier(0.4, 0.05, 0.6, 1);
  animation-iteration-count: infinite;
}
@keyframes gsumCoinFall {
  0%   { opacity: 0; transform: translate3d(0, 0, 0) rotateY(0deg) scale(0.85); }
  10%  { opacity: 1; }
  80%  { opacity: 1; }
  100% { opacity: 0; transform: translate3d(0, 118cqb, 0) rotateY(1440deg) scale(1); }
}

/* The crest, stamped over the panel's top edge. Lands late and hard —
   a slam rather than a fade, which is the difference between an award
   and a notification. */
.gsum__crest {
  position: absolute;
  top: calc(-1 * clamp(18px, 6cqi, 34px));
  left: 50%;
  width:  clamp(38px, 13cqi, 66px);
  height: clamp(43px, 15cqi, 74px);
  transform: translateX(-50%);
  filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.6))
          drop-shadow(0 0 16px rgba(255, 190, 80, 0.45));
  animation: gsumCrestStamp 520ms cubic-bezier(0.2, 1.5, 0.4, 1) 120ms both;
}
@keyframes gsumCrestStamp {
  from { opacity: 0; transform: translateX(-50%) scale(2.1) rotate(-14deg); }
  to   { opacity: 1; transform: translateX(-50%) scale(1)   rotate(0deg); }
}

/* The emoji garland, under the title. Deliberately the one place with
   emoji: they carry the party in a way the palette cannot, and a line
   of them reads as a banner rather than as decoration scattered
   through the numbers. */
.gsum__garland {
  text-align: center;
  font-size: clamp(15px, 5cqi, 26px);
  letter-spacing: 0.24em;
  line-height: 1;
  animation: gsumGarland 2.4s ease-in-out infinite;
}
@keyframes gsumGarland {
  0%, 100% { transform: translateY(0)     scale(1);    filter: none; }
  50%      { transform: translateY(-2px)  scale(1.04); filter: brightness(1.15); }
}

/* The panel itself is louder when it is a win: a warmer border, a gold
   bloom behind it, and a title that arrives with a pop. */
.gsum--party .gsum__panel {
  border-color: rgba(255, 216, 130, 0.75);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8),
              inset 0 0 40px rgba(255, 160, 60, 0.1),
              0 0 60px rgba(246, 201, 91, 0.28);
  animation: gsumPanelPop 460ms cubic-bezier(0.2, 1.35, 0.4, 1) both;
}
@keyframes gsumPanelPop {
  from { opacity: 0; transform: translateY(14px) scale(0.96); }
  to   { opacity: 1; transform: none; }
}
.gsum--party .gsum__title {
  animation: gsumTitleGlow 2.6s ease-in-out infinite;
}
@keyframes gsumTitleGlow {
  0%, 100% { text-shadow: 0 0 20px rgba(255, 180, 80, 0.35), 0 2px 4px rgba(0, 0, 0, 0.9); }
  50%      { text-shadow: 0 0 34px rgba(255, 210, 120, 0.75), 0 2px 4px rgba(0, 0, 0, 0.9); }
}
.gsum--party .gsum__eyebrow {
  animation: gsumGarland 3.2s ease-in-out infinite;
}

/* MOTION IS THE PARTY, so reduced motion keeps the party and drops the
   motion: the confetti and coins are still there, held mid-fall, and
   nothing loops. Removing them entirely would tell the player who
   needs this setting that they won something quieter. */
@media (prefers-reduced-motion: reduce) {
  .gsum__rays,
  .gsum__streamer,
  .gsum__garland,
  .gsum--party .gsum__panel,
  .gsum--party .gsum__title,
  .gsum--party .gsum__eyebrow { animation: none; }
  .gsum__crest { animation: none; opacity: 1; }
  .gsum__confetti,
  .gsum__coin {
    animation: none;
    opacity: 0.9;
    transform: translate3d(var(--drift, 0), 42cqb, 0) rotate(var(--spin, 0deg));
  }
}

.gsum__eyebrow {
  text-align: center;
  font-family: "Rajdhani", sans-serif;
  font-weight: 700;
  font-size: var(--font-2xs);
  letter-spacing: 0.28em;
  color: rgba(160, 220, 170, 0.95);
}
.gsum__title {
  margin: 0;
  text-align: center;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-lg);
  letter-spacing: 0.1em;
  color: #ffe6b4;
  text-shadow: 0 0 20px rgba(255, 180, 80, 0.35), 0 2px 4px rgba(0, 0, 0, 0.9);
}
.gsum__body {
  display: flex;
  flex-direction: column;
  gap: clamp(8px, 2.2cqb, 18px);
  margin-top: clamp(4px, 1.2cqb, 10px);
  /* min-height: 0 is what actually lets a flex child shrink below its
     content height. Without it `overflow-y: auto` here is decoration —
     the body would size to its content and push the button out of the
     panel exactly as before. */
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.gsum__group { display: flex; flex-direction: column; gap: clamp(2px, 0.8cqb, 6px); }
.gsum__group-title {
  margin: 0 0 clamp(2px, 0.8cqb, 6px);
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-2xs);
  letter-spacing: 0.24em;
  color: rgba(255, 196, 110, 0.9);
  border-bottom: 1px solid rgba(255, 190, 110, 0.22);
  padding-bottom: clamp(2px, 0.7cqb, 5px);
}

/* --- Rows ---------------------------------------------------------
   The leader is a repeating gradient rather than a border so it sits
   on the text baseline and stretches to whatever is left between the
   label and the value. */
.gsum__row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: 6px;
  font-size: var(--font-xs);
  color: rgba(255, 238, 210, 0.92);
}
.gsum__row::after {
  content: "";
  grid-column: 2;
  height: 1px;
  align-self: center;
  background: repeating-linear-gradient(90deg,
              rgba(255, 210, 150, 0.35) 0 2px, transparent 2px 6px);
}
/* Neither column may push the row wider than the panel: the label
   wraps, the value never does, and the dotted leader in between is the
   part that gives. Without the min-width:0 a long label in a language
   with longer words (German, Hungarian) would shove the number off the
   right-hand edge — the same clipping as the box-sizing bug, arriving
   by a different route. */
.gsum__row-label {
  grid-column: 1;
  min-width: 0;
  overflow-wrap: anywhere;
}
.gsum__row-value {
  grid-column: 3;
  white-space: nowrap;
  font-family: "Cinzel", serif;
  font-weight: 900;
  letter-spacing: 0.04em;
  color: #fff2d4;
}
/* The hint drops to its own full-width line so a long one cannot
   squeeze the value into a wrap. */
.gsum__row-hint {
  grid-column: 1 / -1;
  font-size: var(--font-2xs);
  color: rgba(255, 214, 150, 0.7);
}
.gsum__row--good .gsum__row-value { color: #9fe6a8; }
.gsum__row--warn .gsum__row-value { color: #ffd479; }
.gsum__row--bad  .gsum__row-value { color: #ff9a8a; }

/* --- Bars ---------------------------------------------------------
   Used for two different things that happen to share a shape: where
   you stand against other players, and how close an unfinished task
   is. Both are a proportion, so both are drawn as one. */
.gsum__bar { display: flex; flex-direction: column; gap: 2px; margin-top: clamp(3px, 1cqb, 8px); }
.gsum__bar-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  font-size: var(--font-xs);
  color: rgba(255, 238, 210, 0.92);
}
.gsum__bar-value {
  font-family: "Cinzel", serif;
  font-weight: 900;
  color: #ffe6b4;
}
.gsum__bar-track {
  height: clamp(6px, 1.8cqb, 11px);
  background: rgba(0, 0, 0, 0.62);
  border: 1px solid rgba(255, 200, 130, 0.3);
  border-radius: 999px;
  overflow: hidden;
}
.gsum__bar-fill {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, #ffe6a0, #d8a040);
  /* Grows in, once, on arrival. This is the one animated thing on the
     screen: a bar that fills is read as an achievement, and a bar that
     is simply there is read as furniture. */
  animation: gsumFill 620ms cubic-bezier(.2,.8,.3,1) both;
  transform-origin: left center;
}
@keyframes gsumFill {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
/* Tasks are a different colour from standing, so "how I did against
   people" and "what I nearly finished" do not read as one list. */
.gsum__bar--task .gsum__bar-fill { background: linear-gradient(90deg, #9fd6a8, #4f8f62); }
.gsum__bar-caption {
  font-size: var(--font-2xs);
  color: rgba(255, 214, 150, 0.66);
}

.gsum__note {
  margin: clamp(4px, 1.4cqb, 10px) 0 0;
  text-align: center;
  font-size: var(--font-2xs);
  letter-spacing: 0.08em;
  color: rgba(255, 200, 140, 0.9);
}

.gsum__confirm {
  margin-top: clamp(6px, 2cqb, 14px);
  align-self: center;
  padding: clamp(7px, 2.2cqb, 13px) clamp(20px, 6cqi, 40px);
  border-radius: 999px;
  border: 2px solid rgba(255, 220, 130, 0.9);
  background: linear-gradient(180deg, #ffdc82, #d8a040);
  color: #2a1806;
  font-family: "Cinzel", serif;
  font-weight: 900;
  font-size: var(--font-sm);
  letter-spacing: 0.16em;
  cursor: pointer;
}
.gsum__confirm:disabled { opacity: 0.4; cursor: default; }
.gsum__confirm:not(:disabled):hover { filter: brightness(1.08); }
.gsum__confirm:not(:disabled):active { transform: translateY(1px); }

@media (prefers-reduced-motion: reduce) {
  .gsum { animation: none; }
  .gsum__bar-fill { animation: none; }
}
