/* NEON RUSH — UI design system. Palette: magenta #ff2d95, cyan #00e5ff,
   amber #ffb300 on deep violet blacks.
   Type system: 'Orbitron' (packaged woff2, inlined in index.html) is the
   brand/display face — wordmark, HUD numerals, chips. The humanist system
   stack stays for small instructional copy only. */

/* ==================================================================== */
/* MOTION TOKENS (DESCRIPTIVE, measured from this file 2026-08-19 for   */
/* item 9, reports/item9-motion.md). This block DOCUMENTS the system    */
/* the tree actually uses; it declares no custom properties and changes */
/* no rule, because the release/1.0 pixel gate forbids visual drift.    */
/*                                                                      */
/* Duration bands (values cluster here; no tokens enforce them):        */
/*   tap/tracking  0.05-0.2s   modal values 0.15 / 0.12 / 0.1          */
/*   state change  0.22-0.55s  modal values 0.5 / 0.45 / 0.4           */
/*   emphasis      0.56-1.6s   pulse and charge loops                  */
/*   ambience      1.7-7.8s    embers, flicker, smoke (negative        */
/*                             delays desync loop phases for capture)  */
/*   JS choreography: score count-up 900ms, chips/tickWallet 700ms,    */
/*   chip-flight arc 640ms, chips stagger 405ms, layer swap 300/340ms. */
/*                                                                      */
/* Easing: house overshoot family cubic-bezier(0.2, Y, 0.4, 1) where Y  */
/* is the punch dial, graduated by event weight: entrances 1.2-1.4      */
/* (panelIn/sheetIn/pvIn), kicks and toasts 1.5-1.6, purchases and      */
/* reward pops 1.8-1.9 (cardPop/chipsBump), combo tiers 2.2. Decel      */
/* beziers of the (0.1-0.3, 0.6-0.9, x, 1) shape on exits; ease-in-out  */
/* on ambient loops only. linear appears ONLY on constant-rate loops,   */
/* on gauges tracking a live physics value (easing there would lie      */
/* about the data), and in the visibility-delay idiom. Never on         */
/* entrances or tap feedback.                                           */
/*                                                                      */
/* Type: three voices. Orbitron display (titles, buttons, hero          */
/* numerals), --ui-sans (labels, sub-copy), --num + tabular-nums on     */
/* every money/time numeral (grouped rule near the file foot; Orbitron  */
/* digit 7 is the reason). Sizes are optical per surface, 5.5-26px      */
/* fixed plus clamp() ramps, NOT a modular scale.                       */
/*                                                                      */
/* Spacing: 1px-granular optical placement with a soft 2px rhythm at    */
/* small sizes. This tree does not follow a 4/8 grid; snapping it now   */
/* would be exactly the visual drift the pixel gate blocks.             */
/*                                                                      */
/* Accessibility tier: body.reduced-motion (persisted setting, boot     */
/* default from the OS hint) stops every strobe/pulse/ember loop but    */
/* keeps one-shot fades and entrances. Render-side twin: uReduced.      */
/* ==================================================================== */

:root {
  --magenta: #ff2d95;
  --cyan: #00e5ff;
  --amber: #ffb300;
  --ink: #0a0614;
  --display: 'Orbitron', 'Avenir Next', 'Segoe UI', system-ui, sans-serif;
  /* SECONDARY UI FACE. The italic display face is reserved for screen
     titles, buttons and hero numerals; labels, rows, sub-copy and
     instructions read in this neutral sans (medium weight, normal style,
     letter-spacing <= 0.04em), one voice shouting, one voice speaking.
     Applied by the grouped rule at the end of this file. */
  --ui-sans: -apple-system, 'SF Pro Text', 'Segoe UI', Roboto, Helvetica,
    Arial, sans-serif;
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  /* HOW MUCH OF THE BOTTOM OF THE FRAME THE GARAGE SHEET OWNS, in px.
     ui.js re-stamps this on :root every time the sheet's geometry changes
     (open, tab change, peek collapse, preview bar, resize) and game.js reads
     it — or the identical ui.garageSheetHeight() — to frame the showroom
     camera in the FREE area above the sheet. 0 while the garage is closed. */
  --garage-sheet-h: 0px;
}

* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  height: 100%;
  overflow: hidden;
  background: var(--ink);
  font-family: 'Avenir Next', 'Segoe UI', system-ui, -apple-system, sans-serif;
  touch-action: none;
  overscroll-behavior: none;
  user-select: none;
  -webkit-user-select: none;
}

#game-canvas {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

#input-layer {
  position: fixed;
  inset: 0;
  z-index: 30;
}

/* layers ride ABOVE the input layer (z30): the layers themselves are
   pointer-events none so open road still steers/taps through them, while
   buttons/panels inside (pointer-events auto) actually receive touches */
.layer {
  position: fixed;
  inset: 0;
  z-index: 32;
  pointer-events: none;
  /* Screens used to cross-fade on opacity alone, which reads as a slideshow:
     the outgoing screen just dims in place. Adding a small scale and blur to
     the EXIT gives the change a direction -- the old screen recedes and
     defocuses while the incoming panel's existing panelIn animation springs
     forward over it. Kept deliberately small (1.5% and 5px): at racing-game
     pace anything bigger reads as sluggish rather than polished.
     Transform/filter are compositor-only, so this costs no layout. */
  transition:
    opacity 0.3s ease,
    transform 0.34s cubic-bezier(0.2, 0.9, 0.25, 1),
    filter 0.3s ease;
  transform-origin: 50% 45%;
  will-change: opacity, transform;
}

.hidden { opacity: 0 !important; visibility: hidden; }

/* scoped to .layer on purpose: `.hidden` is also used on chips, rows and
   buttons where display:none or a plain fade is what is wanted */
.layer.hidden {
  transform: scale(0.985);
  filter: blur(5px);
}

/* COMPOSITOR PARKING (ui.js showLayer/hideLayer). A hidden layer is invisible
   but not free: WebKit keeps its backdrop-filter elements and every infinite
   CSS animation inside it alive on the UI-process compositor at display
   rate, and with ten screens that was ~45 ms of every warm frame on an
   iPhone 16 Pro (device lab 2026-09-02). `parked` lands ~420 ms after the
   exit transition finished and removes the subtree from rendering
   altogether; the animations are paused as well for engines without
   content-visibility. Never combine with the blur/scale overrides: parked
   is always also hidden, and the class comes off BEFORE hidden does so the
   entrance still plays from the blurred state. */
.parked { content-visibility: hidden; }
/* .hidden is opacity 0 + visibility hidden, so nothing under it can show;
   the pause also covers chips like #drift-chip whose .drift-charge pulse
   kept the compositor ticking at display rate all run long. A paused
   animation resumes in phase when the class comes off, and one-shot
   entrance animations are started BY the class coming off, so nothing
   visible changes. Pseudo-elements are listed because `.parked *` does not
   reach #over-screen::after (wreckFlicker). */
.parked *, .parked::before, .parked::after,
.hidden *, .hidden::before, .hidden::after { animation-play-state: paused !important; }

/* Capture rig (?shot=, class set by the inline hook in index.html): land
   layers with no fade. The transition above is frame-driven, so on the
   headless SwiftShader renderer it takes seconds of wall clock, not 0.35s —
   long enough that the layout audit measures a fully-staged screen at
   opacity 0 and calls it INCONCLUSIVE. Players keep the fade; only the
   automated rig skips it, and only the TIMING changes, not the layout. */
:root.shot-mode .layer { transition: none !important; transform: none !important; filter: none !important; }

/* Same frame-starvation problem one level down. Every panel below enters on
   panelIn/sheetIn/pvIn (opacity 0 -> 1, no `forwards` fill), so while that
   entrance is still running its subtree is under opacity 0.05 and the audit
   discards it. With only the .layer fade killed above, the layer lands at
   once while its panel is still fading — the screen then audits PARTIALLY
   populated (settings 61 boxes -> 3, over-panel 16 -> 2, bonus 44 -> 5),
   which is worse than INCONCLUSIVE because a 3-box screen reports "clean"
   without admitting it never staged. These animations carry no fill, so
   `none` renders the element's base style, which IS the animation's end
   state — the panel simply starts landed. */
:root.shot-mode .over-panel,
:root.shot-mode #garage-panel,
:root.shot-mode #garage-preview,
:root.shot-mode #bonus-panel,
:root.shot-mode .pause-panel,
:root.shot-mode #ranks-panel,
:root.shot-mode #store-panel,
:root.shot-mode #settings-panel,
:root.shot-mode .confirm-panel,
:root.shot-mode #over-missions.open .om-body { animation: none !important; }

/* ------------------------------------------------------------- title */
#title-screen {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  padding: calc(12vh + var(--safe-top)) 24px calc(4vh + var(--safe-bottom));
  background: radial-gradient(ellipse at 50% 120%, rgba(255, 45, 149, 0.14), transparent 55%);
}

/* bottom scrim: keeps TAP TO DRIVE + instructions legible over the car's
   blown-out tail lights without dimming the whole scene */
#title-screen::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 44vh;
  z-index: -1;
  background: linear-gradient(to top,
    rgba(10, 6, 20, 0.85) 0%,
    rgba(10, 6, 20, 0.55) 42%,
    transparent 100%);
}

.title-wrap { position: relative; text-align: center; }

/* soft radial scrim behind the wordmark: separates the logo from the busy
   building signage crossing its edges without flattening the whole scene */
.title-wrap::before {
  content: '';
  position: absolute;
  inset: -9vh -24vw -6vh;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(ellipse at center,
    rgba(10, 6, 20, 0.58) 0%,
    rgba(10, 6, 20, 0.38) 48%,
    rgba(10, 6, 20, 0.0) 74%);
}

#logo {
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 0.95;
  font-family: var(--display);
  font-weight: 900;
  letter-spacing: 0.04em;
  transform: skewX(-8deg);
}

.logo-line1 {
  /* NIGHT is five characters where NEON was four, so this line is now the
     LONGER one -- sized down slightly so it still clears 360px without the
     tracking collapsing. */
  font-size: clamp(42px, 15vw, 92px);
  background: linear-gradient(180deg, #ffffff 28%, #c8f6ff 58%, #6fe6ff 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: #6fe6ff;
  filter:
    drop-shadow(0 0 8px rgba(0, 229, 255, 0.85))
    drop-shadow(0 0 30px rgba(0, 229, 255, 0.45));
  animation: hum 3.2s ease-in-out infinite;
}

.logo-line2 {
  /* MILE is four characters against NIGHT's five, so the hierarchy inverts:
     the shorter word takes the larger size and the lockup stays a wedge. */
  font-size: clamp(58px, 21vw, 128px);
  background: linear-gradient(180deg, #ffffff 24%, #ffcbe6 52%, #ff56ad 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: #ff56ad;
  filter:
    drop-shadow(0 0 8px rgba(255, 45, 149, 0.9))
    drop-shadow(0 0 34px rgba(255, 45, 149, 0.5));
  animation: hum 3.2s ease-in-out 0.4s infinite;
}

@keyframes hum {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.86; }
  92% { opacity: 0.95; }
}

.tagline {
  margin-top: 16px;
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(9px, 2.6vw, 13px);
  letter-spacing: 0.32em;
  text-indent: 0.32em;
  white-space: nowrap;
  color: rgba(220, 230, 255, 0.78);
}

@media (max-width: 420px) {
  .tagline { letter-spacing: 0.24em; text-indent: 0.24em; }
}

.title-bottom { text-align: center; }

/* demoted to a hint line — the CTA stack owns the launch action now */
#tap-start {
  margin-top: 16px;
  font-family: var(--display);
  font-size: clamp(9px, 2.6vw, 12px);
  font-weight: 700;
  letter-spacing: 0.24em;
  text-indent: 0.24em;
  color: rgba(255, 216, 138, 0.85);
  text-shadow: 0 0 10px rgba(255, 179, 0, 0.5), 0 1px 6px rgba(0, 0, 0, 0.9);
  animation: pulse 1.4s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

#best-title {
  margin-top: 14px;
  font-size: clamp(9px, 2.6vw, 12px);
  letter-spacing: 0.22em;
  white-space: nowrap;
  color: rgba(205, 218, 245, 0.78);
  font-weight: 600;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.9);
}

/* --------------------------------------------------------------- HUD */
/* top scrim: a quiet legibility gradient pinned behind the score/actions
   row, so world signage (the red DRIVE billboard) can never collide with
   the score read. Painted before #hud's children, so all instruments sit
   on top; fades out well above mid-frame to keep the road untouched. */
#hud::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: calc(118px + var(--safe-top));
  background: linear-gradient(to bottom,
    rgba(5, 6, 16, 0.58),
    rgba(5, 6, 16, 0.30) 52%,
    rgba(5, 6, 16, 0));
  pointer-events: none;
}

#hud-top {
  position: absolute;
  top: calc(12px + var(--safe-top));
  left: 0; right: 0;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  padding: 0 18px;
}

/* score is the dominant instrument: big, white, cyan halo */
#score {
  font-family: var(--display);
  font-size: clamp(28px, 8.4vw, 42px);
  font-weight: 800;
  font-style: italic;
  color: #fff;
  text-shadow: 0 0 12px rgba(0, 229, 255, 0.65), 0 2px 12px rgba(0, 0, 0, 0.8);
  letter-spacing: 0.02em;
}

/* combo chip: skewed plate with a live decay bar along its base.
   TIER ESCALATION (ui.js sets .t2/.t3/.t4): ×2-3 amber → ×4-5 hot orange →
   ×6+ hot magenta, with the glow, size and punch-in all climbing — a ×6
   badge must read hotter than a ×2 at a glance, like Asphalt's chain tiers. */
#combo {
  --cc: var(--amber);
  --cc-rgb: 255, 179, 0;
  position: relative;
  display: inline-block;
  margin-top: 6px;
  padding: 4px 10px 7px;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(13px, 3.8vw, 17px);
  color: var(--cc);
  text-shadow: 0 0 10px rgba(var(--cc-rgb), 0.75);
  background: rgba(26, 15, 4, 0.55);
  border: 1px solid rgba(var(--cc-rgb), 0.55);
  border-radius: 6px;
  transform: skewX(-8deg);
  box-shadow: 0 0 14px rgba(var(--cc-rgb), 0.22), inset 0 0 10px rgba(var(--cc-rgb), 0.08);
  overflow: hidden;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

#combo.t3 {
  --cc: #ff8a45;
  --cc-rgb: 255, 110, 45;
  background: rgba(32, 11, 4, 0.58);
  font-size: clamp(14px, 4.1vw, 18px);
  box-shadow: 0 0 18px rgba(var(--cc-rgb), 0.35), inset 0 0 10px rgba(var(--cc-rgb), 0.12);
}

#combo.t4 {
  --cc: #ff5fb2;
  --cc-rgb: 255, 45, 149;
  background: rgba(36, 6, 23, 0.62);
  font-size: clamp(15px, 4.4vw, 20px);
  box-shadow: 0 0 24px rgba(var(--cc-rgb), 0.5), 0 0 46px rgba(var(--cc-rgb), 0.22),
    inset 0 0 12px rgba(var(--cc-rgb), 0.18);
  animation: comboHot 0.7s ease-in-out infinite alternate;
}

@keyframes comboHot {
  from { box-shadow: 0 0 18px rgba(255, 45, 149, 0.4), 0 0 36px rgba(255, 45, 149, 0.16), inset 0 0 10px rgba(255, 45, 149, 0.14); }
  to   { box-shadow: 0 0 30px rgba(255, 45, 149, 0.65), 0 0 58px rgba(255, 45, 149, 0.3), inset 0 0 15px rgba(255, 45, 149, 0.24); }
}

#combo-decay {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--cc), rgba(255, 255, 255, 0.92));
  box-shadow: 0 0 8px rgba(var(--cc-rgb), 0.9);
  transform-origin: left;
  transform: scaleX(1);
}

#combo.pop { animation: comboPop 0.35s cubic-bezier(0.2, 2.2, 0.4, 1); }
#combo.t3.pop { animation: comboPop3 0.4s cubic-bezier(0.2, 2.2, 0.4, 1); }
#combo.t4.pop { animation: comboPop4 0.45s cubic-bezier(0.2, 2.2, 0.4, 1), comboHot 0.7s 0.45s ease-in-out infinite alternate; }

@keyframes comboPop {
  0% { transform: skewX(-8deg) scale(1.6) rotate(-4deg); }
  100% { transform: skewX(-8deg) scale(1) rotate(0); }
}

@keyframes comboPop3 {
  0% { transform: skewX(-8deg) scale(1.9) rotate(-5deg); }
  100% { transform: skewX(-8deg) scale(1) rotate(0); }
}

@keyframes comboPop4 {
  0% { transform: skewX(-8deg) scale(2.2) rotate(-7deg); }
  55% { transform: skewX(-8deg) scale(0.94) rotate(1.5deg); }
  100% { transform: skewX(-8deg) scale(1) rotate(0); }
}

.hud-actions {
  display: flex;
  gap: 8px;
}

#mute-btn,
#pause-btn {
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 8, 24, 0.55);
  border: 1px solid rgba(120, 140, 200, 0.25);
  border-radius: 12px;
  width: 42px; height: 42px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: rgba(215, 228, 255, 0.92);
  cursor: pointer;
}

#mute-btn svg { width: 20px; height: 20px; display: block; }

#pause-btn svg { width: 18px; height: 18px; display: block; }

#pause-btn:active { transform: scale(0.92); }

#pause-btn.paused {
  color: #9beeff;
  border-color: rgba(0, 229, 255, 0.55);
  box-shadow: 0 0 12px rgba(0, 229, 255, 0.3);
}
#mute-btn .spk-wave { transition: opacity 0.15s; }
#mute-btn .spk-slash { opacity: 0; stroke: var(--magenta); transition: opacity 0.15s; }
#mute-btn.muted .spk-wave { opacity: 0; }
#mute-btn.muted .spk-slash { opacity: 1; }
#mute-btn.muted { color: rgba(198, 206, 232, 0.78); }

/* drift chip: magenta plate above the speedo — lit for the whole slide.
   It shows the DRIFT word + live accrued drift points (+240 counting up);
   the amber combo chip (top-left) owns the ×N multiplier, so one value is
   never stated twice in two competing systems. */
#drift-chip {
  position: absolute;
  left: 20px;
  bottom: calc(92px + var(--safe-bottom));
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: clamp(120px, 32vw, 160px);
  padding: 7px 13px 8px;
  border-radius: 6px;
  transform: skewX(-8deg);
  font-family: var(--display);
  background: linear-gradient(135deg, rgba(255, 45, 149, 0.30), rgba(110, 10, 66, 0.42));
  border: 1px solid rgba(255, 45, 149, 0.65);
  box-shadow: 0 0 18px rgba(255, 45, 149, 0.35), inset 0 0 12px rgba(255, 45, 149, 0.15);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

.drift-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

/* drift charge bar: fills across the slide as points bank (ui.js scales it
   off live driftScore) — the meter is the mechanic's heartbeat, not the text */
#drift-meter {
  height: 3px;
  border-radius: 2px;
  background: rgba(255, 234, 246, 0.16);
  overflow: hidden;
}

#drift-meter-fill {
  display: block;
  height: 100%;
  border-radius: 2px;
  background: linear-gradient(90deg, #ff2d95, #ffd9ec);
  box-shadow: 0 0 8px rgba(255, 45, 149, 0.95);
  transform-origin: left;
  transform: scaleX(0);
  transition: transform 0.15s linear;
}

/* the honest drift->nitro link (physics refills nitroFuel while sliding),
   stated in the nitro cluster's cyan so the two systems visibly hand off */
.drift-charge {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.18em;
  color: #9beeff;
  text-shadow: 0 0 8px rgba(0, 229, 255, 0.65);
  animation: pulse 0.9s ease-in-out infinite;
}

.drift-charge svg {
  width: 8px;
  height: 8px;
  flex: none;
  fill: var(--cyan);
  filter: drop-shadow(0 0 4px rgba(0, 229, 255, 0.85));
}

#drift-chip:not(.hidden) { animation: driftHum 0.7s ease-in-out infinite alternate; }

@keyframes driftHum {
  from { box-shadow: 0 0 12px rgba(255, 45, 149, 0.3), inset 0 0 10px rgba(255, 45, 149, 0.12); }
  to   { box-shadow: 0 0 26px rgba(255, 45, 149, 0.6), inset 0 0 16px rgba(255, 45, 149, 0.22); }
}

.drift-word {
  font-size: clamp(14px, 4vw, 18px);
  font-weight: 800;
  letter-spacing: 0.2em;
  color: #fff;
  text-shadow: 0 0 10px rgba(255, 45, 149, 0.95), 0 0 26px rgba(255, 45, 149, 0.6);
}

/* live drift points: stays in the chip's magenta family (white-pink) so the
   chip reads as ONE system — no amber echo of the combo chip up top */
#drift-pts {
  min-width: 3.2ch;
  font-size: clamp(12px, 3.4vw, 15px);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: #ffeaf6;
  text-shadow: 0 0 9px rgba(255, 45, 149, 0.95), 0 0 20px rgba(255, 45, 149, 0.55);
}

#drift-pts:empty { display: none; }

#hud-bottom {
  position: absolute;
  bottom: calc(16px + var(--safe-bottom));
  left: 0; right: 0;
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  padding: 0 20px;
}

/* speedo: deliberately secondary to the score — smaller, neutral white,
   amber gauge accent instead of the score's cyan halo */
#speedo {
  font-family: var(--display);
  font-style: italic;
  font-weight: 700;
  color: rgba(240, 246, 255, 0.92);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
  display: flex;
  align-items: baseline;
  gap: 6px;
  transition: text-shadow 0.25s;
}

#speedo::before {
  content: '';
  align-self: flex-end;
  width: 6px;
  height: clamp(18px, 5vw, 25px);
  margin-right: 6px;
  margin-bottom: 5px;
  border-radius: 2px;
  transform: skewX(-12deg);
  background: linear-gradient(to top, var(--amber), rgba(255, 179, 0, 0.12));
  box-shadow: 0 0 10px rgba(255, 179, 0, 0.55);
}

/* BOOSTING: the whole readout flips to nitro cyan and surges — number goes
   ice-white/cyan, the amber gauge accent and KM/H unit flip cyan with it, and
   the block breathes on a fast pulse so the payoff registers at a glance */
#speedo.nitro {
  color: #eafcff;
  text-shadow: 0 0 12px rgba(90, 215, 255, 1), 0 0 38px rgba(0, 229, 255, 0.85);
  transform-origin: left bottom;
  animation: speedoSurge 0.45s ease-in-out infinite alternate;
}

@keyframes speedoSurge {
  from { transform: scale(1); }
  to   { transform: scale(1.055); }
}

#speedo.nitro::before {
  background: linear-gradient(to top, var(--cyan), rgba(0, 229, 255, 0.14));
  box-shadow: 0 0 12px rgba(0, 229, 255, 0.9), 0 0 26px rgba(0, 229, 255, 0.45);
}

#speedo.nitro .unit {
  color: #9beeff;
  text-shadow: 0 0 10px rgba(0, 229, 255, 0.65), 0 1px 4px rgba(0, 0, 0, 0.9);
}

#speed-val { font-size: clamp(23px, 6.8vw, 35px); }

#speedo .unit {
  font-size: clamp(9px, 2.6vw, 12px);
  letter-spacing: 0.2em;
  font-weight: 600;
  color: rgba(255, 199, 87, 0.85);
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.4), 0 1px 4px rgba(0, 0, 0, 0.9);
  transition: color 0.2s;
}

/* nitro cluster: bolt icon + segmented skewed track, label inside */
#nitro-cluster {
  position: relative;
  display: flex;
  align-items: center;
  gap: 9px;
  margin-bottom: 7px;
}

/* boost flare: cyan bloom pooling behind the whole cluster while the trough
   drains — the bar doesn't just deplete, it visibly VENTS light */
#nitro-flare {
  position: absolute;
  inset: -36px -42px;
  z-index: -1;
  border-radius: 50%;
  background: radial-gradient(ellipse at 58% 50%,
    rgba(165, 238, 255, 0.85), rgba(0, 180, 255, 0.34) 48%, transparent 75%);
  filter: blur(8px);
  opacity: 0;
  transition: opacity 0.2s;
}

#nitro-flare.on {
  animation: nitroFlarePulse 0.4s ease-in-out infinite alternate;
}

@keyframes nitroFlarePulse {
  from { opacity: 0.72; transform: scale(1); }
  to   { opacity: 1; transform: scale(1.16); }
}

#nitro-bolt {
  width: 15px;
  height: 15px;
  flex: none;
  fill: var(--cyan);
  filter: drop-shadow(0 0 6px rgba(0, 229, 255, 0.85));
}

#nitro-bar {
  position: relative;
  width: clamp(118px, 34vw, 172px);
  height: 19px;
  border-radius: 4px;
  border: 1px solid rgba(80, 200, 255, 0.55);
  background: rgba(8, 12, 28, 0.66);
  overflow: hidden;
  transform: skewX(-12deg);
  box-shadow: 0 0 12px rgba(0, 180, 255, 0.18), inset 0 0 8px rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  transition: border-color 0.2s, box-shadow 0.2s;
}

/* fill level rides --nitro (0..1, set by ui.js on #nitro-bar). Inset-based
   sizing — not scaleX — so the boost chevrons and the leading-edge sliver
   render at true scale at any fill level */
#nitro-fill {
  position: absolute;
  top: 2px; bottom: 2px; left: 2px;
  right: calc(2px + (1 - var(--nitro, 0.5)) * (100% - 4px));
  border-radius: 2px;
  background: linear-gradient(90deg, #0af, #6ef);
  transition: right 0.12s linear;
  box-shadow: 0 0 10px rgba(80, 200, 255, 0.9);
}

/* 5-segment ticks over the fill — masked to short top/bottom notches so the
   center band stays clean for the NITRO label instead of ticks slicing
   through its glyphs */
.nitro-ticks {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(90deg,
    transparent 0 calc(20% - 1px), rgba(5, 9, 22, 0.9) calc(20% - 1px) calc(20% + 1px),
    transparent calc(20% + 1px) calc(40% - 1px), rgba(5, 9, 22, 0.9) calc(40% - 1px) calc(40% + 1px),
    transparent calc(40% + 1px) calc(60% - 1px), rgba(5, 9, 22, 0.9) calc(60% - 1px) calc(60% + 1px),
    transparent calc(60% + 1px) calc(80% - 1px), rgba(5, 9, 22, 0.9) calc(80% - 1px) calc(80% + 1px),
    transparent calc(80% + 1px));
  -webkit-mask-image: linear-gradient(180deg,
    #000 0 4px, transparent 4px calc(100% - 4px), #000 calc(100% - 4px) 100%);
  mask-image: linear-gradient(180deg,
    #000 0 4px, transparent 4px calc(100% - 4px), #000 calc(100% - 4px) 100%);
}

/* label rides an opaque dark halo so it reads across both the lit fill edge
   and the empty track at glance distance */
.nitro-label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--display);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  color: rgba(240, 250, 255, 0.98);
  text-shadow:
    0 0 2px rgba(5, 9, 22, 1),
    0 0 3px rgba(5, 9, 22, 1),
    0 0 5px rgba(5, 9, 22, 0.95),
    0 1px 6px rgba(5, 9, 22, 0.9);
}

/* BOOSTING: the whole cluster goes hot — near-white border, big cyan bloom,
   pulsing but with a bright floor so any captured frame reads as "draining
   under boost", never as the idle bar */
#nitro-bar.active {
  border-color: rgba(205, 245, 255, 1);
  animation: nitroBarActive 0.5s ease-in-out infinite alternate;
}

@keyframes nitroBarActive {
  from { box-shadow: 0 0 20px rgba(90, 205, 255, 0.85), 0 0 46px rgba(0, 180, 255, 0.4), inset 0 0 10px rgba(100, 215, 255, 0.35); }
  to   { box-shadow: 0 0 32px rgba(150, 230, 255, 1), 0 0 72px rgba(0, 195, 255, 0.6), inset 0 0 15px rgba(130, 225, 255, 0.5); }
}

#nitro-bar.charged { animation: nitroCharged 1.5s ease-in-out infinite; }

@keyframes nitroCharged {
  0%, 100% { box-shadow: 0 0 10px rgba(0, 180, 255, 0.25), inset 0 0 8px rgba(0, 0, 0, 0.6); }
  50% { box-shadow: 0 0 22px rgba(80, 200, 255, 0.75), inset 0 0 12px rgba(80, 200, 255, 0.2); }
}

/* draining fill: hotter white-cyan gradient + a white leading-edge sliver at
   the drain point; flash floor is >1 so the boost state is unmistakable in
   every frame, not a coin flip on animation phase */
#nitro-fill.active {
  background: linear-gradient(90deg, #2bc8ff, #d9faff);
  box-shadow: 0 0 14px rgba(150, 235, 255, 1), 0 0 30px rgba(60, 195, 255, 0.7);
  animation: nitroFlash 0.4s linear infinite;
}

/* boost chevrons: angled light bands scrolling toward the drain edge — the
   bar is visibly VENTING, not just sitting lit */
#nitro-fill.active::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 2px;
  background: repeating-linear-gradient(115deg,
    rgba(255, 255, 255, 0) 0 7px,
    rgba(255, 255, 255, 0.5) 7px 10px,
    rgba(255, 255, 255, 0) 10px 17px);
  animation: chevScroll 0.45s linear infinite;
}

@keyframes chevScroll {
  from { background-position: 0 0; }
  to   { background-position: -34px 0; }
}

#nitro-fill::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0; right: 0;
  width: 6px;
  border-radius: 2px;
  background: transparent;
  transition: background 0.15s;
}

#nitro-fill.active::after {
  background: rgba(245, 253, 255, 0.98);
  box-shadow: 0 0 12px rgba(170, 240, 255, 1);
}

@keyframes nitroFlash {
  0%, 100% { filter: brightness(1.12); }
  50% { filter: brightness(1.55); }
}

/* ignition flash: one-shot white-hot pop the instant boost lights (ui.js
   pulses .ignite on the rising nitro edge, then removes it) */
#nitro-bar.ignite {
  /* barIgnite listed last so its one-shot flash wins the box-shadow while it
     runs; the infinite active pulse takes back over when it ends */
  animation: nitroBarActive 0.5s ease-in-out infinite alternate, barIgnite 0.5s ease-out;
}

@keyframes barIgnite {
  0% {
    border-color: #ffffff;
    filter: brightness(2);
    box-shadow: 0 0 46px rgba(210, 248, 255, 1), 0 0 96px rgba(0, 210, 255, 0.9);
  }
  100% { filter: brightness(1); }
}

/* label goes hot while venting */
#nitro-bar.active .nitro-label {
  color: #ffffff;
  text-shadow:
    0 0 2px rgba(5, 9, 22, 1),
    0 0 4px rgba(5, 9, 22, 0.95),
    0 0 9px rgba(140, 225, 255, 0.95),
    0 0 18px rgba(0, 229, 255, 0.8);
}

/* CHARGING (drift refills nitro — driven off drive.drifting by ui.js):
   magenta-rimmed bar, magenta-tipped fill, a rightward energy sweep, and the
   bolt flips to the drift's magenta. The border/tip are constant so the
   state reads in ANY captured frame regardless of sweep phase. */
#nitro-bar.charging {
  border-color: rgba(255, 130, 200, 0.9);
  animation: nitroChargingBar 0.55s ease-in-out infinite alternate;
}

@keyframes nitroChargingBar {
  from { box-shadow: 0 0 12px rgba(255, 45, 149, 0.35), inset 0 0 8px rgba(20, 4, 14, 0.6); }
  to   { box-shadow: 0 0 24px rgba(255, 45, 149, 0.7), 0 0 44px rgba(255, 45, 149, 0.28), inset 0 0 10px rgba(255, 45, 149, 0.25); }
}

#nitro-bar.charging #nitro-fill {
  background: linear-gradient(90deg, #0af, #6ef 62%, #ffb3e0);
  box-shadow: 0 0 12px rgba(150, 220, 255, 1), 0 0 22px rgba(255, 45, 149, 0.5);
}

#nitro-bar.charging #nitro-fill::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 2px;
  background: linear-gradient(105deg, transparent 25%, rgba(255, 255, 255, 0.55) 50%, transparent 75%);
  background-size: 200% 100%;
  animation: chargeSweep 0.8s linear infinite;
}

@keyframes chargeSweep {
  from { background-position: 130% 0; }
  to   { background-position: -130% 0; }
}

#nitro-bar.charging .nitro-label {
  text-shadow:
    0 0 2px rgba(5, 9, 22, 1),
    0 0 4px rgba(5, 9, 22, 0.95),
    0 0 10px rgba(255, 45, 149, 0.85);
}

#nitro-bolt.charging {
  fill: #ffb3dd;
  filter:
    drop-shadow(0 0 6px rgba(255, 120, 195, 0.95))
    drop-shadow(0 0 14px rgba(255, 45, 149, 0.7));
  animation: pulse 0.55s ease-in-out infinite;
}

/* bolt icon flares white while boosting */
#nitro-bolt.active {
  fill: #eafcff;
  filter:
    drop-shadow(0 0 7px rgba(170, 240, 255, 1))
    drop-shadow(0 0 18px rgba(0, 229, 255, 0.85));
  animation: pulse 0.4s ease-in-out infinite;
}

/* ------------------------------------------------------------ popups */
/* right-of-center at ~58% height: reward text never occludes the
   lane corridor where the player scans for the next dodge */
#popups {
  position: absolute;
  top: 58%;
  left: 54%;
  right: 4%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}

/* optional anchor wrapper: ui.popup(text, cls, {x, y}) pins the flyer at
   normalized viewport coords (e.g. beside the car that was just passed) */
.popup-anchor {
  position: fixed;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 25;
}

.popup {
  font-family: var(--display);
  font-size: clamp(20px, 6vw, 30px);
  font-weight: 800;
  font-style: italic;
  letter-spacing: 0.06em;
  animation: popupFly 1.8s cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
}

.popup.nearmiss {
  color: var(--amber);
  text-shadow: 0 0 14px rgba(255, 179, 0, 0.9), 0 2px 10px rgba(0, 0, 0, 0.7);
}

@keyframes popupFly {
  0% { transform: scale(0.4) translateY(10px); opacity: 0; }
  9% { transform: scale(1.25) translateY(0); opacity: 1; }
  16% { transform: scale(1); }
  80% { opacity: 1; transform: scale(1); }
  100% { transform: translateY(-46px) scale(0.92); opacity: 0; }
}

/* screenshot rig: a held popup is pinned at peak visibility — no fly-in,
   no fade windows — so the capture frame is guaranteed to contain it */
.popup.held {
  animation: none !important;
  opacity: 1 !important;
  transform: none !important;
}

/* ---------------------------------------------- in-run mission tracker */
/* compact chip just above the popups area: mission icon in its accent well
   + the live n/target readout. ui.js missionTrack() adds .show for ~2s per
   tracked-stat advance; the accent comes from the SAME per-stat variables
   as the mission cards (see .mcard[data-stat] block). */
#mission-tracker {
  --mc: #00e5ff;
  --mc-rgb: 0, 229, 255;
  position: absolute;
  top: 51.5%;
  left: 54%;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 13px 5px 6px;
  border-radius: 999px;
  background: linear-gradient(135deg, rgba(var(--mc-rgb), 0.16), rgba(15, 12, 32, 0.78) 62%);
  border: 1px solid rgba(var(--mc-rgb), 0.5);
  box-shadow: 0 0 14px rgba(var(--mc-rgb), 0.22), 0 4px 14px rgba(0, 0, 0, 0.4);
  opacity: 0;
  transform: translateY(8px) scale(0.88);
  transition: opacity 0.5s ease, transform 0.5s ease;
  pointer-events: none;
}

#mission-tracker.show {
  opacity: 1;
  transform: none;
  transition: opacity 0.16s ease-out, transform 0.2s cubic-bezier(0.2, 1.5, 0.4, 1);
}

#mission-tracker .mt-icon {
  width: 22px;
  height: 22px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  color: var(--mc);
  background: radial-gradient(circle at 32% 26%,
    rgba(var(--mc-rgb), 0.35), rgba(var(--mc-rgb), 0.1) 78%);
  border: 1px solid rgba(var(--mc-rgb), 0.5);
}

#mission-tracker .mt-icon svg {
  width: 13px;
  height: 13px;
  display: block;
  filter: drop-shadow(0 0 3px rgba(var(--mc-rgb), 0.85));
}

#mission-tracker .mt-val {
  font-family: var(--display);
  font-weight: 800;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.07em;
  white-space: nowrap;
  color: #fff;
  text-shadow: 0 0 8px rgba(var(--mc-rgb), 0.75), 0 1px 4px rgba(0, 0, 0, 0.7);
}

/* mission-complete beat: the pill lands on its full n/n readout and grows
   the amber BANKED tag — a satisfied target reads COMPLETE, never re-armed
   (game.js suppresses further tracker ticks for done/claim-pending slots) */
#mission-tracker .mt-banked { display: none; }

#mission-tracker.banked { border-color: rgba(255, 190, 70, 0.6); }

#mission-tracker.banked .mt-banked {
  display: inline;
  font-family: var(--display);
  font-weight: 800;
  font-size: 8px;
  letter-spacing: 0.1em;
  white-space: nowrap;
  color: #ffd678;
  text-shadow: 0 0 7px rgba(255, 179, 0, 0.55), 0 1px 4px rgba(0, 0, 0, 0.7);
}

/* --------------------------------------------------------- game over */
/* The backdrop is a staged crash vignette (game.js orbits the lit wreck
   with cross-lights + embers): instead of crushing it under a near-black
   scrim, a shallow blur + exposure lift keeps the wreck and skyline
   reading, an additive fire glow pools around the wreck, and DOM embers
   drift up behind the card. */
#over-screen {
  display: flex;
  align-items: center;
  justify-content: center;
  /* headroom clears the top-right wallet pill; the panel scrolls internally
     if a reward-heavy card ever exceeds the viewport */
  padding: calc(58px + var(--safe-top)) 0 calc(10px + var(--safe-bottom));
}

/* shallow depth-of-field with a soft edge vignette for focus. The old heavy
   blur(4px) + saturate(1.25) + brightness(1.22) lift repainted the live 3D
   wreck as a soft airbrushed illustration — a different art style from
   gameplay. Kept to a subtle DOF + near-neutral grade so the backdrop still
   reads as the SAME rendered scene, just out of focus behind the card. */
#over-screen::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  backdrop-filter: blur(2px) saturate(1.06) brightness(1.05);
  -webkit-backdrop-filter: blur(2px) saturate(1.06) brightness(1.05);
  background:
    radial-gradient(ellipse 130% 96% at 50% 38%,
      rgba(10, 6, 20, 0) 44%,
      rgba(10, 6, 20, 0.24) 74%,
      rgba(10, 6, 20, 0.58) 100%),
    linear-gradient(to top, rgba(10, 6, 20, 0.42), transparent 15%);
}

/* warm crash light pooling around the wreck at the bottom of frame,
   flickering like live fire — additive but restrained: tight, dim pools
   that light the wreck's dark mass without flooding it with flat paint */
#over-screen::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  mix-blend-mode: screen;
  background:
    radial-gradient(ellipse 62% 20% at 50% 99%, rgba(255, 92, 28, 0.22), transparent 62%),
    radial-gradient(ellipse 38% 12% at 44% 95%, rgba(255, 45, 149, 0.16), transparent 66%),
    radial-gradient(ellipse 22% 7% at 53% 97%, rgba(255, 200, 100, 0.20), transparent 72%);
  animation: wreckFlicker 2.8s ease-in-out infinite;
}

@keyframes wreckFlicker {
  0%, 100% { opacity: 0.78; }
  18% { opacity: 0.95; }
  34% { opacity: 0.84; }
  55% { opacity: 1; }
  76% { opacity: 0.88; }
}

/* ember sparks + smoke rising off the wreck, behind the results card.
   Negative animation delays keep every ember mid-flight in any captured
   frame instead of clustered at spawn. */
#over-fx {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}

#over-fx i {
  position: absolute;
  bottom: 3vh;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: #ffcf7a;
  box-shadow: 0 0 8px 2px rgba(255, 140, 50, 0.95);
  opacity: 0;
  animation: emberRise 3.4s linear infinite;
}

/* every third ember picks up the magenta of the wreck's neon rim light */
#over-fx i:nth-of-type(3n) {
  background: #ff77bd;
  box-shadow: 0 0 7px 2px rgba(255, 45, 149, 0.8);
}

#over-fx i:nth-of-type(4n + 1) { width: 6px; height: 6px; }
#over-fx i:nth-of-type(5n + 2) { width: 3px; height: 3px; }

#over-fx i:nth-of-type(1)  { left: 20%; --sx: 34px;  animation-duration: 3.2s; animation-delay: -0.5s; }
#over-fx i:nth-of-type(2)  { left: 31%; --sx: -26px; animation-duration: 2.7s; animation-delay: -1.8s; }
#over-fx i:nth-of-type(3)  { left: 40%; --sx: 22px;  animation-duration: 3.8s; animation-delay: -2.6s; }
#over-fx i:nth-of-type(4)  { left: 47%; --sx: -18px; animation-duration: 2.9s; animation-delay: -0.9s; }
#over-fx i:nth-of-type(5)  { left: 54%; --sx: 30px;  animation-duration: 3.5s; animation-delay: -2.1s; }
#over-fx i:nth-of-type(6)  { left: 62%; --sx: -32px; animation-duration: 3.0s; animation-delay: -1.3s; }
#over-fx i:nth-of-type(7)  { left: 70%; --sx: 20px;  animation-duration: 4.1s; animation-delay: -3.2s; }
#over-fx i:nth-of-type(8)  { left: 78%; --sx: -22px; animation-duration: 2.8s; animation-delay: -0.2s; }
#over-fx i:nth-of-type(9)  { left: 26%; --sx: 18px;  animation-duration: 4.4s; animation-delay: -3.7s; }
#over-fx i:nth-of-type(10) { left: 58%; --sx: -14px; animation-duration: 3.3s; animation-delay: -2.9s; }
#over-fx i:nth-of-type(11) { left: 36%; --sx: 26px;  animation-duration: 2.6s; animation-delay: -1.1s; }
#over-fx i:nth-of-type(12) { left: 66%; --sx: -20px; animation-duration: 3.7s; animation-delay: -0.7s; }
/* the second bank: a denser, lower, faster spray right off the wreck line */
#over-fx i:nth-of-type(13) { left: 44%; --sx: -30px; bottom: 1vh;   animation-duration: 2.3s; animation-delay: -1.6s; }
#over-fx i:nth-of-type(14) { left: 51%; --sx: 16px;  bottom: 1.5vh; animation-duration: 2.1s; animation-delay: -0.4s; }
#over-fx i:nth-of-type(15) { left: 57%; --sx: -24px; bottom: 1vh;   animation-duration: 2.5s; animation-delay: -2.0s; }
#over-fx i:nth-of-type(16) { left: 33%; --sx: 28px;  bottom: 2vh;   animation-duration: 2.4s; animation-delay: -1.2s; }
#over-fx i:nth-of-type(17) { left: 73%; --sx: -16px; bottom: 1.5vh; animation-duration: 2.9s; animation-delay: -2.4s; }
#over-fx i:nth-of-type(18) { left: 15%; --sx: 24px;  bottom: 2vh;   animation-duration: 3.1s; animation-delay: -0.8s; }
#over-fx i:nth-of-type(19) { left: 84%; --sx: -28px; bottom: 1vh;   animation-duration: 2.7s; animation-delay: -1.9s; }
#over-fx i:nth-of-type(20) { left: 49%; --sx: 12px;  bottom: 3vh;   animation-duration: 3.6s; animation-delay: -3.1s; }

/* ---- debris field: hot metal shards thrown clear of the impact, tumbling
   as they settle. Sharp, short-lived, low in frame — the visual difference
   between 'the car stopped' and 'the car HIT something'. */
#over-fx b {
  position: absolute;
  bottom: 4vh;
  width: 7px;
  height: 2.5px;
  border-radius: 1px;
  background: linear-gradient(90deg, #fff3d0, #ff9a3c 55%, rgba(255, 90, 20, 0));
  box-shadow: 0 0 7px 1px rgba(255, 130, 40, 0.75);
  opacity: 0;
  animation: debrisFly 2.6s cubic-bezier(0.16, 0.6, 0.4, 1) infinite;
}

#over-fx b:nth-of-type(3n) {
  background: linear-gradient(90deg, #ffd9ec, #ff2d95 55%, rgba(255, 45, 149, 0));
  box-shadow: 0 0 7px 1px rgba(255, 45, 149, 0.7);
}

#over-fx b:nth-of-type(1) { left: 46%; --dx: -132px; --dy: -34px; --rot: -420deg; animation-duration: 2.4s; animation-delay: -1.05s; }
#over-fx b:nth-of-type(2) { left: 52%; --dx: 148px;  --dy: -52px; --rot: 380deg;  animation-duration: 2.8s; animation-delay: -2.2s; }
#over-fx b:nth-of-type(3) { left: 49%; --dx: -96px;  --dy: -78px; --rot: 300deg;  animation-duration: 3.1s; animation-delay: -0.4s; }
#over-fx b:nth-of-type(4) { left: 55%; --dx: 104px;  --dy: -22px; --rot: -520deg; animation-duration: 2.2s; animation-delay: -1.7s; }
#over-fx b:nth-of-type(5) { left: 43%; --dx: -158px; --dy: -64px; --rot: 460deg;  animation-duration: 3.4s; animation-delay: -2.8s; }
#over-fx b:nth-of-type(6) { left: 51%; --dx: 78px;   --dy: -92px; --rot: -340deg; animation-duration: 2.9s; animation-delay: -0.9s; }
#over-fx b:nth-of-type(7) { left: 47%; --dx: -68px;  --dy: -18px; --rot: 560deg;  animation-duration: 2.1s; animation-delay: -1.4s; }
#over-fx b:nth-of-type(8) { left: 54%; --dx: 172px;  --dy: -70px; --rot: -300deg; animation-duration: 3.3s; animation-delay: -2.5s; }

@keyframes debrisFly {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 0; }
  6%   { opacity: 1; }
  70%  { opacity: 0.7; }
  100% { transform: translate(var(--dx, 90px), var(--dy, -40px)) rotate(var(--rot, 360deg)); opacity: 0; }
}

@keyframes emberRise {
  0% { transform: translate(0, 0) scale(1); opacity: 0; }
  7% { opacity: 1; }
  60% { opacity: 0.85; }
  100% { transform: translate(var(--sx, 20px), -52vh) scale(0.35); opacity: 0; }
}

#over-fx .smoke {
  position: absolute;
  bottom: -8vh;
  width: 54vw;
  height: 30vh;
  border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(150, 120, 165, 0.55), transparent 62%);
  filter: blur(18px);
  opacity: 0;
  animation: smokeRise 6.5s ease-out infinite;
}

#over-fx .smoke:nth-of-type(1) { left: 8%;  animation-delay: -2.2s; }
#over-fx .smoke:nth-of-type(2) { left: 44%; animation-duration: 7.8s; animation-delay: -5.1s; }
/* third plume, warmer and tighter — the column still burning off the hood */
#over-fx .smoke:nth-of-type(3) {
  left: 26%;
  width: 40vw;
  height: 26vh;
  background: radial-gradient(ellipse at center, rgba(190, 130, 110, 0.5), transparent 60%);
  animation-duration: 5.6s;
  animation-delay: -1.3s;
}

@keyframes smokeRise {
  0% { transform: translateY(0) scale(0.7); opacity: 0; }
  22% { opacity: 0.15; }
  100% { transform: translateY(-46vh) scale(1.5); opacity: 0; }
}

.over-panel {
  position: relative;
  z-index: 2;   /* above the blur/glow/ember staging layers */
  pointer-events: auto;
  text-align: center;
  max-width: min(94vw, 420px);
  max-height: 100%;
  overflow-y: auto;
  /* NEVER sideways. overflow-y:auto silently promotes overflow-x to auto, so
     one over-wide child turned the results card into a horizontally
     scrolling surface with its own content hanging off the edge. */
  overflow-x: hidden;
  /* narrower side rails at phone widths buy the button row and the season
     lane ~11px of usable card each side */
  padding: 18px clamp(14px, 4.5vw, 34px) 18px;
  border-radius: 22px;
  /* Opaque enough that the wreck behind it cannot draw ON the copy. At 0.72
     the city's horizon lasers and the hero's own bodywork cut straight
     across the reward rows and the CTA — the second half of the player's
     "results screen overlaps its text" report, and invisible to a box audit
     because the offending geometry is in the 3D frame, not the DOM. */
  background: linear-gradient(180deg, rgba(15, 10, 32, 0.9), rgba(10, 7, 24, 0.93));
  border: 1px solid rgba(255, 45, 149, 0.35);
  box-shadow: 0 0 60px rgba(255, 45, 149, 0.18), inset 0 0 40px rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  animation: panelIn 0.5s cubic-bezier(0.2, 1.4, 0.4, 1);
}

@keyframes panelIn {
  from { transform: scale(0.7) translateY(30px); opacity: 0; }
  to { transform: scale(1) translateY(0); opacity: 1; }
}

.over-label {
  font-family: var(--display);
  font-size: clamp(24px, 7vw, 34px);
  font-weight: 900;
  font-style: italic;
  letter-spacing: 0.18em;
  text-indent: 0.18em;
  color: #fff;
  /* two-layer glow: a tight white core keeps the letterforms crisp, a wide
     low-alpha accent supplies the neon ambience. One fat mid-radius magenta
     wash read as bloom, not craft. */
  text-shadow: 0 0 2px rgba(255, 255, 255, 0.9), 0 0 26px rgba(255, 45, 149, 0.18);
}

/* .hidden alone only makes a box invisible — it keeps its space. On the
   results card that left two phantom blocks (NEW BEST!, the prestige chip)
   holding ~55px of dead air above the score on every run that did not earn
   them, which is height the CTA row needed at 360x780. */
#new-best.hidden,
.pbadge.hidden { display: none; }

#new-best {
  margin-top: 8px;
  font-family: var(--display);
  font-size: clamp(12px, 3.6vw, 16px);
  font-weight: 700;
  letter-spacing: 0.26em;
  text-indent: 0.26em;
  color: var(--amber);
  text-shadow: 0 0 12px rgba(255, 179, 0, 0.9);
  animation: pulse 0.9s ease-in-out infinite;
}

#final-score {
  margin-top: 8px;
  font-family: var(--display);
  font-size: clamp(40px, 12vw, 62px);
  font-weight: 800;
  font-style: italic;
  color: #fff;
  /* same two-layer treatment as the screen titles: crisp core, wide
     low-alpha cyan ambience */
  text-shadow: 0 0 2px rgba(255, 255, 255, 0.9), 0 0 26px rgba(0, 229, 255, 0.18);
}

#final-best {
  margin-top: 6px;
  font-size: clamp(11px, 3.2vw, 14px);
  letter-spacing: 0.3em;
  color: rgba(180, 195, 230, 0.65);
  font-weight: 700;
}

/* PB delta line: the record this run moved ('+2,140 VS WEEK BEST') —
   amber gain language, sat tight under the score/best pair */
#pb-delta {
  margin-top: 5px;
  font-family: var(--display);
  font-weight: 800;
  font-style: italic;
  font-size: clamp(10px, 3vw, 13px);
  letter-spacing: 0.14em;
  color: #ffd376;
  text-shadow: 0 0 12px rgba(255, 179, 0, 0.55), 0 1px 4px rgba(0, 0, 0, 0.8);
}

#pb-delta.hidden { display: none; }

/* streak reminder at the card foot — the come-back-tomorrow hook */
#over-streak {
  margin-top: 12px;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 6px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 8px;
  letter-spacing: 0.16em;
  text-indent: 0.08em;
  color: rgba(255, 224, 160, 0.85);
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.35), 0 1px 3px rgba(0, 0, 0, 0.8);
}

#over-streak.hidden { display: none; }

#over-streak svg {
  width: 11px;
  height: 11px;
  flex: none;
  stroke: var(--amber);
  filter: drop-shadow(0 0 4px rgba(255, 179, 0, 0.6));
}

/* run-stats row: distance / top speed / near misses / best combo — the
   results screen sells the run back with the HUD's amber/cyan accent split */
#over-stats {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid rgba(120, 140, 200, 0.16);
  display: flex;
  justify-content: center;
  align-items: stretch;
}

#over-stats .stat {
  flex: 1 1 0;
  padding: 0 clamp(6px, 2.4vw, 14px);
  text-align: center;
}

#over-stats .stat + .stat { border-left: 1px solid rgba(120, 140, 200, 0.14); }

.stat-val {
  font-family: var(--display);
  font-style: italic;
  font-weight: 700;
  font-size: clamp(14px, 4.2vw, 19px);
  white-space: nowrap;
  color: #fff;
}

.stat-val.cyan {
  color: #9beeff;
  text-shadow: 0 0 10px rgba(0, 229, 255, 0.4), 0 1px 6px rgba(0, 0, 0, 0.8);
}

.stat-val.amber {
  color: #ffd376;
  text-shadow: 0 0 10px rgba(255, 179, 0, 0.4), 0 1px 6px rgba(0, 0, 0, 0.8);
}

.stat-unit {
  margin-left: 3px;
  font-size: 0.52em;
  font-style: normal;
  font-weight: 600;
  letter-spacing: 0.08em;
  opacity: 0.75;
}

.stat-label {
  margin-top: 4px;
  font-family: var(--display);
  font-size: 7px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-indent: 0.12em;
  white-space: nowrap;
  color: rgba(170, 185, 220, 0.6);
}

.over-buttons {
  margin-top: 14px;
  /* A GRID, NOT A 3-ACROSS FLEX ROW. Three skewed plates fighting for 288px
     of card put DRIVE AGAIN's hit box a couple of pixels off LOBBY's at
     360px and left no room for the label. The primary CTA now owns a full
     row of its own — it cannot collide with anything at any width, and it is
     the biggest tap target on the card. */
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

#restart-btn { grid-column: 1 / -1; }

.over-buttons button {
  font-family: var(--display);
  font-size: clamp(12px, 3.5vw, 15px);
  font-weight: 700;
  letter-spacing: 0.1em;
  padding: 14px 20px;
  border-radius: 14px;
  border: none;
  color: #fff;
  cursor: pointer;
  transform: skewX(-6deg);
  transition: transform 0.12s, filter 0.12s;
}

.over-buttons button:active { transform: skewX(-6deg) scale(0.94); filter: brightness(1.3); }

#restart-btn {
  background: linear-gradient(135deg, #ff2d95, #b8156a);
  box-shadow: 0 4px 24px rgba(255, 45, 149, 0.45);
}

#share-btn {
  background: rgba(30, 40, 80, 0.6);
  border: 1px solid rgba(0, 229, 255, 0.45);
  box-shadow: 0 4px 18px rgba(0, 229, 255, 0.18);
}

/* ----------------------------------------------------------- effects */
/* IMPACT PUNCH. The old single white plate faded 0.9 -> 0 over half a second:
   a slow wash with no direction and no heat, which is what made the crash
   read as a state change rather than a hit. Now three layers on three
   different clocks — a 0.14s white core, a 0.42s shock ring, a 0.6s fire
   bloom — so the frame snaps, blows out and then burns down. */
#flash {
  position: fixed;
  inset: 0;
  z-index: 40;
  pointer-events: none;
  overflow: hidden;
}

#flash i {
  position: absolute;
  display: block;
  opacity: 0;
  will-change: opacity, transform;
}

.fl-core { inset: 0; background: #fff; }

.fl-heat {
  inset: 0;
  mix-blend-mode: screen;
  background:
    radial-gradient(ellipse 85% 55% at 50% 58%, rgba(255, 168, 72, 0.9), transparent 66%),
    radial-gradient(ellipse 120% 90% at 50% 62%, rgba(255, 45, 149, 0.42), transparent 72%);
}

.fl-shock {
  left: 50%;
  top: 58%;
  width: 44vmax;
  height: 44vmax;
  margin: -22vmax 0 0 -22vmax;
  border-radius: 50%;
  border: 3px solid rgba(255, 245, 225, 0.9);
  box-shadow: 0 0 38px rgba(255, 140, 55, 0.85), inset 0 0 32px rgba(255, 120, 40, 0.5);
}

#flash.go .fl-core  { animation: flCore  0.14s ease-out; }
#flash.go .fl-shock { animation: flShock 0.42s cubic-bezier(0.12, 0.8, 0.25, 1); }
#flash.go .fl-heat  { animation: flHeat  0.60s cubic-bezier(0.1, 0.7, 0.3, 1); }

@keyframes flCore {
  0% { opacity: 0.92; }
  35% { opacity: 0.3; }
  100% { opacity: 0; }
}

@keyframes flShock {
  0% { opacity: 0.95; transform: scale(0.06); }
  100% { opacity: 0; transform: scale(1.18); }
}

@keyframes flHeat {
  0% { opacity: 0; transform: scale(1.22); }
  9% { opacity: 1; }
  100% { opacity: 0; transform: scale(1); }
}

/* the HUD takes the hit too — a hard downward jolt that settles in ~0.3s.
   Without it the interface sat perfectly still through an impact that was
   throwing the camera around, and the crash read as a state change. */
#hud.crash-kick { animation: hudKick 0.32s cubic-bezier(0.2, 0.9, 0.3, 1); }

@keyframes hudKick {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg); }
  14%  { transform: translate3d(-5px, 9px, 0) rotate(0.7deg); }
  36%  { transform: translate3d(4px, -5px, 0) rotate(-0.45deg); }
  62%  { transform: translate3d(-2px, 3px, 0) rotate(0.2deg); }
  100% { transform: translate3d(0, 0, 0) rotate(0deg); }
}

#vignette-pulse {
  position: fixed;
  inset: 0;
  z-index: 15;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s;
  background: radial-gradient(ellipse at center, transparent 55%, rgba(40, 160, 255, 0.28));
}

#vignette-pulse.on { opacity: 1; animation: nitroVig 0.5s ease-in-out infinite alternate; }

@keyframes nitroVig {
  from { opacity: 0.55; }
  to { opacity: 1; }
}

/* ------------------------------------------------- nitro post-stack */
/* Full-frame DOM boost FX, layered between the 3D frame (z<16) and the HUD
   (z20). ui.js toggles .on off drive.nitro every frame and pulses .ignite on
   the rising edge. All loops use negative delays + wide-opacity keyframes so
   the deterministic capture always lands mid-effect. */
#nitro-fx {
  position: fixed;
  inset: 0;
  z-index: 16;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.28s ease;
}

#nitro-fx.on { opacity: 1; }

/* tunnel edge-blur: was a full-frame backdrop-filter blur(3px) masked out of
   the center. A backdrop filter re-renders the whole backdrop on EVERY
   compositor frame (120 Hz on ProMotion, driven by the streak animations
   below), independent of the WebGL frame rate, and was the single largest
   item in the nitro collapse on device. The same edge blur + saturate now
   lives in the grade pass (rendering.js, uNitro) where it costs a few taps
   once per rendered frame. The element stays for the markup/ablation rig. */
.nfx-blur { display: none; }

/* dark tunnel vignette + red/cyan rims offset either side of the vanishing
   point = the chromatic shift of a lens being shoved forward */
.nfx-vig {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 108% 86% at 48.8% 46%, transparent 58%, rgba(255, 40, 70, 0.19) 79%, transparent 94%),
    radial-gradient(ellipse 108% 86% at 51.2% 46%, transparent 58%, rgba(0, 229, 255, 0.21) 81%, transparent 96%),
    radial-gradient(ellipse 118% 94% at 50% 46%, transparent 42%, rgba(3, 6, 18, 0.34) 74%, rgba(2, 4, 14, 0.6) 100%);
}

/* warp spokes: thin cyan rays bursting from the vanishing point, masked off
   the center, spinning one 24° pattern-period per loop (seamless) */
.nfx-warp {
  position: absolute;
  inset: -35%;
  transform-origin: 50% 47%;
  /* no filter here on purpose. This layer rotates every frame, and a
     filter: blur(1px) on it was re-applied per composite over a 170%-inset
     surface (9 Mpx at DPR 3). The 0.7deg gradient ramps below already soften
     each ray by more than that blur did at the visible radius (1px is about
     0.1deg at 500px), so the look is unchanged. */
  background: repeating-conic-gradient(from 3deg at 50% 47%,
    transparent 0deg 7.2deg,
    rgba(150, 230, 255, 0.5) 7.9deg 8.4deg,
    transparent 9.1deg 15.6deg,
    rgba(255, 255, 255, 0.32) 16.2deg 16.5deg,
    transparent 17.1deg 24deg);
  -webkit-mask-image: radial-gradient(ellipse 62% 46% at 50% 47%, transparent 0 30%, #000 64%);
  mask-image: radial-gradient(ellipse 62% 46% at 50% 47%, transparent 0 30%, #000 64%);
  opacity: 0.62;
  animation: warpSpin 2.8s linear infinite;
}

@keyframes warpSpin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(24deg); }
}

/* anamorphic streaks: horizontal light bars flying outward across the WHOLE
   frame (top through bottom), left half flying left, right half right. Each
   is visible for ~80% of its loop so any captured frame carries several. */
.nfx-streaks { position: absolute; inset: 0; overflow: hidden; }

.nfx-streaks i {
  position: absolute;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, transparent,
    rgba(160, 235, 255, 0.5) 18%,
    rgba(242, 252, 255, 1) 38% 62%,
    rgba(160, 235, 255, 0.5) 82%, transparent);
  box-shadow: 0 0 8px 2px rgba(140, 230, 255, 0.9), 0 0 26px 6px rgba(0, 229, 255, 0.42);
  opacity: 0;
  animation: streakFly 0.62s linear infinite;
}

@keyframes streakFly {
  0%   { transform: translateX(0) scaleX(0.45); opacity: 0; }
  12%  { opacity: var(--o, 0.9); }
  78%  { opacity: var(--o, 0.9); }
  100% { transform: translateX(var(--fly, 30vw)) scaleX(1.8); opacity: 0; }
}

.nfx-streaks i:nth-child(1)  { top: 7%;  left: 6%;  width: 30vw; --fly: -26vw; --o: 0.85; animation-delay: -0.11s; }
.nfx-streaks i:nth-child(2)  { top: 13%; left: 58%; width: 24vw; --fly: 30vw;  --o: 0.7;  animation-duration: 0.5s;  animation-delay: -0.32s; }
.nfx-streaks i:nth-child(3)  { top: 22%; left: 2%;  width: 20vw; --fly: -24vw; --o: 0.75; animation-duration: 0.55s; animation-delay: -0.45s; height: 3px; }
.nfx-streaks i:nth-child(4)  { top: 30%; left: 66%; width: 32vw; --fly: 26vw;  --o: 0.9;  animation-duration: 0.7s;  animation-delay: -0.2s; }
.nfx-streaks i:nth-child(5)  { top: 38%; left: -4%; width: 26vw; --fly: -22vw; --o: 0.8;  animation-duration: 0.48s; animation-delay: -0.05s; }
.nfx-streaks i:nth-child(6)  { top: 44%; left: 72%; width: 28vw; --fly: 24vw;  --o: 0.85; animation-duration: 0.6s;  animation-delay: -0.5s; height: 3px; }
.nfx-streaks i:nth-child(7)  { top: 55%; left: 0%;  width: 22vw; --fly: -26vw; --o: 0.8;  animation-duration: 0.52s; animation-delay: -0.27s; }
.nfx-streaks i:nth-child(8)  { top: 62%; left: 74%; width: 24vw; --fly: 26vw;  --o: 0.75; animation-duration: 0.66s; animation-delay: -0.38s; }
.nfx-streaks i:nth-child(9)  { top: 72%; left: -2%; width: 30vw; --fly: -24vw; --o: 0.85; animation-duration: 0.58s; animation-delay: -0.15s; height: 3px; }
.nfx-streaks i:nth-child(10) { top: 79%; left: 64%; width: 34vw; --fly: 28vw;  --o: 0.9;  animation-duration: 0.5s;  animation-delay: -0.42s; }
.nfx-streaks i:nth-child(11) { top: 88%; left: 4%;  width: 26vw; --fly: -28vw; --o: 0.8;  animation-duration: 0.64s; animation-delay: -0.55s; }
.nfx-streaks i:nth-child(12) { top: 94%; left: 60%; width: 30vw; --fly: 26vw;  --o: 0.85; animation-duration: 0.56s; animation-delay: -0.3s; height: 3px; }

/* ignition flare: one-shot anamorphic burst — a horizontal lens bar + cyan
   core bloom snapping wide then dying inside ~0.7s */
.nfx-flare {
  position: absolute;
  inset: 0;
  opacity: 0;
  /* soft edges come from the gradient ramps (was filter: blur(2px), which is
     re-applied on every composite while the flare scales through ignite) */
  background:
    linear-gradient(180deg, transparent 45.4%, rgba(200, 244, 255, 0.85) 49.3% 50.7%, transparent 54.6%),
    radial-gradient(ellipse 56% 30% at 50% 50%, rgba(220, 250, 255, 0.85), rgba(0, 229, 255, 0.4) 44%, transparent 70%);
}

#nitro-fx.ignite .nfx-flare { animation: nfxIgnite 0.7s cubic-bezier(0.17, 0.84, 0.44, 1); }

@keyframes nfxIgnite {
  0%   { opacity: 0; transform: scale(0.25, 0.6); }
  16%  { opacity: 1; transform: scale(1, 1); }
  100% { opacity: 0; transform: scale(1.6, 1.15); }
}

/* one-shot HUD kick on ignition: the instruments physically take the shove */
#hud.kick { animation: hudKick 0.45s cubic-bezier(0.2, 1.6, 0.4, 1); }

@keyframes hudKick {
  0%   { transform: scale(1) translateY(0); }
  28%  { transform: scale(1.025) translateY(3px); }
  60%  { transform: scale(0.995) translateY(-1px); }
  100% { transform: scale(1) translateY(0); }
}

/* ==================================================== META-GAME UI */
/* -------------------------------------------------- chips currency pill */
/* ONE home for the wallet across the whole meta shell: top-RIGHT, one
   scale. The title hub's absolute pill is aligned to sit exactly where the
   garage/bonus header pills land, so the balance visibly persists through
   every screen transition instead of teleporting corners. */
.chips-pill {
  position: absolute;
  top: calc(19px + var(--safe-top));
  right: 14px;
  left: auto;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 15px 8px 12px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  color: #ffe9c0;
  text-shadow: 0 0 10px rgba(255, 179, 0, 0.55), 0 1px 4px rgba(0, 0, 0, 0.8);
  background: linear-gradient(135deg, rgba(52, 34, 8, 0.6), rgba(22, 13, 5, 0.75));
  border: 1px solid rgba(255, 179, 0, 0.5);
  border-radius: 999px;
  box-shadow: 0 0 18px rgba(255, 179, 0, 0.18), inset 0 0 12px rgba(255, 179, 0, 0.08);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.chips-pill svg {
  width: 15px;
  height: 15px;
  flex: none;
  fill: var(--amber);
  filter: drop-shadow(0 0 5px rgba(255, 179, 0, 0.8));
}

.chips-pill svg .chip-core { fill: #5a3c05; }

/* header variant: same pill in flow, compacted a step so BACK + title +
   the dual-currency wallet all fit a 390px header without clipping */
.chips-pill.small {
  position: static;
  flex: none;
  gap: 5px;
  padding: 7px 11px 7px 9px;
  font-size: 12.5px;
}

.chips-pill.small svg { width: 13px; height: 13px; }

.chips-pill.small .wallet-div { height: 12px; margin: 0 1px; }

/* game-over wallet: same top-right anchor as the hub pill, held above the
   over-screen's blur/ember staging layers */
#chips-pill-over { z-index: 2; }

/* chip pulse when the balance changes (ui.js re-arms the class) */
.chips-pill.bump { animation: chipsBump 0.5s cubic-bezier(0.2, 1.8, 0.4, 1); }

@keyframes chipsBump {
  0% { transform: scale(1.14); box-shadow: 0 0 30px rgba(255, 179, 0, 0.55), inset 0 0 14px rgba(255, 179, 0, 0.25); }
  100% { transform: scale(1); }
}

/* live wallet count-up on the results card: while the run payout banks,
   the header pill runs hot amber — number, border and glow — so the
   climbing balance reads as MONEY ARRIVING, not a quiet reflow */
.chips-pill.ticking {
  border-color: rgba(255, 179, 0, 0.85);
  box-shadow: 0 0 22px rgba(255, 179, 0, 0.4), inset 0 0 10px rgba(255, 179, 0, 0.18);
}

.chips-pill.ticking .chips-num {
  color: var(--amber);
  text-shadow: 0 0 10px rgba(255, 179, 0, 0.85);
}

/* chip particle in transit from the +N payout pill to the header wallet
   (ui.js positions it with a transform along a small arc) */
.chip-fly {
  position: fixed;
  left: -8px;
  top: -8px;
  width: 16px;
  height: 16px;
  z-index: 60;
  pointer-events: none;
  will-change: transform;
}

.chip-fly svg {
  width: 100%;
  height: 100%;
  fill: var(--amber);
  filter: drop-shadow(0 0 7px rgba(255, 179, 0, 0.9));
}

/* ---------------------------------------------------- title CTA stack */
#play-btn {
  pointer-events: auto;
  cursor: pointer;
  width: min(74vw, 310px);
  min-height: 58px;
  font-family: var(--display);
  font-weight: 900;
  font-style: italic;
  font-size: clamp(19px, 5.6vw, 24px);
  letter-spacing: 0.34em;
  text-indent: 0.34em;
  color: #fff;
  text-shadow: 0 0 12px rgba(255, 190, 225, 0.9);
  transform: skewX(-8deg);
  background: linear-gradient(160deg, #ff4da6 0%, #ff2d95 42%, #99114f 100%);
  border: 1px solid rgba(255, 170, 215, 0.75);
  border-radius: 13px;
  box-shadow:
    0 0 28px rgba(255, 45, 149, 0.5),
    0 8px 30px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.35),
    inset 0 0 18px rgba(255, 120, 190, 0.25);
  animation: playHum 1.8s ease-in-out infinite alternate;
  transition: transform 0.12s, filter 0.12s;
}

#play-btn:active { transform: skewX(-8deg) scale(0.95); filter: brightness(1.25); }

@keyframes playHum {
  from { box-shadow: 0 0 20px rgba(255, 45, 149, 0.4), 0 8px 30px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.35), inset 0 0 14px rgba(255, 120, 190, 0.2); }
  to   { box-shadow: 0 0 40px rgba(255, 45, 149, 0.7), 0 8px 30px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.35), inset 0 0 22px rgba(255, 140, 200, 0.32); }
}

/* four meta pillars (GARAGE / RANKS / STORE / BONUS) as a 2x2 block matched
   to the PLAY button's width — one CTA column, no crowding at 390px */
.cta-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  justify-content: center;
  gap: 10px;
  width: min(74vw, 310px);
  margin: 13px auto 0;
}

.cta-row button {
  pointer-events: auto;
  cursor: pointer;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 48px;
  min-width: 0;
  padding: 0 10px;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(11px, 3.2vw, 13px);
  letter-spacing: 0.2em;
  text-indent: 0.1em;
  color: #d8f6ff;
  text-shadow: 0 0 8px rgba(0, 229, 255, 0.55);
  background: rgba(8, 14, 30, 0.55);
  border: 1px solid rgba(0, 229, 255, 0.55);
  border-radius: 11px;
  transform: skewX(-8deg);
  box-shadow: 0 0 16px rgba(0, 229, 255, 0.22), 0 4px 18px rgba(0, 0, 0, 0.45), inset 0 0 12px rgba(0, 229, 255, 0.08);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  transition: transform 0.12s, filter 0.12s;
}

.cta-row button:active { transform: skewX(-8deg) scale(0.94); filter: brightness(1.35); }

.cta-row button svg {
  width: 15px;
  height: 15px;
  flex: none;
  fill: currentColor;
  filter: drop-shadow(0 0 4px rgba(0, 229, 255, 0.6));
}

#bonus-btn {
  color: #ffe3ad;
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.55);
  border-color: rgba(255, 179, 0, 0.55);
  background: rgba(30, 20, 6, 0.55);
  box-shadow: 0 0 16px rgba(255, 179, 0, 0.2), 0 4px 18px rgba(0, 0, 0, 0.45), inset 0 0 12px rgba(255, 179, 0, 0.08);
}

#bonus-btn svg { filter: drop-shadow(0 0 4px rgba(255, 179, 0, 0.6)); }

/* claimable badge: hot magenta dot at the button corner */
#bonus-badge {
  position: absolute;
  top: -5px;
  right: -5px;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #ffb4dd, #ff2d95 65%);
  border: 1px solid rgba(255, 255, 255, 0.6);
  box-shadow: 0 0 10px rgba(255, 45, 149, 0.95), 0 0 22px rgba(255, 45, 149, 0.5);
  animation: pulse 1.1s ease-in-out infinite;
}

/* badge carrying a claimable COUNT (ui.js setBonusBadge(n)) — the plain
   dot above stays for the bare daily-ready state */
#bonus-badge.n {
  width: auto;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--display);
  font-weight: 900;
  font-size: 9px;
  line-height: 1;
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}

/* ------------------------------------------------ panel chrome (shared) */
.panel-top {
  position: absolute;
  top: calc(12px + var(--safe-top));
  left: 0; right: 0;
  /* THREE HARD COLUMNS, not space-between. The old flex row let the nowrap
     title and the wallet pill push past the BACK button's box at 360px —
     BACK's tappable rect landed on top of both 'GARAGE' and the 'AXIOM GT'
     sub-line (player-reported hit-target conflict). A grid gives the title a
     column of its own that starts AFTER back's box ends, and the 12px gutters
     absorb the skewX rect flare on both plates, so the three never touch at
     any width. */
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 12px;
  padding: 0 12px;
  /* MUST outrank the scrolling sheet below it. Panels like #store-panel set
     backdrop-filter, which creates a stacking context that paints above an
     un-z-indexed absolute sibling — that buried BACK under the store list and
     trapped the player in the store once they scrolled (QA blocker). */
  z-index: 5;
}

.back-btn {
  pointer-events: auto;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 44px;
  /* justify-self keeps the plate at its intrinsic width inside the grid's
     first column instead of stretching across it */
  justify-self: start;
  padding: 0 10px 0 7px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.13em;
  color: #d8f6ff;
  text-shadow: 0 0 8px rgba(0, 229, 255, 0.55);
  background: rgba(8, 14, 30, 0.6);
  border: 1px solid rgba(0, 229, 255, 0.45);
  border-radius: 11px;
  transform: skewX(-8deg);
  box-shadow: 0 0 12px rgba(0, 229, 255, 0.18), inset 0 0 10px rgba(0, 229, 255, 0.06);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  transition: transform 0.12s, filter 0.12s;
}

.back-btn:active { transform: skewX(-8deg) scale(0.94); filter: brightness(1.35); }

.back-btn svg { width: 14px; height: 14px; flex: none; }

.panel-title {
  /* the grid's centre column: min-width 0 so a long title shrinks INSIDE its
     own track rather than shoving the row wider than the screen, and centred
     text so it still reads as a centred header */
  min-width: 0;
  text-align: center;
  font-family: var(--display);
  font-weight: 900;
  font-style: italic;
  /* 4.1vw (was 4.6) keeps 'SETTINGS' — the longest header word — inside the
     centre track at 360px instead of spilling under BACK */
  font-size: clamp(13px, 4.1vw, 22px);
  letter-spacing: 0.15em;
  text-indent: 0.15em;
  color: #fff;
  /* two-layer glow (see .over-label): crisp white core + wide 0.18-alpha
     magenta ambience instead of one wide soft wash */
  text-shadow: 0 0 2px rgba(255, 255, 255, 0.9), 0 0 26px rgba(255, 45, 149, 0.18);
  transform: skewX(-8deg);
  white-space: nowrap;
}

/* ------------------------------------------------------------ garage */
#garage-screen {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

/* soft floor scrim under the sheet so panel and street blend */
/* soft floor scrim under the sheet so panel and street blend. It is sized
   off the LIVE sheet height, so a collapsed (peek) sheet stops dimming the
   floor the car is standing on. */
#garage-screen::before {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: calc(var(--garage-sheet-h, 30vh) + 60px);
  background: linear-gradient(to top, rgba(10, 6, 20, 0.72), transparent);
}

/* THE SHEET'S HEIGHT BUDGET. Before this the panel had none at all: it was
   the last child of a justify-content:flex-end column and simply grew with
   the item count until it covered the car the player came here to look at
   (player report: "Garage Menu still Covers car View"). The budget is a
   fraction of the viewport, so at 360x780 — the smallest phone that ships —
   the sheet can never take more than ~43% of the screen and the car keeps a
   guaranteed clear area above it. The grid scrolls INSIDE the budget. */
#garage-panel {
  pointer-events: auto;
  position: relative;
  /* Reads as one sentence: never more than 43% of the frame, never more than
     330px, and NEVER so tall that the car is left less than 400px of clear
     frame above it — with a 200px floor so the shelf stays usable on a short
     viewport. At 360x780 all four agree on 330px, which leaves the hero a
     384px band between the header and the sheet. */
  max-height: max(200px, min(43vh, 330px, calc(100vh - 400px)));
  display: flex;
  flex-direction: column;
  min-height: 0;
  margin: 0 10px calc(10px + var(--safe-bottom));
  padding: 4px 12px 13px;
  border-radius: 20px;
  background: linear-gradient(180deg, rgba(16, 12, 34, 0.78), rgba(10, 7, 22, 0.86));
  border: 1px solid rgba(0, 229, 255, 0.3);
  box-shadow:
    0 0 34px rgba(0, 229, 255, 0.12),
    0 -12px 40px rgba(0, 0, 0, 0.45),
    inset 0 1px 0 rgba(160, 230, 255, 0.14),
    inset 0 0 34px rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  animation: sheetIn 0.4s cubic-bezier(0.2, 1.3, 0.4, 1);
}

@keyframes sheetIn {
  from { transform: translateY(40px); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}

/* peek handle: collapses the sheet to tabs-only so the car can be looked at
   with nothing in front of it, and back again. Full-width so it is an easy
   thumb target without stealing height from the grid. */
#garage-handle {
  pointer-events: auto;
  cursor: pointer;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  min-height: 30px;
  padding: 0;
  background: none;
  border: none;
  color: rgba(175, 195, 230, 0.72);
  font-family: var(--display);
  font-weight: 700;
  font-size: 8.5px;
  letter-spacing: 0.2em;
  text-indent: 0.2em;
  transition: color 0.15s;
}

#garage-handle:active { color: #fff; }

#garage-handle .gh-grip {
  width: 34px;
  height: 3px;
  border-radius: 2px;
  background: rgba(120, 200, 235, 0.5);
  box-shadow: 0 0 8px rgba(0, 229, 255, 0.4);
}

#garage-handle svg {
  width: 13px;
  height: 13px;
  transition: transform 0.22s;
}

#garage-panel.peek #garage-handle svg { transform: rotate(180deg); }

/* PEEK STATE: the grid and the preview bar duck away, the tab row stays so
   the player can still steer the shelf, and the car gets the whole frame. */
#garage-panel.peek #garage-items,
#garage-panel.peek #garage-preview { display: none; }
#garage-panel.peek #garage-tabs { margin-bottom: 2px; }

#garage-tabs {
  flex: none;
  display: flex;
  gap: 7px;
  margin-bottom: 11px;
}

.gtab {
  pointer-events: auto;
  cursor: pointer;
  flex: 1 1 0;
  min-height: 44px;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(10px, 2.8vw, 12px);
  letter-spacing: 0.16em;
  text-indent: 0.16em;
  color: rgba(175, 195, 230, 0.68);
  background: rgba(22, 18, 44, 0.55);
  border: 1px solid rgba(120, 140, 200, 0.22);
  border-radius: 9px;
  transform: skewX(-6deg);
  transition: color 0.15s, border-color 0.15s, box-shadow 0.15s, background 0.15s;
}

.gtab.active {
  color: #fff;
  text-shadow: 0 0 9px rgba(0, 229, 255, 0.85);
  border-color: rgba(0, 229, 255, 0.75);
  background: linear-gradient(180deg, rgba(0, 229, 255, 0.17), rgba(0, 90, 130, 0.2));
  box-shadow: 0 0 15px rgba(0, 229, 255, 0.32), inset 0 0 11px rgba(0, 229, 255, 0.13);
}

/* the shelf SCROLLS inside the sheet's budget instead of pushing it taller */
#garage-items {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* room for the fade + the scroll rail so a card edge never sits under it */
  padding: 1px 5px 10px 1px;
  scrollbar-width: thin;
  scrollbar-color: rgba(0, 229, 255, 0.4) transparent;
  /* the grid is scrollable: soften its bottom edge so a half-row reads as
     "more below" rather than as a clipped row */
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 16px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 16px), transparent);
}

/* a shelf short enough to fit needs no fade at all */
#garage-items.nofade {
  -webkit-mask-image: none;
  mask-image: none;
  padding-bottom: 1px;
}

#garage-items::-webkit-scrollbar { width: 4px; }
#garage-items::-webkit-scrollbar-track { background: transparent; }
#garage-items::-webkit-scrollbar-thumb {
  background: rgba(0, 229, 255, 0.35);
  border-radius: 3px;
}

/* ------------------------------------------- garage PREVIEW bar (try-on)
   The sheet's commit bar. It is deliberately AMBER-on-dark — the shelf's
   owned/equipped language is cyan, so a previewing sheet can never be
   mistaken for an owned one — and it carries the only control in the garage
   that spends anything. Same grammar as the store's RADIO rows: tap to
   preview, a separate explicit control to commit. */
#garage-preview {
  flex: none;
  margin-top: 9px;
  padding: 8px 9px 9px;
  border-radius: 14px;
  background: linear-gradient(180deg, rgba(255, 179, 0, 0.16), rgba(26, 18, 8, 0.72));
  border: 1px solid rgba(255, 179, 0, 0.6);
  box-shadow: 0 0 20px rgba(255, 179, 0, 0.2), inset 0 0 16px rgba(0, 0, 0, 0.4);
  animation: pvIn 0.22s cubic-bezier(0.2, 1.2, 0.4, 1);
}

#garage-preview.hidden { display: none; }

@keyframes pvIn {
  from { transform: translateY(10px); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}

.gpv-head {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  margin-bottom: 8px;
}

.gpv-tag {
  flex: none;
  padding: 3px 7px;
  border-radius: 6px;
  background: rgba(255, 179, 0, 0.9);
  color: #241500;
  font-family: var(--display);
  font-weight: 900;
  font-size: 8px;
  letter-spacing: 0.16em;
  text-indent: 0.16em;
  animation: pvPulse 1.5s ease-in-out infinite alternate;
}

@keyframes pvPulse {
  from { box-shadow: 0 0 0 rgba(255, 179, 0, 0); }
  to   { box-shadow: 0 0 12px rgba(255, 179, 0, 0.75); }
}

.gpv-name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(9px, 2.9vw, 11px);
  letter-spacing: 0.1em;
  color: #fff;
  text-shadow: 0 0 10px rgba(255, 179, 0, 0.5);
}

.gpv-kind {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
  font-family: var(--display);
  font-weight: 500;
  font-size: 7.5px;
  letter-spacing: 0.14em;
  color: rgba(226, 210, 180, 0.8);
}

.gpv-acts {
  display: grid;
  grid-template-columns: 1fr 1.45fr;
  gap: 8px;
}

.gpv-acts button {
  pointer-events: auto;
  cursor: pointer;
  min-height: 42px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 6px 8px;
  border-radius: 10px;
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(9.5px, 2.9vw, 11.5px);
  letter-spacing: 0.1em;
  white-space: nowrap;
  transform: skewX(-6deg);
  transition: filter 0.12s, transform 0.1s;
}

.gpv-acts button:active { transform: skewX(-6deg) scale(0.95); filter: brightness(1.25); }

#gpv-cancel {
  color: rgba(210, 220, 240, 0.9);
  background: rgba(20, 18, 34, 0.75);
  border: 1px solid rgba(150, 165, 205, 0.4);
}

#gpv-buy {
  color: #2a1a00;
  background: linear-gradient(160deg, #ffd66b 0%, #ffb300 48%, #b87a00 100%);
  border: 1px solid rgba(255, 226, 160, 0.85);
  box-shadow: 0 0 18px rgba(255, 179, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.45);
}

#gpv-buy svg { width: 12px; height: 12px; fill: currentColor; }

/* gem-priced try-ons buy in the premium colour, not the chips colour */
#gpv-buy.gems {
  color: #f6efff;
  background: linear-gradient(160deg, #b98aff 0%, #8f45ff 50%, #4a1c96 100%);
  border-color: rgba(215, 185, 255, 0.8);
  box-shadow: 0 0 18px rgba(150, 90, 255, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.3);
}

/* a wallet that cannot cover the price says so instead of pretending */
#gpv-buy.broke {
  color: rgba(226, 214, 190, 0.75);
  background: rgba(38, 30, 16, 0.85);
  border-color: rgba(255, 179, 0, 0.35);
  box-shadow: none;
}

/* bundle-only items (they arrive WITH a car) can be tried on but not bought
   alone — the commit slot states that instead of offering a dead button */
#gpv-buy.bundled {
  color: rgba(210, 220, 245, 0.8);
  background: rgba(22, 20, 40, 0.8);
  border-color: rgba(150, 165, 205, 0.35);
  box-shadow: none;
  font-size: clamp(8px, 2.4vw, 9.5px);
  cursor: default;
}

/* the card being tried on: amber ring, so PREVIEWING never reads as OWNED */
.gcard.previewing {
  border-color: rgba(255, 179, 0, 0.9);
  background: linear-gradient(180deg, rgba(255, 179, 0, 0.12), rgba(24, 16, 6, 0.72));
  box-shadow: 0 0 18px rgba(255, 179, 0, 0.4), inset 0 0 13px rgba(255, 179, 0, 0.12);
}

.gtag.preview {
  color: #241500;
  padding: 2px 6px;
  border-radius: 5px;
  background: rgba(255, 179, 0, 0.92);
  animation: pvPulse 1.5s ease-in-out infinite alternate;
}

/* swatch card: finish disc + name + state line */
.gcard {
  pointer-events: auto;
  cursor: pointer;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 98px;
  padding: 10px 4px 9px;
  border-radius: 13px;
  background: rgba(20, 16, 40, 0.6);
  border: 1px solid rgba(120, 140, 200, 0.22);
  box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.35);
  transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
}

.gcard:active { transform: scale(0.95); }

.gcard.equipped {
  border-color: rgba(0, 229, 255, 0.85);
  background: linear-gradient(180deg, rgba(0, 229, 255, 0.1), rgba(14, 11, 30, 0.7));
  box-shadow: 0 0 17px rgba(0, 229, 255, 0.35), inset 0 0 13px rgba(0, 229, 255, 0.1);
}

.gswatch {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.25);
  background:
    radial-gradient(circle at 32% 26%, rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0) 30%),
    radial-gradient(circle at 50% 60%, var(--sw), color-mix(in srgb, var(--sw) 40%, #05070f) 88%);
  box-shadow:
    0 0 13px color-mix(in srgb, var(--sw) 55%, transparent),
    inset 0 -5px 9px rgba(0, 0, 0, 0.5);
}

/* glow/trail categories read as light, not paint: hot core + halo ring */
.gswatch.glow, .gswatch.trail {
  background:
    radial-gradient(circle at 50% 50%, #fff 8%, var(--sw) 30%, color-mix(in srgb, var(--sw) 30%, #05070f) 78%);
  box-shadow:
    0 0 17px color-mix(in srgb, var(--sw) 80%, transparent),
    inset 0 0 9px color-mix(in srgb, var(--sw) 70%, transparent);
}

/* rims: accent ring around a dark alloy center */
.gswatch.rim {
  background:
    radial-gradient(circle at 50% 50%, #2a3444 0 38%, var(--sw) 46% 58%, #131a26 66%);
  box-shadow: 0 0 13px color-mix(in srgb, var(--sw) 65%, transparent), inset 0 -4px 8px rgba(0, 0, 0, 0.5);
}

.gcard.locked .gswatch { filter: saturate(0.68) brightness(0.8); }

.gname {
  font-family: var(--display);
  font-weight: 700;
  font-size: 8.5px;
  letter-spacing: 0.1em;
  text-align: center;
  white-space: nowrap;
  color: rgba(235, 242, 255, 0.95);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
}

.gcard.locked .gname { color: rgba(190, 200, 228, 0.72); }

.gtag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 8px;
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
}

.gtag svg { width: 9px; height: 9px; fill: currentColor; }

.gtag.eq {
  color: #9beeff;
  text-shadow: 0 0 8px rgba(0, 229, 255, 0.85);
}

.gtag.own { color: rgba(170, 185, 220, 0.65); }

.gtag.price {
  color: #ffd376;
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.5);
}

.gcard.broke .gtag.price { color: rgba(255, 211, 118, 0.55); text-shadow: none; }

/* can't afford: shake + the price flashes hot red */
.gcard.deny { animation: cardShake 0.4s ease-in-out; }

.gcard.deny .gtag.price {
  color: #ff5f6e;
  text-shadow: 0 0 10px rgba(255, 60, 80, 0.95);
  animation: pulse 0.14s ease-in-out 3;
}

@keyframes cardShake {
  0%, 100% { transform: translateX(0); }
  18% { transform: translateX(-7px); }
  38% { transform: translateX(6px); }
  58% { transform: translateX(-4px); }
  78% { transform: translateX(3px); }
}

/* buy+equip flourish: card pops and rings a cyan pulse outward */
.gcard.bought { animation: cardPop 0.5s cubic-bezier(0.2, 1.9, 0.4, 1); }

.gcard.bought::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 14px;
  border: 2px solid rgba(140, 240, 255, 0.95);
  box-shadow: 0 0 18px rgba(0, 229, 255, 0.8);
  animation: ringOut 0.65s ease-out forwards;
  pointer-events: none;
}

.gcard.justEquipped { animation: cardPop 0.35s cubic-bezier(0.2, 1.9, 0.4, 1); }

@keyframes cardPop {
  0% { transform: scale(1.14); }
  100% { transform: scale(1); }
}

@keyframes ringOut {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(1.22); }
}

/* ------------------------------------------------------------- bonus */
#bonus-screen {
  display: flex;
  flex-direction: column;
}

.bonus-center {
  flex: 1;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: calc(70px + var(--safe-top)) 16px calc(24px + var(--safe-bottom));
  /* same rule as ranks/settings: a mission-heavy board must scroll, not clip */
  max-height: 100%;
  overflow-y: auto;
  touch-action: pan-y;
  -webkit-overflow-scrolling: touch;
}

.bonus-center > #bonus-panel { margin: auto 0; }

#bonus-panel {
  pointer-events: auto;
  width: min(92vw, 400px);
  padding: 20px clamp(16px, 5vw, 26px) 22px;
  border-radius: 20px;
  background: linear-gradient(180deg, rgba(16, 12, 34, 0.78), rgba(10, 7, 22, 0.88));
  border: 1px solid rgba(255, 179, 0, 0.35);
  box-shadow:
    0 0 40px rgba(255, 179, 0, 0.12),
    0 14px 44px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(255, 220, 150, 0.12),
    inset 0 0 34px rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  animation: panelIn 0.45s cubic-bezier(0.2, 1.4, 0.4, 1);
}

/* ONE card component, three rows: the daily streak, the EVENING BONUS and
   the WEEKLY COMMIT share every d-* selector below so the board reads as
   one family — a new row here means extending these groups, not cloning
   them (clones are how the two cards drift apart one tweak at a time) */
#daily-row,
#evening-row,
#commit-row {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  padding: 13px 14px 11px;
  border-radius: 13px;
  border: 1px solid rgba(255, 179, 0, 0.4);
  background: linear-gradient(135deg, rgba(60, 40, 10, 0.5), rgba(30, 18, 6, 0.6));
  box-shadow: inset 0 0 14px rgba(255, 179, 0, 0.08);
}

#evening-row,
#commit-row { margin-top: 10px; }

/* the global .hidden keeps layout space (opacity/visibility — it exists to
   fade layers); a CONTRACT-ABSENT row must leave no ghost gap in the panel,
   same scoped display:none the prestige badge uses */
#evening-row.hidden,
#commit-row.hidden { display: none; }

/* headline strip of the card (icon / title+sub / CLAIM or receipt) — the
   7-day streak ladder renders underneath as .streak-row */
#daily-row .d-top,
#evening-row .d-top,
#commit-row .d-top {
  display: flex;
  align-items: center;
  /* WRAPS instead of overflowing. Un-wrapped, the nowrap title + countdown
     kept their intrinsic width, the row ran past the card, and the CLAIMED
     receipt's box came to rest ON TOP of '+650 TMRW · 9H' at 360px. Clamping
     the text alone fixed the collision but chopped the title to 'DAILY ST…';
     letting the receipt drop to its own right-aligned line keeps every word. */
  flex-wrap: wrap;
  gap: 8px 12px;
}

/* the row's elastic column: 150px is the width 'DAILY STREAK' needs, so the
   receipt wraps below rather than squeezing the title into an ellipsis */
#daily-row .d-main,
#evening-row .d-main,
#commit-row .d-main {
  flex: 1 1 150px;
  min-width: 0;
  overflow: hidden;
}

/* Superseded by #daily-row .d-main above. This selector out-specifies the
   class rule (id + 2 pseudo/class + element), and its `flex: 1 1 0` basis
   pinned the headline column to zero — which is why the receipt never
   wrapped and 'DAILY STREAK' rendered as 'DAILY ST…' at 360px. */

/* ---- 7-day streak ladder: one pip per rung of STREAK_VALUES. Banked days
   check off amber, TODAY burns bright, TMRW carries a cyan preview ring,
   the tail stays dim — the whole escalation is legible at a glance. */
.streak-row {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
}

.spip {
  position: relative;   /* anchors the chip-claimed check overlay */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 5px 0 4px;
  border-radius: 8px;
  border: 1px solid rgba(120, 140, 200, 0.16);
  background: rgba(14, 12, 28, 0.45);
}

.spip .sp-day {
  font-family: var(--display);
  font-weight: 700;
  font-size: 5.5px;
  letter-spacing: 0.08em;
  color: rgba(170, 182, 214, 0.55);
  white-space: nowrap;
}

.spip .sp-amt {
  font-family: var(--display);
  font-weight: 800;
  font-size: 7.5px;
  font-variant-numeric: tabular-nums;
  color: rgba(210, 220, 245, 0.75);
  white-space: nowrap;
}

/* THREE EXPLICIT CHIP STATES (ui.js renderBonus derives them from the
   claimed index). Past, today and future used to render near-identically;
   now each state is unmistakable at a glance. */

/* CLAIMED: banked rung, dimmed fill, 60% presence, small check overlay */
.spip.chip-claimed {
  border-color: rgba(255, 190, 70, 0.3);
  background: rgba(48, 34, 12, 0.4);
  opacity: 0.6;
}

.spip.chip-claimed::after {
  content: '\2713';
  position: absolute;
  top: 0;
  right: 2px;
  font-size: 6px;
  font-weight: 700;
  line-height: 1.6;
  color: #ffd678;
}

.spip.chip-claimed .sp-day { color: rgba(255, 214, 120, 0.7); }
.spip.chip-claimed .sp-amt { color: rgba(255, 214, 120, 0.8); }

/* TODAY: the ONLY saturated fill on the row, hot gold, the live rung */
.spip.chip-today {
  border-color: rgba(255, 190, 70, 0.85);
  background: rgba(64, 44, 14, 0.6);
  box-shadow: 0 0 10px rgba(255, 179, 0, 0.35), inset 0 0 8px rgba(255, 179, 0, 0.12);
}

.spip.chip-today .sp-day { color: #ffe9c0; }
.spip.chip-today .sp-amt { color: #ffe9c0; text-shadow: 0 0 8px rgba(255, 179, 0, 0.7); }

/* today, already claimed: keeps the gold identity but reads as a receipt
   (check overlay from chip-claimed, heat off, presence dropped) */
.spip.chip-today.chip-claimed {
  opacity: 0.75;
  box-shadow: none;
  border-color: rgba(255, 190, 70, 0.5);
}

/* FUTURE: neutral outline, muted text, promised but not yet in play */
.spip.chip-future {
  border-color: rgba(120, 140, 200, 0.22);
  background: rgba(14, 12, 28, 0.3);
}

.spip.chip-future .sp-day { color: rgba(150, 162, 194, 0.6); }
.spip.chip-future .sp-amt { color: rgba(170, 182, 214, 0.6); }

/* CLAIM-ready: the whole row is the tap target (game.js grants on tap) and
   it breathes like a live button instead of posing as a status line */
#daily-row.ready,
#evening-row.ready,
#commit-row.ready {
  cursor: pointer;
  border-color: rgba(255, 179, 0, 0.65);
  animation: dailyReady 1.6s ease-in-out infinite;
}

@keyframes dailyReady {
  0%, 100% { box-shadow: 0 0 10px rgba(255, 179, 0, 0.15), inset 0 0 14px rgba(255, 179, 0, 0.08); }
  50% { box-shadow: 0 0 26px rgba(255, 179, 0, 0.4), inset 0 0 18px rgba(255, 179, 0, 0.14); }
}

#daily-row.ready:active,
#evening-row.ready:active,
#commit-row.ready:active { filter: brightness(1.25); }

#daily-row.claimed,
#evening-row.claimed,
#commit-row.claimed { opacity: 0.82; }

/* solid amber CLAIM plate: dark text on a hot fill — unmistakably a button */
#daily-row .d-claim,
#evening-row .d-claim,
#commit-row .d-claim {
  margin-left: auto;
  flex: none;
  padding: 12px 19px;
  border-radius: 11px;
  font-family: var(--display);
  font-weight: 900;
  font-size: 12px;
  letter-spacing: 0.16em;
  text-indent: 0.08em;
  color: #231303;
  white-space: nowrap;
  transform: skewX(-6deg);
  background: linear-gradient(165deg, #ffd977 0%, #ffb300 55%, #dd8f00 100%);
  border: 1px solid rgba(255, 233, 180, 0.85);
  box-shadow:
    0 0 18px rgba(255, 179, 0, 0.5),
    0 4px 14px rgba(0, 0, 0, 0.4),
    inset 0 1px 0 rgba(255, 255, 255, 0.55);
  transition: transform 0.12s;
}

#daily-row.ready:active .d-claim,
#evening-row.ready:active .d-claim,
#commit-row.ready:active .d-claim { transform: skewX(-6deg) scale(0.93); }

#daily-row .d-icon,
#evening-row .d-icon,
#commit-row .d-icon {
  width: 34px;
  height: 34px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, rgba(255, 220, 150, 0.35), rgba(90, 60, 10, 0.6));
  border: 1px solid rgba(255, 179, 0, 0.55);
  box-shadow: 0 0 12px rgba(255, 179, 0, 0.35);
}

#daily-row .d-icon svg,
#evening-row .d-icon svg,
#commit-row .d-icon svg { width: 17px; height: 17px; fill: var(--amber); filter: drop-shadow(0 0 5px rgba(255, 179, 0, 0.8)); }

#daily-row .d-title,
#evening-row .d-title,
#commit-row .d-title {
  font-family: var(--display);
  font-weight: 800;
  font-size: 12px;
  letter-spacing: 0.12em;
  color: #ffe9c0;
  text-shadow: 0 0 9px rgba(255, 179, 0, 0.5);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#daily-row .d-sub,
#evening-row .d-sub,
#commit-row .d-sub {
  margin-top: 3px;
  display: flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  font-family: var(--display);
  font-weight: 600;
  font-size: 8px;
  letter-spacing: 0.1em;
  color: rgba(200, 190, 165, 0.7);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* d-sub's inline clock glyph (post-claim NEXT +500 countdown) */
#daily-row .d-sub svg,
#evening-row .d-sub svg,
#commit-row .d-sub svg { width: 9px; height: 9px; flex: none; }

/* claimed: checked amber CLAIMED +500 receipt — the same claimed language
   as the mission rows' chip; clearly NOT a button */
#daily-row .d-state,
#evening-row .d-state,
#commit-row .d-state {
  margin-left: auto;
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 6px 8px;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 8.5px;
  letter-spacing: 0.09em;
  white-space: nowrap;
  color: rgba(190, 202, 232, 0.75);
  background: rgba(20, 18, 38, 0.55);
  border: 1px solid rgba(120, 140, 200, 0.25);
}

#daily-row .d-state svg,
#evening-row .d-state svg,
#commit-row .d-state svg { width: 11px; height: 11px; flex: none; }

#daily-row.claimed .d-state,
#evening-row.claimed .d-state,
#commit-row.claimed .d-state {
  color: #ffd678;
  border-color: rgba(255, 190, 70, 0.4);
  background: rgba(48, 34, 12, 0.5);
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.4);
}

/* ---- EVENING BONUS row states. ARMED is a statement, not a button: the
   card keeps its chrome but drops presence, so the one hot thing on an
   armed board stays the daily CLAIM above it. READY/CLAIMED ride the
   shared .ready/.claimed rules with #daily-row. */
#evening-row.armed { opacity: 0.62; }

/* ---- WEEKLY COMMIT row. The amber accent hooks feed the shared .m-bar /
   .m-val components inside the row, so its progress lane matches a DONE
   mission card's amber rather than defaulting to mission cyan. */
#commit-row { --mc: #ffb300; --mc-rgb: 255, 179, 0; }

/* the three commit plates: one tap commits (free promise, no confirm).
   44px min-height is the touch-target floor for every NEW control; the
   plates split the card width evenly so no option reads as the default. */
.wc-opts {
  display: flex;
  gap: 8px;
}

.wc-opt {
  pointer-events: auto;
  cursor: pointer;
  flex: 1 1 0;
  min-width: 0;
  min-height: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  border-radius: 11px;
  font-family: var(--display);
  color: #ffd376;
  background: rgba(52, 34, 8, 0.5);
  border: 1px solid rgba(255, 179, 0, 0.35);
  box-shadow: inset 0 1px 0 rgba(255, 220, 150, 0.1);
  transition: transform 0.12s, filter 0.12s;
}

.wc-opt b {
  font-weight: 900;
  font-size: 15px;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.45);
}

/* face/weight/tracking from the TYPE PAIR labels group (end of sheet) */
.wc-opt small {
  font-size: 6.5px;
  color: rgba(255, 214, 120, 0.7);
}

.wc-opt:active {
  transform: scale(0.95);
  filter: brightness(1.3);
}

.bonus-sub {
  margin: 18px 2px 8px;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-family: var(--display);
  font-weight: 700;
  font-size: 9px;
  letter-spacing: 0.22em;
  color: rgba(200, 212, 240, 0.8);
}

.bsub-count {
  font-size: 8px;
  letter-spacing: 0.14em;
  color: rgba(155, 238, 255, 0.75);
  text-shadow: 0 0 7px rgba(0, 229, 255, 0.4);
}

/* ---- mission cards: per-stat accent slots, not a settings list.
   Accent variables keyed off the tracked stat (ui.js sets data-stat) —
   shared by the bonus board, the WRECKED strip (.mcard.compact) and the
   in-run tracker chip, so a stat's color means ONE thing everywhere:
   chevrons/cyan = near miss, arc/magenta = drift, gauge/amber = speed */
.mcard[data-stat="nearmiss"], #mission-tracker[data-stat="nearmiss"] { --mc: #00e5ff; --mc-rgb: 0, 229, 255; }
.mcard[data-stat="drift"],    #mission-tracker[data-stat="drift"]    { --mc: #ff2d95; --mc-rgb: 255, 45, 149; }
.mcard[data-stat="speed"],    #mission-tracker[data-stat="speed"]    { --mc: #ffb300; --mc-rgb: 255, 179, 0; }
.mcard[data-stat="combo"],    #mission-tracker[data-stat="combo"]    { --mc: #ff8a45; --mc-rgb: 255, 120, 55; }
.mcard[data-stat="dist"],     #mission-tracker[data-stat="dist"]     { --mc: #a06bff; --mc-rgb: 150, 95, 255; }

.mcard {
  --mc: #00e5ff;
  --mc-rgb: 0, 229, 255;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px;
  border-radius: 13px;
  background: linear-gradient(135deg, rgba(var(--mc-rgb), 0.10), rgba(15, 12, 32, 0.65) 58%);
  border: 1px solid rgba(var(--mc-rgb), 0.28);
  box-shadow: inset 0 0 16px rgba(0, 0, 0, 0.35);
}

.mcard + .mcard { margin-top: 8px; }

/* 36px icon well: accent-tinted plate carrying the stat glyph */
.m-icon {
  width: 36px;
  height: 36px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  color: var(--mc);
  background: radial-gradient(circle at 32% 26%,
    rgba(var(--mc-rgb), 0.32), rgba(var(--mc-rgb), 0.08) 78%);
  border: 1px solid rgba(var(--mc-rgb), 0.45);
  box-shadow: 0 0 12px rgba(var(--mc-rgb), 0.22), inset 0 0 9px rgba(var(--mc-rgb), 0.12);
}

.m-icon svg {
  width: 19px;
  height: 19px;
  display: block;
  filter: drop-shadow(0 0 4px rgba(var(--mc-rgb), 0.8));
}

.m-main { flex: 1 1 0; min-width: 0; }

.m-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.m-label {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(8.5px, 2.6vw, 10.5px);
  letter-spacing: 0.08em;
  color: rgba(235, 242, 255, 0.95);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.m-reward {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: none;
  font-family: var(--display);
  font-weight: 800;
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: #ffd376;
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.5);
}

.m-reward svg { width: 10px; height: 10px; fill: currentColor; }

.m-meta {
  margin-top: 7px;
  display: flex;
  align-items: center;
  gap: 9px;
}

/* trough: NON-emissive neutral, recessed by an inner shadow — at 0 progress
   it must read empty. Only the fill span carries the accent + glow (the old
   lit-cyan track made 0/5 read as complete at 2x DPR). */
.m-bar {
  flex: 1;
  height: 6px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(120, 140, 180, 0.15);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.65);
  overflow: hidden;
}

.m-bar span {
  display: block;
  height: 100%;
  border-radius: 3px;
  background: linear-gradient(90deg,
    var(--mc, #00b8e0),
    color-mix(in srgb, var(--mc, #a8f2ff) 55%, #ffffff));
  box-shadow: 0 0 8px rgba(var(--mc-rgb, 0, 229, 255), 0.85);
  transform-origin: left;
}

/* x/y value chip: amber pill so the readout is a scoreboard, not a decimal */
.m-val {
  flex: none;
  padding: 3px 8px;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 8.5px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
  white-space: nowrap;
  text-align: center;
  min-width: 4.5ch;
  color: #ffd376;
  text-shadow: 0 0 7px rgba(255, 179, 0, 0.4);
  background: rgba(52, 34, 8, 0.5);
  border: 1px solid rgba(255, 179, 0, 0.3);
}

/* COMPLETED mission card: target cleared this run — full amber bar, lit
   state chip, reward marked banked. A finished goal must never sit behind
   a stale partial bar. */
.mcard.done {
  border-color: rgba(255, 179, 0, 0.45);
  box-shadow: 0 0 18px rgba(255, 179, 0, 0.12), inset 0 0 12px rgba(255, 179, 0, 0.06);
}

.mcard.done .m-bar span {
  background: linear-gradient(90deg, #c77f00, var(--amber), #ffd376);
  box-shadow: 0 0 9px rgba(255, 179, 0, 0.7);
}

.mcard.done .m-val {
  color: #180f02;
  background: linear-gradient(180deg, #ffc63d, #f0a400);
  border-color: rgba(255, 214, 120, 0.9);
  text-shadow: none;
}

.mcard.done .m-reward {
  color: #ffd376;
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.6);
}

/* CLAIM-pending mission card: target cleared, reward waiting on the tap.
   The card breathes and the state chip is a PULSING solid-amber button —
   the same plate language as the daily CLAIM, sized for the mission row. */
.mcard.claimable {
  border-color: rgba(255, 195, 40, 0.65);
  animation: mcardBreathe 1.6s ease-in-out infinite;
}

@keyframes mcardBreathe {
  0%, 100% { box-shadow: 0 0 12px rgba(255, 179, 0, 0.12), inset 0 0 10px rgba(255, 179, 0, 0.05); }
  50% { box-shadow: 0 0 26px rgba(255, 179, 0, 0.32), inset 0 0 14px rgba(255, 179, 0, 0.1); }
}

.m-claim {
  flex: none;
  padding: 6px 15px;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 900;
  font-size: 10px;
  letter-spacing: 0.14em;
  text-indent: 0.07em;
  color: #231303;
  white-space: nowrap;
  cursor: pointer;
  background: linear-gradient(165deg, #ffd977 0%, #ffb300 55%, #dd8f00 100%);
  border: 1px solid rgba(255, 233, 180, 0.85);
  box-shadow:
    0 0 14px rgba(255, 179, 0, 0.55),
    0 3px 10px rgba(0, 0, 0, 0.4),
    inset 0 1px 0 rgba(255, 255, 255, 0.55);
  animation: claimPulse 1.1s ease-in-out infinite;
  transition: transform 0.12s;
}

.m-claim:active { transform: scale(0.92); }

@keyframes claimPulse {
  0%, 100% { transform: scale(1); box-shadow: 0 0 10px rgba(255, 179, 0, 0.4), 0 3px 10px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.55); }
  50% { transform: scale(1.07); box-shadow: 0 0 24px rgba(255, 190, 30, 0.85), 0 3px 12px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.6); }
}

/* banked: checked CLAIMED chip (post-tap resting state) */
.m-val.m-claimed {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: #180f02;
  background: linear-gradient(180deg, #ffc63d, #f0a400);
  border-color: rgba(255, 214, 120, 0.9);
  text-shadow: none;
}

.m-val.m-claimed svg {
  width: 9px;
  height: 9px;
  flex: none;
}

.mcard.justClaimed {
  animation: none;
  border-color: rgba(255, 179, 0, 0.45);
}

/* ▲ delta marker on the progress numeral: THIS run moved the bar */
.m-val.up::before { content: '\25B2 '; color: #ffd376; }

/* claim deadline on a banked-but-uncollected row: quotes the SAME
   hoursUntilRefresh clock as the board-refresh footer */
.m-expire {
  margin-top: 5px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 6.5px;
  letter-spacing: 0.16em;
  color: rgba(255, 200, 110, 0.85);
  text-shadow: 0 0 6px rgba(255, 179, 0, 0.35);
}

.m-expire svg { width: 8px; height: 8px; flex: none; }

/* gem-priced instant re-roll on LIVE rows (bonus board only): tiny violet
   pill — reroll arrows + gem price — beside the progress numeral */
.m-reroll {
  pointer-events: auto;
  cursor: pointer;
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 4px 7px;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 800;
  font-size: 8px;
  color: #f3ecff;
  background: linear-gradient(160deg, #a06bff 0%, #7a34e8 55%, #47188f 100%);
  border: 1px solid rgba(210, 180, 255, 0.6);
  box-shadow: 0 0 8px rgba(150, 90, 255, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.3);
  transition: transform 0.1s, filter 0.12s;
}

.m-reroll:active { transform: scale(0.92); filter: brightness(1.2); }

.m-reroll svg { width: 9px; height: 9px; flex: none; fill: #fff; }
.m-reroll .stroke-ico { fill: none; stroke: #fff; }
.m-reroll svg .gem-core { fill: #4a2a80; }

/* re-roll outcomes on the card itself */
.mcard.deny { animation: cardShake 0.4s ease-in-out; }
.mcard.justRolled { animation: cardPop 0.5s cubic-bezier(0.2, 1.9, 0.4, 1); }

/* shared qualifier caption — 'in one run' stated once per surface (bonus
   board header slot + WRECKED strip header) so row labels stay tight */
.m-caption {
  font-family: var(--display);
  font-weight: 700;
  font-size: 7.5px;
  letter-spacing: 0.2em;
  text-indent: 0.1em;
  color: rgba(170, 185, 220, 0.62);
}

#mission-caption { margin: -3px 2px 9px; }

/* COMPACT variant of the same card for the WRECKED strip: identical
   anatomy (accent icon well, label + reward, bar, state chip), tightened
   to sit between the payout line and the stat row */
.mcard.compact { padding: 7px 9px; gap: 9px; border-radius: 11px; }
.mcard.compact + .mcard.compact { margin-top: 6px; }
.mcard.compact .m-icon { width: 26px; height: 26px; border-radius: 8px; }
.mcard.compact .m-icon svg { width: 15px; height: 15px; }
.mcard.compact .m-label { font-size: 8.5px; }
.mcard.compact .m-reward { font-size: 9px; }
.mcard.compact .m-reward svg { width: 9px; height: 9px; }
.mcard.compact .m-meta { margin-top: 5px; gap: 7px; }
.mcard.compact .m-bar { height: 4px; border-radius: 2px; }
.mcard.compact .m-val { font-size: 8px; padding: 2px 7px; }
.mcard.compact .m-claim { font-size: 8.5px; padding: 5px 13px; }

/* board-refresh footer off the meta.js rotation clock */
#mission-foot {
  margin-top: 13px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 8px;
  letter-spacing: 0.2em;
  text-indent: 0.1em;
  color: rgba(170, 185, 220, 0.6);
}

#mission-foot svg { width: 11px; height: 11px; flex: none; }

/* -------------------------------------------------------------- toast */
#toasts {
  position: fixed;
  top: calc(72px + var(--safe-top));
  left: 0; right: 0;
  z-index: 45;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  pointer-events: none;
}

.toast {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 24px;
  font-family: var(--display);
  font-weight: 800;
  font-size: 13px;
  letter-spacing: 0.16em;
  text-indent: 0.08em;
  color: #ffe9c0;
  text-shadow: 0 0 10px rgba(255, 179, 0, 0.6);
  background: linear-gradient(135deg, rgba(52, 34, 8, 0.85), rgba(24, 14, 6, 0.9));
  border: 1px solid rgba(255, 179, 0, 0.6);
  border-radius: 999px;
  box-shadow: 0 0 22px rgba(255, 179, 0, 0.35), 0 8px 26px rgba(0, 0, 0, 0.5), inset 0 0 12px rgba(255, 179, 0, 0.1);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  animation: toastIn 0.4s cubic-bezier(0.2, 1.6, 0.4, 1), toastOut 0.45s ease-in 2.3s forwards;
}

.toast svg { width: 14px; height: 14px; flex: none; fill: var(--amber); filter: drop-shadow(0 0 5px rgba(255, 179, 0, 0.8)); }

@keyframes toastIn {
  from { transform: translateY(-26px) scale(0.85); opacity: 0; }
  to   { transform: translateY(0) scale(1); opacity: 1; }
}

@keyframes toastOut {
  to { transform: translateY(-16px) scale(0.92); opacity: 0; }
}

/* mission-complete flyer (mid-run, popup container) */
.popup.mission {
  font-size: clamp(15px, 4.4vw, 22px);
  color: #9beeff;
  text-shadow: 0 0 14px rgba(0, 229, 255, 0.9), 0 2px 10px rgba(0, 0, 0, 0.7);
}

/* -------------------------------------------- game-over payout lines */
#over-chips {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  padding: 8px 18px;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(13px, 3.9vw, 16px);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.08em;
  color: #ffe9c0;
  text-shadow: 0 0 10px rgba(255, 179, 0, 0.55);
  background: linear-gradient(135deg, rgba(52, 34, 8, 0.55), rgba(24, 14, 6, 0.65));
  border: 1px solid rgba(255, 179, 0, 0.5);
  border-radius: 999px;
  box-shadow: 0 0 16px rgba(255, 179, 0, 0.2), inset 0 0 10px rgba(255, 179, 0, 0.08);
}

#over-chips.hidden { display: none; }

#over-chips svg { width: 14px; height: 14px; flex: none; fill: var(--amber); filter: drop-shadow(0 0 5px rgba(255, 179, 0, 0.8)); }

#over-chips svg .chip-core { fill: #5a3c05; }

/* WRECKED-card mission strip: shared .mcard.compact rows (ui.js
   missionRowEl) under the shared caption line */
#over-missions:not(:empty) { margin-top: 12px; text-align: left; }

#over-missions .m-caption {
  margin-bottom: 6px;
  text-align: center;
}

#over-missions .mcard.compact + .mcard.compact { margin-top: 5px; }

/* ------------------------------------------------------- pause sheet */
/* game.js holds the sim frozen in STATE.PAUSED behind this; the scrim dims
   and defocuses the live frame so RESUME is the only hot thing on screen */
#pause-screen {
  display: flex;
  align-items: center;
  justify-content: center;
}

#pause-screen::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 42%,
    rgba(8, 6, 18, 0.5), rgba(6, 4, 14, 0.8));
  backdrop-filter: blur(6px) brightness(0.85) saturate(0.9);
  -webkit-backdrop-filter: blur(6px) brightness(0.85) saturate(0.9);
}

.pause-panel {
  position: relative;
  pointer-events: auto;
  text-align: center;
  width: min(84vw, 330px);
  padding: 30px clamp(20px, 6vw, 30px) 24px;
  border-radius: 22px;
  background: rgba(14, 10, 30, 0.78);
  border: 1px solid rgba(0, 229, 255, 0.35);
  box-shadow:
    0 0 60px rgba(0, 229, 255, 0.16),
    0 18px 50px rgba(0, 0, 0, 0.55),
    inset 0 0 40px rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  animation: panelIn 0.4s cubic-bezier(0.2, 1.4, 0.4, 1);
}

.pause-label {
  font-family: var(--display);
  font-size: clamp(22px, 6.6vw, 30px);
  font-weight: 900;
  font-style: italic;
  letter-spacing: 0.2em;
  text-indent: 0.2em;
  color: #fff;
  text-shadow: 0 0 18px rgba(0, 229, 255, 0.85), 0 0 44px rgba(0, 229, 255, 0.4);
}

#pause-score {
  margin-top: 9px;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(11px, 3.2vw, 14px);
  letter-spacing: 0.22em;
  font-variant-numeric: tabular-nums;
  color: rgba(155, 238, 255, 0.85);
  text-shadow: 0 0 10px rgba(0, 229, 255, 0.4);
}

.pause-buttons {
  margin-top: 22px;
  display: flex;
  flex-direction: column;
  gap: 11px;
}

.pause-buttons button {
  pointer-events: auto;
  cursor: pointer;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(12px, 3.5vw, 15px);
  letter-spacing: 0.16em;
  text-indent: 0.16em;
  color: #fff;
  border: none;
  border-radius: 13px;
  transform: skewX(-6deg);
  transition: transform 0.12s, filter 0.12s;
}

.pause-buttons button:active { transform: skewX(-6deg) scale(0.94); filter: brightness(1.3); }

#pause-resume-btn {
  min-height: 54px;
  font-weight: 900;
  letter-spacing: 0.26em;
  text-indent: 0.26em;
  background: linear-gradient(160deg, #ff4da6 0%, #ff2d95 42%, #99114f 100%);
  border: 1px solid rgba(255, 170, 215, 0.75);
  box-shadow:
    0 0 26px rgba(255, 45, 149, 0.45),
    inset 0 1px 0 rgba(255, 255, 255, 0.35),
    inset 0 0 16px rgba(255, 120, 190, 0.22);
}

.pause-row {
  display: flex;
  gap: 11px;
}

.pause-row button {
  flex: 1 1 0;
  min-height: 46px;
  font-size: clamp(10px, 3vw, 12px);
}

#pause-restart-btn {
  color: #d8f6ff;
  text-shadow: 0 0 8px rgba(0, 229, 255, 0.55);
  background: rgba(8, 14, 30, 0.6);
  border: 1px solid rgba(0, 229, 255, 0.5);
  box-shadow: 0 0 14px rgba(0, 229, 255, 0.18), inset 0 0 12px rgba(0, 229, 255, 0.07);
}

#pause-quit-btn {
  color: rgba(205, 218, 245, 0.85);
  background: rgba(18, 16, 36, 0.6);
  border: 1px solid rgba(120, 140, 200, 0.3);
  box-shadow: inset 0 0 12px rgba(0, 0, 0, 0.4);
}

/* ================================================= RANKED + STORE SPINE */
/* ------------------------------------------- dual-currency wallet pill */
/* the ONE wallet pill grows a violet GEMS segment: chips | divider | gems.
   Same geometry everywhere (hub / headers / results). */
.wallet-div {
  width: 1px;
  height: 15px;
  flex: none;
  margin: 0 2px;
  background: linear-gradient(180deg, transparent, rgba(210, 200, 255, 0.35), transparent);
}

.chips-pill .gem-ico {
  fill: #b06bff;
  filter: drop-shadow(0 0 5px rgba(160, 100, 255, 0.85));
}

.chips-pill .gem-ico .gem-core { fill: #4a2a80; }

.chips-pill .gems-num {
  color: #e2d2ff;
  text-shadow: 0 0 10px rgba(160, 100, 255, 0.6), 0 1px 4px rgba(0, 0, 0, 0.8);
}


/* ------------------------------------------------- hub CTA variants */
#ranks-btn {
  color: #ffd9ec;
  text-shadow: 0 0 8px rgba(255, 45, 149, 0.55);
  border-color: rgba(255, 45, 149, 0.55);
  background: rgba(32, 8, 22, 0.55);
  box-shadow: 0 0 16px rgba(255, 45, 149, 0.2), 0 4px 18px rgba(0, 0, 0, 0.45), inset 0 0 12px rgba(255, 45, 149, 0.08);
}

#ranks-btn svg { filter: drop-shadow(0 0 4px rgba(255, 45, 149, 0.6)); }

#store-btn {
  color: #e6d6ff;
  text-shadow: 0 0 8px rgba(160, 100, 255, 0.55);
  border-color: rgba(160, 100, 255, 0.55);
  background: rgba(22, 12, 40, 0.55);
  box-shadow: 0 0 16px rgba(160, 100, 255, 0.2), 0 4px 18px rgba(0, 0, 0, 0.45), inset 0 0 12px rgba(160, 100, 255, 0.08);
}

#store-btn svg { filter: drop-shadow(0 0 4px rgba(160, 100, 255, 0.6)); }

/* ------------------------------------------------- prestige tier badge */
/* shield chip: hub (under the wallet), ladder rows, results card. Tier
   color rides --pt / --pt-rgb set by the tier-* class. */
.pbadge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px 5px 9px;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 9.5px;
  letter-spacing: 0.14em;
  text-indent: 0.05em;
  color: var(--pt, #c8d2e4);
  text-shadow: 0 0 8px rgba(var(--pt-rgb, 200, 210, 228), 0.6), 0 1px 3px rgba(0, 0, 0, 0.8);
  background: linear-gradient(135deg, rgba(var(--pt-rgb, 200, 210, 228), 0.14), rgba(10, 8, 20, 0.72));
  border: 1px solid rgba(var(--pt-rgb, 200, 210, 228), 0.5);
  box-shadow: 0 0 14px rgba(var(--pt-rgb, 200, 210, 228), 0.16), inset 0 0 10px rgba(0, 0, 0, 0.35);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.pbadge svg {
  width: 12px;
  height: 12px;
  flex: none;
  fill: var(--pt, #c8d2e4);
  filter: drop-shadow(0 0 4px rgba(var(--pt-rgb, 200, 210, 228), 0.8));
}

.pbadge.tier-bronze { --pt: #e09a5c; --pt-rgb: 224, 154, 92; }
.pbadge.tier-silver { --pt: #ccd6e8; --pt-rgb: 204, 214, 232; }
.pbadge.tier-gold   { --pt: #ffd35e; --pt-rgb: 255, 211, 94; }
.pbadge.tier-neon   { --pt: #ff2d95; --pt-rgb: 255, 45, 149; }

/* hub placement: status block under the top-right wallet */
#hub-prestige {
  position: absolute;
  top: calc(58px + var(--safe-top));
  right: 14px;
}

/* results card placement: centered chip between BEST and the payout */
#over-prestige {
  margin: 8px auto 0;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* --------------------------------------------------------- ranks panel */
#ranks-screen {
  display: flex;
  flex-direction: column;
}

.ranks-center {
  flex: 1;
  display: flex;
  /* flex-start + auto margins: the board centres while it fits and yields to
     scrolling when it doesn't. Centring a too-tall panel pushed the bottom
     four ladder rows off-screen at 360x780 with no way to reach them. */
  align-items: flex-start;
  justify-content: center;
  padding: calc(66px + var(--safe-top)) 14px calc(20px + var(--safe-bottom));
  max-height: 100%;
  overflow-y: auto;
  touch-action: pan-y;
  -webkit-overflow-scrolling: touch;
}

.ranks-center > #ranks-panel { margin: auto 0; }

#ranks-panel {
  pointer-events: auto;
  width: min(94vw, 410px);
  padding: 16px clamp(13px, 4vw, 20px) 15px;
  border-radius: 20px;
  background: linear-gradient(180deg, rgba(16, 12, 34, 0.8), rgba(10, 7, 22, 0.88));
  border: 1px solid rgba(255, 45, 149, 0.35);
  box-shadow:
    0 0 40px rgba(255, 45, 149, 0.12),
    0 14px 44px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(255, 170, 215, 0.12),
    inset 0 0 34px rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  animation: panelIn 0.45s cubic-bezier(0.2, 1.4, 0.4, 1);
}

.ranks-head { text-align: center; margin-bottom: 11px; }

.ranks-title {
  font-family: var(--display);
  font-weight: 900;
  font-style: italic;
  font-size: 13.5px;
  letter-spacing: 0.22em;
  text-indent: 0.11em;
  color: #ffd9ec;
  text-shadow: 0 0 14px rgba(255, 45, 149, 0.65), 0 1px 4px rgba(0, 0, 0, 0.8);
}

/* rewards rule-line: the ladder's economy stated once, quietly */
.ranks-sub {
  margin-top: 5px;
  font-family: var(--display);
  font-weight: 500;
  font-size: 8px;
  letter-spacing: 0.2em;
  color: rgba(255, 220, 170, 0.62);
}

/* 5.1.2 disclosure. Deliberately larger and brighter than .ranks-sub above it:
   this exists to be read by a stranger opening the screen for the first time,
   which is exactly the case that failed review. */
.ranks-legal {
  margin-top: 6px;
  font-family: var(--display);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: 0.14em;
  color: rgba(245, 245, 247, 0.8);
}

#ranks-tabs {
  display: flex;
  gap: 6px;
  margin-bottom: 10px;
}

.rtab {
  pointer-events: auto;
  cursor: pointer;
  flex: 1 1 0;
  min-height: 38px;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(9px, 2.5vw, 10.5px);
  letter-spacing: 0.12em;
  text-indent: 0.12em;
  color: rgba(175, 195, 230, 0.68);
  background: rgba(22, 18, 44, 0.55);
  border: 1px solid rgba(120, 140, 200, 0.22);
  border-radius: 9px;
  transform: skewX(-6deg);
  transition: color 0.15s, border-color 0.15s, box-shadow 0.15s, background 0.15s;
}

.rtab.active {
  color: #fff;
  text-shadow: 0 0 9px rgba(255, 45, 149, 0.85);
  border-color: rgba(255, 45, 149, 0.75);
  background: linear-gradient(180deg, rgba(255, 45, 149, 0.17), rgba(120, 15, 70, 0.2));
  box-shadow: 0 0 15px rgba(255, 45, 149, 0.32), inset 0 0 11px rgba(255, 45, 149, 0.13);
}

/* ladder rows: rank plate / racer / tier shield / score */
.lrow {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 10px 7px 8px;
  border-radius: 11px;
  background: rgba(20, 16, 40, 0.45);
  border: 1px solid rgba(120, 140, 200, 0.16);
}

.lrow + .lrow { margin-top: 5px; }

.l-rank {
  flex: none;
  width: 30px;
  font-family: var(--display);
  font-weight: 900;
  font-style: italic;
  font-size: 12px;
  text-align: center;
  color: rgba(170, 185, 220, 0.75);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}

/* podium plates: brightness steps in the SAME steel family as the tier
   shields, hierarchy without adding a third and fourth hue to a screen
   whose accents are already spoken for (pink = you, cyan = next target,
   gold = currency) */
.lrow[data-rank="1"] .l-rank { color: #e9eef8; text-shadow: 0 0 9px rgba(220, 230, 248, 0.55); }
.lrow[data-rank="2"] .l-rank { color: #c2cddf; text-shadow: 0 0 9px rgba(194, 205, 223, 0.45); }
.lrow[data-rank="3"] .l-rank { color: #9aa7bd; text-shadow: 0 0 9px rgba(154, 167, 189, 0.4); }

.l-name {
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--display);
  font-weight: 700;
  font-size: 10.5px;
  letter-spacing: 0.1em;
  color: rgba(225, 232, 250, 0.9);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}

/* row-level tier shield: icon-only version of the pbadge */
.l-tier {
  flex: none;
  display: inline-flex;
  width: 15px;
  height: 15px;
}

.l-tier svg {
  width: 100%;
  height: 100%;
  fill: var(--pt, #c8d2e4);
  filter: drop-shadow(0 0 4px rgba(var(--pt-rgb, 200, 210, 228), 0.35));
}

/* explicit UNRANKED slot: dimmed outline shield — every row carries the
   prestige column, so a missing tier reads as a state, not a layout gap */
.l-tier.unranked svg {
  fill: none;
  stroke: rgba(150, 165, 205, 0.45);
  stroke-width: 1.8;
  filter: none;
  opacity: 0.8;
}

/* ACCENT DISCIPLINE: in-row shields are NEUTRAL STEEL, one hue family at
   four brightnesses (dark bronze -> bright neon). The board stops running
   bronze/silver/gold/pink shields against gold currency, pink YOU and cyan
   NEXT all at once; gold in a row now always means chips. The full metal
   identities survive where the tier IS the subject (#ranks-tiers strip,
   pbadges). */
.l-tier.tier-bronze { --pt: #77839a; --pt-rgb: 119, 131, 154; }
.l-tier.tier-silver { --pt: #9aa7bd; --pt-rgb: 154, 167, 189; }
.l-tier.tier-gold   { --pt: #bcc8db; --pt-rgb: 188, 200, 219; }
.l-tier.tier-neon   { --pt: #e0e8f5; --pt-rgb: 224, 232, 245; }

.l-score {
  flex: none;
  font-family: var(--display);
  font-weight: 700;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  /* neutral: cyan belongs to the NEXT marker alone on this screen */
  color: #d7e0f0;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}

/* the player's own row: hot magenta plate, unmissable mid-ladder */
.lrow.you {
  background: linear-gradient(135deg, rgba(255, 45, 149, 0.2), rgba(60, 12, 40, 0.5));
  border-color: rgba(255, 45, 149, 0.7);
  box-shadow: 0 0 18px rgba(255, 45, 149, 0.28), inset 0 0 14px rgba(255, 45, 149, 0.1);
  animation: youRow 1.8s ease-in-out infinite alternate;
}

@keyframes youRow {
  from { box-shadow: 0 0 12px rgba(255, 45, 149, 0.2), inset 0 0 12px rgba(255, 45, 149, 0.08); }
  to   { box-shadow: 0 0 24px rgba(255, 45, 149, 0.4), inset 0 0 16px rgba(255, 45, 149, 0.14); }
}

.lrow.you .l-name { color: #fff; text-shadow: 0 0 9px rgba(255, 45, 149, 0.8); }
.lrow.you .l-rank { color: #ffd9ec; }
/* pink-tinted, not amber: the YOU row carries ONE accent, and gold on this
   board is reserved for currency chips */
.lrow.you .l-score { color: #ffd9ec; text-shadow: 0 0 9px rgba(255, 45, 149, 0.45); }

/* period-record tag inside the player row: 'WEEK BEST' / 'MONTH BEST' /
   'YOUR BEST' — bare 'PB'/'BEST' stays reserved for the all-time record */
.l-you-tag {
  flex: none;
  padding: 2px 7px;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 900;
  font-size: 7px;
  letter-spacing: 0.16em;
  color: #2a0517;
  background: linear-gradient(180deg, #ff7cc0, #ff2d95);
  box-shadow: 0 0 8px rgba(255, 45, 149, 0.6);
}

/* next-target hint row divider: the rival directly above the player */
.lrow.target { border-color: rgba(0, 229, 255, 0.45); }

.lrow.target .l-score::after {
  content: ' \25C0 NEXT';
  font-size: 7px;
  letter-spacing: 0.14em;
  color: rgba(0, 229, 255, 0.85);
  text-shadow: 0 0 6px rgba(0, 229, 255, 0.6);
}

#ranks-foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: 10px;
  font-family: var(--display);
  font-weight: 500;
  font-size: 8.5px;
  letter-spacing: 0.18em;
  color: rgba(170, 185, 220, 0.55);
}

#ranks-foot svg { width: 11px; height: 11px; flex: none; }

/* --------------------------------------------------------- store panel */
#store-screen {
  display: flex;
  flex-direction: column;
}

/* HELD-RUN RESUME STRIP. Only visible while a crash offer is frozen behind
   the sheet (the empty-wallet revive route). The run is still revivable and
   this strip is the way back to it, so the sheet can never read as a
   one-way exit from a run the player is mid-way through paying to save.
   display:none when hidden is load-bearing: the global .hidden is
   opacity+visibility and would leave a 48px hole above every shelf. */
.store-resume {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  width: min(94vw, 410px);
  margin: calc(66px + var(--safe-top)) auto 0;
  padding: 8px 16px;
  min-height: 48px;
  border: 1px solid rgba(255, 45, 149, 0.55);
  border-radius: 10px;
  background: linear-gradient(180deg, rgba(255, 45, 149, 0.22), rgba(255, 45, 149, 0.08));
  color: #ffe6f3;
  font-family: var(--display);
  font-style: italic;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.06em;
  text-align: left;
  animation: resume-pulse 2.4s ease-in-out infinite;
}

.store-resume svg { width: 20px; height: 20px; flex: none; }
.store-resume > span { display: flex; flex-direction: column; gap: 2px; }

.store-resume small {
  font-family: var(--ui-sans);
  font-style: normal;
  font-weight: 600;
  font-size: 10px;
  letter-spacing: 0.04em;
  color: rgba(255, 230, 243, 0.74);
}

.store-resume.hidden { display: none; }
body.reduced-motion .store-resume { animation: none; }

@keyframes resume-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 45, 149, 0); }
  50% { box-shadow: 0 0 16px 0 rgba(255, 45, 149, 0.38); }
}

.store-scroll {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  touch-action: pan-y;
  padding: calc(66px + var(--safe-top)) 14px calc(20px + var(--safe-bottom));
  -webkit-overflow-scrolling: touch;
}

/* the strip already clears the fixed panel-top, so the shelf below it does
   not need to clear it a second time */
#store-screen.has-resume .store-scroll { padding-top: 12px; }

#store-panel {
  pointer-events: auto;
  width: min(94vw, 410px);
  /* auto margins center the sheet when it fits and yield cleanly to
     scrolling when it doesn't */
  margin: auto;
  padding: 14px clamp(12px, 3.6vw, 18px) 14px;
  border-radius: 20px;
  background: linear-gradient(180deg, rgba(16, 12, 34, 0.8), rgba(10, 7, 22, 0.88));
  border: 1px solid rgba(160, 100, 255, 0.38);
  box-shadow:
    0 0 40px rgba(160, 100, 255, 0.13),
    0 14px 44px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(210, 190, 255, 0.12),
    inset 0 0 34px rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  animation: panelIn 0.45s cubic-bezier(0.2, 1.4, 0.4, 1);
}

.store-sub {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin: 13px 2px 8px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: 0.26em;
  color: rgba(215, 200, 255, 0.85);
  text-shadow: 0 0 8px rgba(160, 100, 255, 0.4);
}

.store-sub small {
  font-size: 7.5px;
  letter-spacing: 0.16em;
  color: rgba(170, 185, 220, 0.5);
}

/* shared price plate (the $-tag button on every purchasable tile) */
.price-tag {
  flex: none;
  padding: 8px 13px;
  border-radius: 10px;
  font-family: var(--display);
  font-weight: 900;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: #102006;
  background: linear-gradient(180deg, #b9ffd9, #3ee87f 55%, #1da35a);
  border: 1px solid rgba(210, 255, 230, 0.75);
  box-shadow: 0 0 14px rgba(62, 232, 127, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.5);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
}

/* the plate is a real control on shelves where the card body previews
   (vehicle shelf): tapping it is the explicit buy */
button.price-tag {
  pointer-events: auto;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

/* gem-priced plate variant (exclusives) */
.price-tag.gems {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: #f3ecff;
  background: linear-gradient(180deg, #c9a1ff, #8f45ff 55%, #5b21b8);
  border-color: rgba(220, 195, 255, 0.75);
  box-shadow: 0 0 14px rgba(150, 90, 255, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.35);
  text-shadow: 0 1px 2px rgba(40, 10, 90, 0.7);
}

.price-tag.gems svg { width: 11px; height: 11px; fill: #fff; filter: drop-shadow(0 0 4px rgba(255,255,255,0.6)); }

.price-tag.owned {
  color: rgba(190, 205, 235, 0.8);
  background: rgba(24, 20, 46, 0.7);
  border-color: rgba(120, 140, 200, 0.35);
  box-shadow: none;
  text-shadow: none;
}

/* ---- starter pack hero tile */
#starter-tile {
  position: relative;
  /* the ONE PER ACCOUNT ribbon hangs 8px above this tile's border box. With
     the tiles stacked flush that overhang landed INSIDE the FEATURED tile's
     tap target — two tappable things sharing pixels. The gutter is sized off
     the overhang so the ribbon always floats in dead space between them. */
  margin-top: 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 13px;
  border-radius: 15px;
  background:
    radial-gradient(120% 150% at 0% 0%, rgba(255, 45, 149, 0.22), transparent 55%),
    radial-gradient(120% 150% at 100% 100%, rgba(0, 229, 255, 0.16), transparent 55%),
    linear-gradient(135deg, rgba(46, 20, 70, 0.75), rgba(18, 10, 36, 0.85));
  border: 1px solid rgba(255, 179, 0, 0.55);
  box-shadow: 0 0 22px rgba(255, 179, 0, 0.18), inset 0 0 20px rgba(0, 0, 0, 0.35);
  cursor: pointer;
}

#starter-tile.owned { cursor: default; opacity: 0.78; border-color: rgba(120, 140, 200, 0.35); box-shadow: none; }

.st-ribbon {
  position: absolute;
  top: -8px;
  left: 12px;
  padding: 3px 10px;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 900;
  font-size: 7.5px;
  letter-spacing: 0.2em;
  text-indent: 0.1em;
  color: #2c1a02;
  background: linear-gradient(180deg, #ffe9a0, #ffb300);
  border: 1px solid rgba(255, 240, 200, 0.8);
  box-shadow: 0 0 12px rgba(255, 179, 0, 0.55);
}

.st-main { flex: 1 1 0; min-width: 0; }

.st-name {
  font-family: var(--display);
  font-weight: 900;
  font-style: italic;
  /* held to one line at 360px — 14px/0.16em broke 'STARTER PACK' in half
     next to the wider APP STORE ONLY plate */
  font-size: clamp(11.5px, 3.4vw, 14px);
  letter-spacing: 0.12em;
  white-space: nowrap;
  color: #fff;
  text-shadow: 0 0 12px rgba(255, 179, 0, 0.6);
}

.st-items {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 3px 9px;
  margin-top: 6px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 9px;
  letter-spacing: 0.06em;
}

.st-items .sti { display: inline-flex; align-items: center; gap: 4px; }

.st-items svg { width: 11px; height: 11px; }

.sti.gems { color: #e2d2ff; text-shadow: 0 0 8px rgba(160, 100, 255, 0.6); }
.sti.gems svg { fill: #b06bff; filter: drop-shadow(0 0 4px rgba(160, 100, 255, 0.8)); }
.sti.chips { color: #ffe9c0; text-shadow: 0 0 8px rgba(255, 179, 0, 0.55); }
.sti.chips svg { fill: var(--amber); filter: drop-shadow(0 0 4px rgba(255, 179, 0, 0.8)); }

/* Value framing is the CONTENTS + one COMPUTED comparison (ui.js
   renderStore). The struck-through 'WAS' price that used to live here was a
   reference price the game never charged — a fake anchor — and it is gone
   for good, along with its .st-was rule. */
.st-buy { flex: none; text-align: center; }

.st-value {
  margin-top: 5px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 7px;
  letter-spacing: 0.14em;
  text-indent: 0.07em;
  line-height: 1.5;
  color: rgba(196, 210, 245, 0.82);
}

/* ---- FEATURED rotating slot: today's headline deal, above everything */
#featured-tile {
  pointer-events: auto;
  cursor: pointer;
  position: relative;
  display: flex;
  /* WIDTH 100% IS LOAD-BEARING. This tile is a <button>, and a button
     shrink-to-fits its content even with display:flex -- unlike every other
     row in the store, which is a <div> and fills the shelf. So the FEATURED
     card, the FIRST thing anyone sees in the store, sat at ~62% width with a
     ragged right edge against every card below it. */
  width: 100%;
  align-items: center;
  gap: 12px;
  padding: 13px;
  border-radius: 15px;
  text-align: left;
  background:
    radial-gradient(130% 160% at 100% 0%, rgba(0, 229, 255, 0.20), transparent 55%),
    radial-gradient(120% 150% at 0% 100%, rgba(160, 100, 255, 0.22), transparent 55%),
    linear-gradient(135deg, rgba(20, 30, 66, 0.8), rgba(14, 10, 34, 0.88));
  border: 1px solid rgba(0, 229, 255, 0.5);
  box-shadow: 0 0 22px rgba(0, 229, 255, 0.16), inset 0 0 20px rgba(0, 0, 0, 0.35);
}

#featured-tile:active { transform: scale(0.98); }

.ft-ribbon {
  position: absolute;
  top: -8px;
  left: 12px;
  padding: 3px 10px;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 900;
  font-size: 7.5px;
  letter-spacing: 0.2em;
  text-indent: 0.1em;
  color: #041c22;
  background: linear-gradient(180deg, #b8f6ff, #00e5ff);
  border: 1px solid rgba(210, 250, 255, 0.85);
  box-shadow: 0 0 12px rgba(0, 229, 255, 0.55);
}

#featured-tile .v-thumb { width: 96px; height: auto; flex: none; }

.ft-main { flex: 1 1 0; min-width: 0; }

.ft-name {
  font-family: var(--display);
  font-weight: 900;
  font-style: italic;
  font-size: 14px;
  letter-spacing: 0.14em;
  color: #fff;
  text-shadow: 0 0 12px rgba(0, 229, 255, 0.55);
}

.ft-sub {
  margin-top: 4px;
  font-family: var(--display);
  font-weight: 600;
  font-size: 7px;
  letter-spacing: 0.14em;
  color: rgba(180, 205, 240, 0.75);
}

.ft-rotates {
  margin-top: 6px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 6.5px;
  letter-spacing: 0.18em;
  color: rgba(255, 224, 160, 0.8);
}

.ft-rotates svg {
  width: 9px;
  height: 9px;
  flex: none;
  stroke: var(--amber);
  filter: drop-shadow(0 0 3px rgba(255, 179, 0, 0.5));
}

/* ---- gem pack grid */
#gem-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

.gpack {
  pointer-events: auto;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 11px 8px 10px;
  border-radius: 13px;
  /* the GEM PACKS shelf is the store's violet tier — a flat slate fill is what
     made this block read as the same gray as everything around it */
  background:
    radial-gradient(120% 85% at 50% 0%, rgba(160, 100, 255, 0.18), transparent 62%),
    rgba(24, 16, 46, 0.62);
  border: 1px solid rgba(160, 100, 255, 0.34);
  box-shadow: 0 0 14px rgba(150, 90, 255, 0.08), inset 0 0 16px rgba(0, 0, 0, 0.35);
  transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
}

.gpack:active { transform: scale(0.96); }

.gpack .gp-ico { display: flex; align-items: flex-end; gap: 2px; height: 26px; }

.gpack .gp-ico svg {
  fill: #b06bff;
  filter: drop-shadow(0 0 6px rgba(160, 100, 255, 0.8));
}

.gpack .gp-ico svg .gem-core { fill: #4a2a80; }

.gp-amt {
  font-family: var(--display);
  font-weight: 900;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  color: #ecdfff;
  text-shadow: 0 0 10px rgba(160, 100, 255, 0.6);
}

.gp-tag {
  font-family: var(--display);
  font-weight: 500;
  font-size: 7px;
  letter-spacing: 0.24em;
  text-indent: 0.12em;
  color: rgba(190, 170, 240, 0.6);
}

/* per-gem value badge vs the baseline pack (computed in ui.js renderStore) */
.gp-bonus {
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid rgba(255, 179, 0, 0.5);
  background: rgba(50, 34, 6, 0.55);
  font-family: var(--display);
  font-weight: 800;
  font-size: 6.5px;
  letter-spacing: 0.16em;
  text-indent: 0.08em;
  color: #ffd376;
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.5);
}

/* the flagship pack gets a POPULAR halo */
.gpack.hot { border-color: rgba(255, 179, 0, 0.55); box-shadow: 0 0 16px rgba(255, 179, 0, 0.15), inset 0 0 16px rgba(0, 0, 0, 0.35); }

.gp-hot {
  position: absolute;
  transform: translateY(-19px);
  padding: 2px 8px;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 900;
  font-size: 6.5px;
  letter-spacing: 0.2em;
  color: #2c1a02;
  background: linear-gradient(180deg, #ffe9a0, #ffb300);
  box-shadow: 0 0 10px rgba(255, 179, 0, 0.5);
}

.gpack { position: relative; }

/* ---- DREAM GARAGE: aspirational vehicle SKUs (canvas silhouettes,
   explicit COMING TO GARAGE state — presentation only, never granted) */
#vehicle-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

.vcard {
  pointer-events: auto;
  cursor: pointer;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 12px 8px 11px;
  border-radius: 13px;
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(0, 229, 255, 0.08), transparent 60%),
    rgba(18, 14, 40, 0.72);
  border: 1px solid rgba(0, 229, 255, 0.3);
  box-shadow: inset 0 0 16px rgba(0, 0, 0, 0.4);
  transition: transform 0.1s;
}

.vcard:active { transform: scale(0.97); }

.v-thumb { width: 100%; max-width: 132px; height: auto; }

.v-name {
  font-family: var(--display);
  font-weight: 900;
  font-style: italic;
  font-size: 12.5px;
  letter-spacing: 0.14em;
  color: #fff;
  text-shadow: 0 0 10px rgba(0, 229, 255, 0.5);
}

.v-kind {
  font-family: var(--display);
  font-weight: 600;
  font-size: 6.5px;
  letter-spacing: 0.26em;
  text-indent: 0.13em;
  color: rgba(180, 205, 240, 0.7);
}

.v-coming {
  margin-top: 3px;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid rgba(255, 179, 0, 0.45);
  background: rgba(50, 34, 6, 0.5);
  font-family: var(--display);
  font-weight: 800;
  font-size: 6px;
  letter-spacing: 0.18em;
  text-indent: 0.09em;
  color: #ffd376;
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.45);
}

.v-buy { margin-top: 6px; text-align: center; }

/* the plate must be inline-BLOCK here: as a plain inline span its 8px
   vertical padding paints outside the line box and smears over the bundle
   note above it (the 'struck-through BUNDLE text' capture bug) */
.v-buy .price-tag { display: inline-block; }

/* ---- exclusives row (gem-only cosmetics) */
#excl-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* This shelf went from 2 tiles to all SIX gem cosmetics, so it is now three
     rows rather than one -- and grid only equalises heights WITHIN a row. Item
     names wrap differently ("ULTRAVIOLET" vs "PRISM"), so without this the
     rows stepped in height down the shelf. Same fix as #station-picker. */
  grid-auto-rows: 1fr;
  gap: 8px;
}

.excl {
  pointer-events: auto;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 10px;
  border-radius: 13px;
  /* EXCLUSIVES are the gem tier — same violet key as the gem packs above */
  background:
    linear-gradient(120deg, rgba(160, 100, 255, 0.16), transparent 62%),
    rgba(24, 16, 46, 0.62);
  border: 1px solid rgba(160, 100, 255, 0.34);
  box-shadow: 0 0 14px rgba(150, 90, 255, 0.08), inset 0 0 16px rgba(0, 0, 0, 0.35);
  transition: transform 0.1s;
}

.excl:active { transform: scale(0.96); }

.excl .gswatch { width: 30px; height: 30px; flex: none; }

.ex-main { flex: 1 1 0; min-width: 0; }

.ex-name {
  font-family: var(--display);
  font-weight: 700;
  font-size: 9px;
  letter-spacing: 0.1em;
  color: #f0eaff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}

.ex-kind {
  margin-top: 2px;
  font-family: var(--display);
  font-weight: 500;
  font-size: 6.5px;
  letter-spacing: 0.22em;
  color: rgba(190, 170, 240, 0.55);
}

.ex-price {
  margin-top: 5px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--display);
  font-weight: 900;
  font-size: 10px;
  color: #e2d2ff;
  text-shadow: 0 0 8px rgba(160, 100, 255, 0.6);
}

.ex-price svg { width: 10px; height: 10px; fill: #b06bff; filter: drop-shadow(0 0 4px rgba(160, 100, 255, 0.8)); }

.ex-price.owned { color: rgba(170, 230, 190, 0.9); text-shadow: 0 0 8px rgba(62, 232, 127, 0.4); }
.ex-price.owned svg { fill: #3ee87f; filter: drop-shadow(0 0 4px rgba(62, 232, 127, 0.6)); }

.excl.deny { animation: cardShake 0.4s ease-in-out; }
.excl.bought { animation: cardPop 0.5s cubic-bezier(0.2, 1.9, 0.4, 1); }

/* ---- GEM PERKS: the premium currency's spend side beyond cosmetics —
   PRESTIGE BOOST (24h ×2 chips token) + the mission re-roll signpost */
#perk-grid {
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.perk {
  pointer-events: auto;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px;
  border-radius: 13px;
  text-align: left;
  border: 1px solid rgba(160, 100, 255, 0.35);
  background: linear-gradient(135deg, rgba(52, 30, 86, 0.5), rgba(22, 14, 40, 0.6));
  box-shadow: inset 0 0 14px rgba(150, 90, 255, 0.08);
  transition: transform 0.1s;
}

.perk:active { transform: scale(0.97); }

/* live boost: the tile flips to its green ACTIVE receipt state */
.perk.on {
  cursor: default;
  border-color: rgba(62, 232, 127, 0.4);
  box-shadow: 0 0 12px rgba(62, 232, 127, 0.12), inset 0 0 14px rgba(62, 232, 127, 0.06);
}

.perk-ico {
  width: 34px;
  height: 34px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, rgba(200, 160, 255, 0.3), rgba(60, 30, 110, 0.55));
  border: 1px solid rgba(160, 100, 255, 0.5);
  box-shadow: 0 0 12px rgba(150, 90, 255, 0.3);
}

.perk-ico svg {
  width: 17px;
  height: 17px;
  fill: #c9a4ff;
  filter: drop-shadow(0 0 5px rgba(160, 100, 255, 0.7));
}

.perk-ico .stroke-ico { fill: none; stroke: #c9a4ff; }

.perk-main { flex: 1 1 0; min-width: 0; }

.perk-name {
  font-family: var(--display);
  font-weight: 800;
  font-size: 11px;
  letter-spacing: 0.12em;
  color: #efe6ff;
  text-shadow: 0 0 9px rgba(160, 100, 255, 0.5);
}

.perk-sub {
  margin-top: 3px;
  font-family: var(--display);
  font-weight: 600;
  font-size: 7px;
  letter-spacing: 0.14em;
  color: rgba(205, 190, 235, 0.65);
}

.perk .price-tag { flex: none; }

.perk.deny { animation: cardShake 0.4s ease-in-out; }
.perk.bought { animation: cardPop 0.5s cubic-bezier(0.2, 1.9, 0.4, 1); }

/* ---- remove ads tile */
#circuitpass-tile {
  display: flex;
  align-items: center;
  /* wraps rather than crushing: un-wrapped, the APP STORE ONLY plate left
     the copy column ~98px and broke 'AD-FREE PASS' in half over a five-line
     sub-caption at 360px */
  flex-wrap: wrap;
  gap: 9px 11px;
  margin-top: 13px;
  padding: 11px 12px;
  border-radius: 13px;
  background: rgba(20, 16, 40, 0.6);
  border: 1px solid rgba(0, 229, 255, 0.32);
  box-shadow: inset 0 0 16px rgba(0, 0, 0, 0.35);
  cursor: pointer;
}

/* SELLABLE STATE. Sitting at position 2 it has to hold its own against the
   FEATURED tile above it, so it carries a ribbon and a lit edge. Owned, all
   of that comes off and it reads as a receipt. */
#circuitpass-tile:not(.owned) {
  position: relative;
  margin-top: 20px;
  border-color: rgba(0, 229, 255, 0.55);
  background:
    linear-gradient(180deg, rgba(0, 229, 255, 0.10), rgba(20, 16, 40, 0.62) 60%);
  box-shadow: inset 0 0 16px rgba(0, 0, 0, 0.35), 0 0 18px rgba(0, 229, 255, 0.16);
}

.ra-ribbon {
  position: absolute;
  top: -8px;
  left: 12px;
  padding: 3px 10px;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 900;
  font-size: 7.5px;
  letter-spacing: 0.2em;
  text-indent: 0.1em;
  color: #041c22;
  background: linear-gradient(180deg, #b8f6ff, #00e5ff);
  border: 1px solid rgba(210, 250, 255, 0.85);
  box-shadow: 0 0 12px rgba(0, 229, 255, 0.55);
}

/* THE PITCH, READABLE. Three rows at 10px in the UI face, replacing a 7px
   uppercase run-on at 55% opacity that no player was ever going to read. */
.ra-perks {
  margin-top: 5px;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.ra-perks li {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--ui-sans);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: 0.03em;
  color: #cfe9f2;
}

#circuitpass-tile .ra-perks svg {
  width: 11px;
  height: 11px;
  flex: none;
  fill: var(--cyan);
  filter: drop-shadow(0 0 3px rgba(0, 229, 255, 0.55));
}

#circuitpass-tile.owned { cursor: default; opacity: 0.8; }

#circuitpass-tile .ra-ico {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 10px;
  background: rgba(0, 229, 255, 0.1);
  border: 1px solid rgba(0, 229, 255, 0.35);
}

/* the bolt is a FILLED path, unlike the stroked ad-rectangle it replaced —
   leaving fill:none here drew an invisible icon in a lit box */
#circuitpass-tile .ra-ico svg { width: 18px; height: 18px; fill: var(--cyan); stroke: none; filter: drop-shadow(0 0 4px rgba(0, 229, 255, 0.6)); }

.ra-main { flex: 1 1 155px; min-width: 0; }

.ra-name {
  font-family: var(--display);
  font-weight: 700;
  font-size: 10.5px;
  letter-spacing: 0.12em;
  white-space: nowrap;
  color: #d8f6ff;
  text-shadow: 0 0 8px rgba(0, 229, 255, 0.4);
}

.ra-sub {
  margin-top: 2px;
  font-family: var(--display);
  font-weight: 500;
  font-size: 7px;
  letter-spacing: 0.16em;
  color: rgba(170, 185, 220, 0.55);
}

/* store footnote: the required IAP disclosure on a build that can charge,
   a plain statement of fact on one that cannot (ui.js renderStore) */
.store-foot {
  margin-top: 12px;
  text-align: center;
  font-family: var(--display);
  font-weight: 500;
  font-size: 7px;
  letter-spacing: 0.2em;
  color: rgba(150, 165, 200, 0.45);
}

/* --------------------------------------------- garage PERFORMANCE tab */
/* three upgrade tracks as full-width rows in the garage sheet */
#garage-items.perf { display: block; }

.ucard {
  display: flex;
  align-items: center;
  gap: 10px;
  /* 8px, not 9: the stacked name/effect lines add a row to every card, and
     this keeps all three tracks inside the sheet's budget at 360x780 */
  padding: 8px 10px;
  border-radius: 13px;
  background: rgba(20, 16, 40, 0.6);
  border: 1px solid rgba(120, 140, 200, 0.22);
  box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.35);
}

.ucard + .ucard { margin-top: 8px; }

.ucard[data-track="speed"] { --uc: #ffb300; --uc-rgb: 255, 179, 0; }
.ucard[data-track="nitro"] { --uc: #00e5ff; --uc-rgb: 0, 229, 255; }
.ucard[data-track="drift"] { --uc: #ff2d95; --uc-rgb: 255, 45, 149; }

.u-icon {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  color: var(--uc);
  background: rgba(var(--uc-rgb), 0.1);
  border: 1px solid rgba(var(--uc-rgb), 0.4);
  box-shadow: 0 0 10px rgba(var(--uc-rgb), 0.18);
}

.u-icon svg { width: 19px; height: 19px; filter: drop-shadow(0 0 4px rgba(var(--uc-rgb), 0.7)); }

.u-main { flex: 1 1 0; min-width: 0; }

/* Name and per-level line STACK. Side by side they needed ~191px of a 158px
   row at 360, so the nowrap effect line ('LV 0 · +2% TOP SPEED / LV') ran
   out under the amber buy plate and was chopped mid-word — text painted
   under a button, which no box-intersection audit catches because the audit
   never opens this tab. The name keeps its line, the effect line owns the
   one below it, and an ellipsis is the backstop at any width. */
.u-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1px 8px;
}

.u-name {
  flex: none;
  white-space: nowrap;
  font-family: var(--display);
  font-weight: 700;
  font-size: 9.5px;
  letter-spacing: 0.14em;
  color: #e8eefc;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}

.u-per {
  flex: 1 1 100%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--display);
  font-weight: 500;
  font-size: 6.5px;
  letter-spacing: 0.12em;
  color: rgba(170, 185, 220, 0.6);
  white-space: nowrap;
}

/* stat bar: filled to base+level, with a brighter preview notch for the
   next level so the purchase shows exactly what it buys */
.u-bar {
  position: relative;
  height: 6px;
  margin-top: 6px;
  border-radius: 3px;
  background: rgba(10, 8, 22, 0.9);
  border: 1px solid rgba(120, 140, 200, 0.2);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.6);
  overflow: hidden;
}

.u-bar .fill {
  position: absolute;
  inset: 0;
  transform-origin: left center;
  border-radius: 3px;
  background: linear-gradient(90deg, rgba(var(--uc-rgb), 0.55), var(--uc));
  box-shadow: 0 0 8px rgba(var(--uc-rgb), 0.6);
}

.u-bar .next {
  position: absolute;
  inset: 0;
  transform-origin: left center;
  border-radius: 3px;
  background: rgba(var(--uc-rgb), 0.22);
}

/* level pips: five notches under the bar */
.u-pips { display: flex; gap: 3px; margin-top: 5px; }

.u-pips i {
  flex: 1 1 0;
  height: 3px;
  border-radius: 2px;
  background: rgba(120, 140, 200, 0.22);
}

.u-pips i.on {
  background: var(--uc);
  box-shadow: 0 0 6px rgba(var(--uc-rgb), 0.7);
}

/* price plate: chips-amber buy button / MAX state */
.u-buy {
  pointer-events: auto;
  cursor: pointer;
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 34px;
  padding: 0 11px;
  border-radius: 10px;
  font-family: var(--display);
  font-weight: 900;
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.05em;
  color: #2c1a02;
  background: linear-gradient(180deg, #ffe9a0, #ffb300 55%, #c77f00);
  border: 1px solid rgba(255, 240, 200, 0.75);
  box-shadow: 0 0 12px rgba(255, 179, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.5);
  transition: transform 0.1s, filter 0.12s;
}

.u-buy:active { transform: scale(0.94); filter: brightness(1.15); }

.u-buy svg { width: 10px; height: 10px; fill: #5a3c05; }

.u-buy.broke {
  color: rgba(255, 225, 160, 0.75);
  background: rgba(50, 34, 10, 0.6);
  border-color: rgba(255, 179, 0, 0.3);
  box-shadow: none;
}

.u-buy.broke svg { fill: rgba(255, 179, 0, 0.6); }

.u-buy.max {
  cursor: default;
  color: #0a2a14;
  background: linear-gradient(180deg, #b9ffd9, #3ee87f 55%, #1da35a);
  border-color: rgba(210, 255, 230, 0.7);
  box-shadow: 0 0 12px rgba(62, 232, 127, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.5);
}

.ucard.deny { animation: cardShake 0.4s ease-in-out; }
.ucard.bought { animation: cardPop 0.5s cubic-bezier(0.2, 1.9, 0.4, 1); }

/* --------------------------------------------------- CONTINUE? revive */
#continue-block { margin: 4px 0 10px; }

/* fully collapse when resolved — the global .hidden only fades, and a
   phantom 500px offer block would hollow out the results card */
#continue-block.hidden { display: none; }

.cont-title {
  font-family: var(--display);
  font-weight: 900;
  font-style: italic;
  font-size: 17px;
  letter-spacing: 0.3em;
  text-indent: 0.15em;
  color: #d8f6ff;
  text-shadow: 0 0 16px rgba(0, 229, 255, 0.8), 0 1px 4px rgba(0, 0, 0, 0.8);
}

/* countdown ring: conic depletion driven by stroke-dashoffset (ui.js) */
.cont-ring {
  position: relative;
  width: 76px;
  height: 76px;
  margin: 9px auto 10px;
}

.cont-ring svg {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}

.cont-ring .ring-bg {
  fill: none;
  stroke: rgba(120, 140, 200, 0.22);
  stroke-width: 4.5;
}

.cont-ring .ring-fg {
  fill: none;
  stroke: var(--cyan);
  stroke-width: 4.5;
  stroke-linecap: round;
  stroke-dasharray: 201.1;   /* 2*pi*32 */
  filter: drop-shadow(0 0 6px rgba(0, 229, 255, 0.8));
  transition: stroke-dashoffset 0.2s linear, stroke 0.3s;
}

/* last three seconds: the ring runs hot amber */
.cont-ring.low .ring-fg {
  stroke: var(--amber);
  filter: drop-shadow(0 0 6px rgba(255, 179, 0, 0.8));
}

#cont-secs {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--display);
  font-weight: 900;
  font-size: 26px;
  color: #fff;
  text-shadow: 0 0 14px rgba(0, 229, 255, 0.7);
}

.cont-ring.low #cont-secs { text-shadow: 0 0 14px rgba(255, 179, 0, 0.8); }

/* the stake under the ring: what saying yes protects — the live score,
   plus the mission chips this run already cleared */
/* 12px ABOVE AND BELOW. The bottom was 2px, which measured as a 2px gap
   between the stake line and the button row and read on device as the revive
   buttons colliding with "KEEP YOUR <score>?" — the one line on this card
   whose whole job is to price the decision. Symmetric margins let it sit as
   its own beat instead of as a label stuck to the buttons. */
#cont-stake { margin: 12px 0; }

#cont-stake:empty { display: none; }

.cs-score {
  font-family: var(--display);
  font-weight: 800;
  font-style: italic;
  font-size: clamp(14px, 4.4vw, 18px);
  letter-spacing: 0.08em;
  color: #fff;
  text-shadow: 0 0 14px rgba(0, 229, 255, 0.45), 0 1px 4px rgba(0, 0, 0, 0.8);
}

.cs-score b {
  font-weight: 900;
  color: #9beeff;
  text-shadow: 0 0 14px rgba(0, 229, 255, 0.7);
}

.cs-missions {
  margin-top: 7px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 11px;
  border-radius: 999px;
  border: 1px solid rgba(255, 179, 0, 0.4);
  background: rgba(50, 34, 6, 0.55);
  font-family: var(--display);
  font-weight: 700;
  font-size: 7.5px;
  letter-spacing: 0.14em;
  text-indent: 0.07em;
  color: #ffe0a0;
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.45);
}

.cs-missions svg {
  width: 10px;
  height: 10px;
  flex: none;
  fill: var(--amber);
  filter: drop-shadow(0 0 4px rgba(255, 179, 0, 0.7));
}

.cs-missions svg .chip-core { fill: #5a3c05; }

.cont-buttons {
  display: flex;
  gap: 9px;
  justify-content: center;
}

.cont-buttons button {
  pointer-events: auto;
  cursor: pointer;
  flex: 1 1 0;
  max-width: 150px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: 48px;
  padding: 6px 8px;
  border-radius: 12px;
  font-family: var(--display);
  transform: skewX(-6deg);
  transition: transform 0.1s, filter 0.12s;
}

.cont-buttons button:active { transform: skewX(-6deg) scale(0.94); filter: brightness(1.25); }

/* THE LABEL MUST NOT WRAP. At 360px the button's inner box is 121px and the
   sub-line ('KEEP YOUR RUN') set the column width at 81px — one pixel under
   what 'WATCH AD' needed, so the primary label broke across two lines and
   printed straight through its own sub-line. A box-intersection audit cannot
   see it (it is one element overflowing itself), but the player reads it as
   the results screen overlapping its CTA. Both lines now shrink with the
   viewport and neither is allowed to wrap. */
.cont-buttons button span {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  min-width: 0;
  gap: 2px;
  font-weight: 900;
  font-size: clamp(9.5px, 2.9vw, 11.5px);
  letter-spacing: 0.06em;
  white-space: nowrap;
}

.cont-buttons button small {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: 500;
  font-size: clamp(5.6px, 1.75vw, 6.5px);
  letter-spacing: 0.12em;
  opacity: 0.75;
}

#cont-pass {
  color: #06222c;
  background: linear-gradient(160deg, #7df0ff 0%, #00e5ff 45%, #0a7a96 100%);
  border: 1px solid rgba(180, 245, 255, 0.8);
  box-shadow: 0 0 22px rgba(0, 229, 255, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.45);
  animation: contHum 1.4s ease-in-out infinite alternate;
}

#cont-pass svg { width: 17px; height: 17px; flex: none; fill: #06222c; }

@keyframes contHum {
  from { box-shadow: 0 0 14px rgba(0, 229, 255, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.45); }
  to   { box-shadow: 0 0 30px rgba(0, 229, 255, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.45); }
}

#cont-gems {
  color: #f3ecff;
  background: linear-gradient(160deg, #b98aff 0%, #8f45ff 50%, #4a1c96 100%);
  border: 1px solid rgba(215, 185, 255, 0.75);
  box-shadow: 0 0 20px rgba(150, 90, 255, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.3);
}

#cont-gems .gem-ico { width: 16px; height: 16px; flex: none; fill: #fff; filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.6)); }

#cont-gems .gem-ico .gem-core { fill: #5b21b8; }

#cont-gems.broke { filter: saturate(0.4) brightness(0.7); }

#cont-skip {
  pointer-events: auto;
  cursor: pointer;
  margin-top: 9px;
  padding: 7px 16px;
  background: none;
  border: none;
  font-family: var(--display);
  font-weight: 500;
  font-size: 8.5px;
  letter-spacing: 0.24em;
  color: rgba(170, 185, 220, 0.6);
}

/* BEAT 1 of the two-beat end-of-run flow: while the revive offer is live,
   the decision is the ONLY thing on the card — ring + WATCH AD / 2 GEMS /
   NO THANKS. Score, stats, payout and mission claims are BEAT 2 (the
   results card ui.showOver stages after decline/timeout). */
.over-panel.continuing > :not(#continue-block) { display: none; }

.over-panel.continuing #continue-block { margin: 26px 0 30px; }

/* -------------------------------------------- results reward rows
   TWO-COLUMN LEDGER, not centered ragged lines: icon in a fixed 18px slot,
   label left-aligned in the neutral sans, amount right-aligned in tabular
   numerals. The hairline above the list separates the itemization from the
   +N CHIPS total pill it explains (ui.js _renderOverRewards). */
#over-rewards:not(:empty) {
  width: min(100%, 300px);
  margin: 8px auto 0;
  padding-top: 6px;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
}

.rw-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 4px;
  font-weight: 700;
  font-size: 10px;
  color: #ffe9c0;
  text-shadow: 0 0 9px rgba(255, 179, 0, 0.5), 0 1px 3px rgba(0, 0, 0, 0.8);
}

.rw-row svg { width: 12px; height: 12px; flex: none; fill: var(--amber); filter: drop-shadow(0 0 4px rgba(255, 179, 0, 0.7)); }

/* fixed icon slot: every glyph centers in the same 18px column so the
   label edge is a straight line down the ledger */
.rw-ico {
  flex: none;
  width: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* long labels (rank movement: 'PASSED <rival>, #n THIS WEEK') shrink a
   step instead of wrapping and flinging the amount off its column */
.rw-label {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: left;
  font-family: var(--ui-sans);
  font-weight: 600;
  font-size: clamp(8px, 2.4vw, 10px);
  letter-spacing: 0.04em;
}

.rw-row .rw-amt {
  flex: none;
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--amber);
  text-shadow: 0 0 10px rgba(255, 179, 0, 0.8);
}

/* a TOTAL line, if a caller ever appends one, sits under its own thin
   divider per ledger convention */
.rw-row.total {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
}

/* LOCKED LEDGER ROW — the CIRCUIT PASS line a non-owner sees. Reads as an
   unlit version of the row an owner earns: same column grid, same number,
   cyan instead of amber, dimmed, with the price where the gem chip sits.
   It must never look banked, so the amount loses its glow and the whole row
   sits at 0.62 against the earned lines above it. */
.rw-row.rw-locked {
  pointer-events: auto;
  cursor: pointer;
  opacity: 0.62;
  color: #bfe9f2;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
  border-top: 1px dashed rgba(0, 229, 255, 0.22);
  margin-top: 6px;
  padding-top: 6px;
  transition: opacity 0.15s;
}

.rw-row.rw-locked:active { opacity: 0.9; }
.rw-row.rw-settled { cursor: default; opacity: 0.75; }
.rw-row.rw-settled svg { fill: var(--cyan); }
.rw-row.rw-locked svg { fill: var(--cyan); filter: none; }

.rw-row.rw-locked .rw-amt {
  color: #7fd8e8;
  text-shadow: none;
}

.rw-price {
  flex: none;
  margin-left: 6px;
  padding: 2px 6px;
  border: 1px solid rgba(0, 229, 255, 0.45);
  border-radius: 9999px;
  font-family: var(--display);
  font-style: italic;
  font-weight: 900;
  font-size: 10px;
  letter-spacing: 0.04em;
  color: #d8f6fc;
}

.rw-row.rank { color: #ffd9ec; text-shadow: 0 0 9px rgba(255, 45, 149, 0.5), 0 1px 3px rgba(0, 0, 0, 0.8); }
.rw-row.rank svg { fill: #ff2d95; filter: drop-shadow(0 0 4px rgba(255, 45, 149, 0.7)); }

.rw-row.prestige { color: #e2d2ff; text-shadow: 0 0 9px rgba(160, 100, 255, 0.55), 0 1px 3px rgba(0, 0, 0, 0.8); }
.rw-row.prestige svg { fill: #b06bff; filter: drop-shadow(0 0 4px rgba(160, 100, 255, 0.7)); }

/* ------------------------------------------------ DOUBLE EARNINGS CTA */
#double-btn {
  pointer-events: auto;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  min-height: 44px;
  margin-top: 10px;
  border-radius: 12px;
  font-family: var(--display);
  font-weight: 900;
  font-size: 11.5px;
  letter-spacing: 0.14em;
  color: #2c1a02;
  background: linear-gradient(160deg, #ffe9a0 0%, #ffb300 50%, #b06f00 100%);
  border: 1px solid rgba(255, 240, 200, 0.8);
  box-shadow: 0 0 20px rgba(255, 179, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.5);
  transform: skewX(-6deg);
  transition: transform 0.1s, filter 0.12s;
  animation: dblHum 1.5s ease-in-out infinite alternate;
}

#double-btn:active { transform: skewX(-6deg) scale(0.96); filter: brightness(1.15); }

#double-btn svg { width: 15px; height: 15px; flex: none; fill: #5a3c05; }

#double-btn b { font-variant-numeric: tabular-nums; }

#double-btn.hidden { display: none; }

#double-btn.done {
  cursor: default;
  animation: none;
  color: #0a2a14;
  background: linear-gradient(180deg, #b9ffd9, #3ee87f 55%, #1da35a);
  border-color: rgba(210, 255, 230, 0.7);
  box-shadow: 0 0 14px rgba(62, 232, 127, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.5);
}

@keyframes dblHum {
  from { box-shadow: 0 0 12px rgba(255, 179, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.5); }
  to   { box-shadow: 0 0 28px rgba(255, 179, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.5); }
}

/* store sheet must scroll on touch (the global touch-action: none would
   otherwise eat the pan) */
.store-scroll { pointer-events: auto; }

/* gem-priced garage swatch tag (CHROME NOIR / ULTRAVIOLET) */
.gtag.price.gemp {
  color: #e2d2ff;
  text-shadow: 0 0 8px rgba(160, 100, 255, 0.55);
}

.gtag.price.gemp svg { fill: #b06bff; filter: drop-shadow(0 0 4px rgba(160, 100, 255, 0.7)); }

/* toast icon flavors: gem grants, rank climbs, prestige tiers */
.toast.gem svg { fill: #b06bff; filter: drop-shadow(0 0 5px rgba(160, 100, 255, 0.8)); }
.toast.rank svg { fill: var(--magenta); filter: drop-shadow(0 0 5px rgba(255, 45, 149, 0.8)); }
.toast.prestige svg { fill: #b06bff; filter: drop-shadow(0 0 5px rgba(160, 100, 255, 0.8)); }

/* ================================================================== */
/* ROUND: live prestige ladder + forward hook + CARS tab + numerals   */

/* ---- garage header: the equipped CAR's name rides the panel title */
.panel-title #garage-car-name {
  /* stacked sub-line: an inline name widened the nowrap title and shoved
     the wallet pill off the right edge at 390px */
  display: block;
  margin-top: 1px;
  /* tuned to fit the header grid's centre track at 360px — the old
     8.5px/0.3em ran 104px wide and slid under the BACK plate */
  font-size: 8px;
  font-weight: 700;
  font-style: normal;
  letter-spacing: 0.2em;
  text-indent: 0.2em;
  text-align: center;
  color: #9beeff;
  text-shadow: 0 0 10px rgba(0, 229, 255, 0.7);
}

/* ---- garage CARS tab: two wide hero-variant cards */
#garage-items.cars { grid-template-columns: repeat(2, 1fr); }

.gcard.ccard { min-height: 132px; gap: 4px; padding: 12px 8px 10px; }

.gcard.ccard .v-thumb { width: 110px; max-width: 90%; height: auto; }

.gcard.ccard .gname { font-size: 11px; letter-spacing: 0.14em; }

.ckind {
  font-family: var(--display);
  font-weight: 600;
  font-size: 6.5px;
  letter-spacing: 0.24em;
  text-indent: 0.12em;
  color: rgba(180, 205, 240, 0.65);
}

/* bundle-only chip tag (VESPER ION): arrives with its car, never sold */
.gtag.bundled {
  font-size: 7px;
  color: rgba(170, 200, 255, 0.75);
  text-shadow: none;
}

/* vesper tile state chips on the store NEW METAL shelf */
.v-coming.live {
  color: #9beeff;
  border-color: rgba(0, 229, 255, 0.45);
  background: rgba(4, 34, 44, 0.5);
  text-shadow: 0 0 8px rgba(0, 229, 255, 0.5);
}

.v-coming.own {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: #9beeff;
  border-color: rgba(0, 229, 255, 0.45);
  background: rgba(4, 34, 44, 0.5);
  text-shadow: 0 0 8px rgba(0, 229, 255, 0.5);
}

.v-coming.own svg { width: 8px; height: 8px; }

/* ---- shared tier color tokens for the new prestige surfaces */
/* #ranks-identity joins these groups (instead of restating the hex) so the
   identity strip's tier color can never drift from the tier strip's */
#over-tier .tier-bronze, #ranks-tiers .tier-bronze, #ranks-identity.tier-bronze { --pt: #e09a5c; --pt-rgb: 224, 154, 92; }
#over-tier .tier-silver, #ranks-tiers .tier-silver, #ranks-identity.tier-silver { --pt: #ccd6e8; --pt-rgb: 204, 214, 232; }
#over-tier .tier-gold,   #ranks-tiers .tier-gold,   #ranks-identity.tier-gold   { --pt: #ffd35e; --pt-rgb: 255, 211, 94; }
#over-tier .tier-neon,   #ranks-tiers .tier-neon,   #ranks-identity.tier-neon   { --pt: #ff2d95; --pt-rgb: 255, 45, 149; }

/* Ranks screen only: the career strip keeps its metal hues but at roughly
   half saturation, so the strip still names the tiers without competing
   with the currency gold / player pink / target cyan accents below it.
   The results-card #over-tier lane keeps the full metals. */
#ranks-tiers .tier-bronze { --pt: #bf9c7d; --pt-rgb: 191, 156, 125; }
#ranks-tiers .tier-silver { --pt: #d2d8e0; --pt-rgb: 210, 216, 224; }
#ranks-tiers .tier-gold   { --pt: #d6c186; --pt-rgb: 214, 193, 134; }
#ranks-tiers .tier-neon   { --pt: #cb6295; --pt-rgb: 203, 98, 149; }

/* ---- results-card LIVE PRESTIGE LADDER bar: cur tier -> next tier */
#over-tier { margin: 9px auto 0; width: 100%; }

#over-tier.hidden { display: none; }

.ot-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* same guard as .ow-row: two nowrap tier names plus the percentage must be
     allowed to wrap rather than push past the card at 360px */
  flex-wrap: wrap;
  gap: 2px 8px;
  margin-bottom: 5px;
}

.ot-cur, .ot-next {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 8px;
  letter-spacing: 0.12em;
  white-space: nowrap;
  color: var(--pt, #c8d2e4);
  text-shadow: 0 0 8px rgba(var(--pt-rgb, 200, 210, 228), 0.5);
}

.ot-cur svg, .ot-next svg {
  width: 11px;
  height: 11px;
  flex: none;
  fill: var(--pt, #c8d2e4);
  filter: drop-shadow(0 0 4px rgba(var(--pt-rgb, 200, 210, 228), 0.7));
}

/* pre-bronze state: dimmed outline shield, explicit UNRANKED */
.ot-cur.unranked { color: rgba(150, 165, 205, 0.6); text-shadow: none; }

.ot-cur.unranked svg {
  fill: none;
  stroke: rgba(150, 165, 205, 0.45);
  stroke-width: 1.8;
  filter: none;
}

.ot-pct {
  font-weight: 700;
  font-size: 9px;
  color: #ffe9c0;
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.5);
}

.ot-bar {
  height: 5px;
  border-radius: 999px;
  background: rgba(120, 140, 200, 0.18);
  border: 1px solid rgba(120, 140, 200, 0.14);
  overflow: hidden;
}

.ot-bar > span {
  display: block;
  height: 100%;
  border-radius: inherit;
  transform-origin: left;
  background: linear-gradient(90deg, #ffb300, #ffd35e);
  box-shadow: 0 0 8px rgba(255, 179, 0, 0.6);
}

/* ---- SEASON LANE: cumulative WEEK SCORE + next week-goal payout. The
   PRIMARY progression bar on the results card (the prestige ladder above is
   the long arc), and the same markup renders on the Ranks screen — one lane,
   two surfaces, identical numbers (ui.js _weekLaneHTML). Cyan-keyed so it
   reads as the CIRCUIT lane, distinct from the amber prestige bar. */
/* WIDTH FROM THE CARD, NOT THE SCREEN. min(88vw, 330px) is 317px at a 360px
   phone, but the card's own content box is only ~299px there — so the season
   lane hung 18px past the card's inner edge and the goal payout ('+1,200')
   was sliced in half by the panel's own overflow. Percent of the card can
   never do that at any width. */
/* ONE EDGE FOR THE WHOLE STACK.
   Reported from device as the results screen having "all different width" and
   an order that "does not make sense". The order was fixed in an earlier pass;
   the WIDTHS were not, and they were the louder half of it. Measured on a
   440pt phone, the panel's content box is 374px and the blocks stacked
   directly on each other were:

     #double-btn   374px   (width: 100%)
     #over-week    330px   (max-width: 330px)  -> 22px inset each side
     #over-tier    330px
     #over-next    fit-content, narrower still

   So the eye tracked a left edge that stepped in and out four times down one
   card. Nothing overlapped and nothing clipped, which is why the layout audit
   passed it -- ragged rhythm is invisible to a collision checker.
   Every stacked block now shares the panel's content width. The only
   deliberate exceptions are the hero chips pill and the reward rows, which are
   centred BADGES rather than stack items. */
#over-week { margin: 10px auto 0; width: 100%; }

#over-week.hidden, #ranks-week.hidden { display: none; }

#ranks-week {
  margin: 0 0 9px;
  padding: 8px 10px 7px;
  border-radius: 11px;
  background: rgba(0, 229, 255, 0.05);
  border: 1px solid rgba(0, 229, 255, 0.22);
}

.ow-row {
  display: flex;
  align-items: baseline;
  /* the lane's headline (WEEK SCORE 54,950 +13,120) and its goal payout are
     ~340px of nowrap text together. Wrapping lets the payout drop to its own
     right-aligned line at 360px instead of running off the card. */
  flex-wrap: wrap;
  gap: 2px 6px;
  margin-bottom: 5px;
}

.ow-label {
  font-family: var(--display);
  font-weight: 800;
  font-size: 7px;
  letter-spacing: 0.16em;
  text-indent: 0.08em;
  white-space: nowrap;      /* never let the lane's name stack into a block */
  color: rgba(0, 229, 255, 0.9);
  text-shadow: 0 0 8px rgba(0, 229, 255, 0.5);
}

.ow-total {
  font-weight: 800;
  font-size: 12px;
  color: #fff;
  text-shadow: 0 0 10px rgba(0, 229, 255, 0.5);
}

.ow-delta {
  font-weight: 800;
  font-size: 9px;
  color: #8dffc4;
  text-shadow: 0 0 8px rgba(0, 232, 122, 0.6);
}

.ow-goal {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-weight: 700;
  font-size: 7.5px;
  letter-spacing: 0.06em;
  white-space: nowrap;
  color: rgba(214, 226, 250, 0.86);
}

.ow-goal svg { width: 9px; height: 9px; fill: var(--amber); }

.ow-bar {
  height: 5px;
  border-radius: 999px;
  background: rgba(120, 140, 200, 0.18);
  border: 1px solid rgba(120, 140, 200, 0.14);
  overflow: hidden;
}

.ow-bar > span {
  display: block;
  height: 100%;
  border-radius: inherit;
  transform-origin: left;
  background: linear-gradient(90deg, #00a8c8, #7ff0ff);
  box-shadow: 0 0 9px rgba(0, 229, 255, 0.6);
}

.ow-foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  margin-top: 5px;
  font-weight: 700;
  font-size: 7.5px;
  letter-spacing: 0.09em;
  color: rgba(198, 212, 244, 0.8);
}

.ow-foot svg { width: 10px; height: 10px; flex: none; fill: var(--amber); }

.ow-foot svg.gem-ico, .ow-foot span svg { vertical-align: -1px; }

.ow-foot span { display: inline-flex; align-items: center; gap: 3px; }

/* week-goal reward row on the results receipt — a chips row, cyan-keyed to
   the lane that produced it */
.rw-row.goal svg { fill: #7ff0ff !important; }

/* ---- FREE GEM FAUCET strip (Ranks): the two no-pay gem sources, stated
   in full, with every figure read off the tables that grant them */
#ranks-faucet {
  margin: 0 0 9px;
  padding: 7px 10px;
  border-radius: 11px;
  background: rgba(160, 100, 255, 0.07);
  border: 1px solid rgba(160, 100, 255, 0.28);
}

.rf-head {
  display: flex;
  align-items: center;
  gap: 5px;
  margin-bottom: 5px;
  font-family: var(--display);
  font-weight: 800;
  font-size: 7.5px;
  letter-spacing: 0.2em;
  text-indent: 0.1em;
  color: #e2d2ff;
  text-shadow: 0 0 8px rgba(160, 100, 255, 0.55);
}

.rf-head svg { width: 10px; height: 10px; fill: #b06bff; }

.rf-row {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 3px 0;
}

.rf-row + .rf-row { border-top: 1px solid rgba(160, 100, 255, 0.14); }

.rf-src {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: none;
  width: 84px;
  /* fixed-width column: a label that does not fit must be shortened, never
     allowed to wrap and step this row out of line with its sibling */
  white-space: nowrap;
  font-family: var(--display);
  font-weight: 700;
  font-size: 7px;
  letter-spacing: 0.12em;
  color: rgba(206, 218, 248, 0.9);
}

.rf-src svg { width: 10px; height: 10px; flex: none; fill: var(--amber); }

.rf-val {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
  font-weight: 800;
  font-size: 7.5px;
  color: #e6dcff;
}

.rf-val b { display: inline-flex; align-items: center; gap: 2px; white-space: nowrap; }

.rf-val svg { width: 9px; height: 9px; fill: #b06bff; }

.rf-val i { color: rgba(160, 140, 210, 0.55); font-style: normal; }

.rf-when {
  flex: none;
  white-space: nowrap;
  font-family: var(--display);
  font-weight: 700;
  font-size: 6px;
  letter-spacing: 0.14em;
  color: rgba(178, 190, 224, 0.62);
}

/* prestige tier cell: its one-time gem drip, under the score gate */
.tc-gem {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-top: 2px;
  font-weight: 800;
  font-size: 7.5px;
  color: #d8c8ff;
}

.tc-gem svg { width: 8px; height: 8px; fill: #b06bff; }

/* ---- GROUP 4 disclosure: the four PROGRESS blocks behind one summary row.
   See the note in index.html for why. Styled to match the missions row above
   it so the card has one disclosure pattern rather than two. */
#over-progress { margin-top: 12px; width: 100%; }

#over-progress-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  min-height: 44px;             /* Apple's touch minimum, on its own */
  padding: 8px 12px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.10);
  color: #cfd8ea;
  font: 700 12px/1.2 var(--display);
  letter-spacing: 0.06em;
  text-align: left;
  cursor: pointer;
}

#over-progress-sum { flex: 1; min-width: 0; }

/* WRAP BETWEEN THE FACTS, NEVER INSIDE ONE.
   This block used to carry a comment promising the summary "must never wrap to
   two lines and never ellipsize mid-number" and then implemented none of it --
   no white-space, no overflow, no text-overflow. It silently did nothing, which
   is how a measured 4-way overlap survived on the results card at 390px, the
   standard iPhone width.
   What actually happened: the one-line layout needs ~294px and only 280px is
   available at 390px, so `SILVER SPECTRE` broke across lines as "SILVER" /
   "SPECTRE" and the inline <b>'s union rect swallowed its line-1 siblings.
   One line is NOT reliably reachable -- the tier names run to 14 characters and
   a weekly total can reach seven digits -- so the honest fix is to let the row
   wrap between facts while forbidding a break inside any single fact. Two clean
   lines beat one truncated one, and nothing is ellipsized away. */
#over-progress-sum b,
#over-progress-sum i { white-space: nowrap; }
#over-progress-sum b { color: #f0f4ff; font-weight: 800; }
#over-progress-sum i { font-style: normal; opacity: 0.45; padding: 0 2px; }

.op-chev {
  width: 16px; height: 16px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.2;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform 0.18s ease;
}

#over-progress-head[aria-expanded="true"] .op-chev { transform: rotate(180deg); }
#over-progress-head[aria-expanded="true"] {
  border-color: rgba(0, 229, 255, 0.30);
  background: rgba(0, 229, 255, 0.05);
}

/* first child inside the body loses its own top margin so the disclosure
   opens flush against its header instead of with a double gap */
#over-progress-body > :first-child { margin-top: 8px; }

/* DISPLAY:NONE, NOT JUST .hidden.
   The global rule is `.hidden { opacity: 0 !important; visibility: hidden; }`
   -- which HIDES an element but leaves it occupying its full height. Folding
   the disclosure with `.hidden` alone therefore collapsed nothing: the card
   swapped four visible blocks for ~300px of dead space above the stats rule,
   which looks worse than the flood it was meant to fix.
   The same applies to the four children: each is `.hidden` when it has no
   data, so an empty week lane or a maxed-out prestige ladder was silently
   reserving its own height on every card. */
#over-progress-body.hidden,
#over-week.hidden,
#over-tier.hidden,
#over-next.hidden,
#over-streak.hidden { display: none; }

/* ---- forward hook above DRIVE AGAIN: next weekly rival to beat */
#over-next {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 4px 6px;
  margin: 11px auto 0;
  padding: 6px 11px;
  /* full width, not fit-content: it sits between the prestige ladder and the
     streak line, and a shrink-to-fit pill between two full-width blocks was
     one of the four stepping edges. See the note on #over-week. */
  width: 100%;
  border-radius: 999px;
  background: rgba(0, 229, 255, 0.07);
  border: 1px solid rgba(0, 229, 255, 0.35);
  box-shadow: 0 0 14px rgba(0, 229, 255, 0.12);
  font-family: var(--display);
  font-weight: 700;
  font-size: 8px;
  letter-spacing: 0.1em;
  color: rgba(225, 232, 250, 0.9);
}

#over-next.hidden { display: none; }

#over-next svg { width: 11px; height: 11px; flex: none; fill: var(--amber); }

.on-tag { color: rgba(0, 229, 255, 0.9); text-shadow: 0 0 6px rgba(0, 229, 255, 0.6); }

.on-name { color: #fff; text-shadow: 0 0 8px rgba(0, 229, 255, 0.35); }

.on-score { color: #cfe9ff; }

.on-gap { color: #9beeff; }

.on-reward {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: #ffd376;
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.5);
}

/* ---- Ranks: prestige tier-threshold strip (all-time milestones) */
#ranks-tiers {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
  margin-bottom: 11px;
}

.tcell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 6px 2px 5px;
  border-radius: 9px;
  background: rgba(20, 16, 40, 0.45);
  border: 1px solid rgba(120, 140, 200, 0.16);
  opacity: 0.55;
}

.tcell svg {
  width: 13px;
  height: 13px;
  fill: var(--pt, #c8d2e4);
  filter: drop-shadow(0 0 4px rgba(var(--pt-rgb, 200, 210, 228), 0.6));
}

.tcell.done { opacity: 1; border-color: rgba(var(--pt-rgb), 0.45); }

.tcell.cur {
  box-shadow: 0 0 12px rgba(var(--pt-rgb), 0.35), inset 0 0 10px rgba(var(--pt-rgb), 0.12);
}

.tc-name {
  font-family: var(--display);
  font-weight: 700;
  font-size: 6.5px;
  letter-spacing: 0.16em;
  text-indent: 0.08em;
  color: rgba(225, 232, 250, 0.85);
}

.tc-min { font-size: 8px; font-weight: 700; color: rgba(170, 185, 220, 0.7); }

/* ---- Ranks: pinned week-end payout preview under the player row.
   ONE CARD with the YOU row above it (ui.js adds .has-payout to that row):
   the row keeps the top and sides of the pink border, this strip carries
   the sides and bottom, and the seam between them is a single internal
   hairline (no more stacked amber-on-pink double border butting into the
   next rank row). */
.l-payout {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 3px 9px;
  margin: 0;
  padding: 7px 10px 6px;
  border-radius: 0 0 11px 11px;
  background: rgba(46, 12, 32, 0.5);
  border: 1px solid rgba(255, 45, 149, 0.7);
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 6px 16px rgba(255, 45, 149, 0.15);
  font-family: var(--display);
  font-weight: 700;
  font-size: 7.5px;
  letter-spacing: 0.1em;
}

/* the row half of the merged card: square off the seam edge and hand the
   bottom border to the strip */
.lrow.you.has-payout {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  border-bottom: none;
}

/* normal list rhythm resumes below the merged card */
.l-payout + .lrow { margin-top: 5px; }

.lp-label { color: rgba(255, 233, 192, 0.92); }

.lp-amt {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: #ffd376;
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.45);
}

.lp-amt svg { width: 9px; height: 9px; fill: currentColor; }

.lp-amt.gem { color: #d9b8ff; text-shadow: 0 0 8px rgba(160, 100, 255, 0.5); }

.lp-amt.gem svg { fill: #b06bff; }

.lp-when {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: rgba(170, 185, 220, 0.6);
  text-shadow: none;
}

.lp-when svg { width: 9px; height: 9px; stroke: currentColor; fill: none; }

/* ---- NUMERAL LEGIBILITY (global fix): Orbitron's stylized digit 7 reads
   as a mirrored L — unacceptable anywhere money or time is quoted. The
   packaged latin subset ships no alternate glyph (no salt/ss01 coverage),
   so every currency, price, reward, timer and countdown numeral renders in
   the clean system stack with tabular figures instead. Orbitron stays on
   pure display text (wordmark, labels, HUD score/speed, ladder names). */
:root {
  --num: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto,
    'Helvetica Neue', Arial, sans-serif;
}

.chips-num, .gems-num,                                   /* wallets */
.price-tag, .gtag.price, .st-value, .sti,                /* store prices */
.gp-amt, .gp-bonus, .u-buy, .ex-price,                   /* packs / upgrades */
.m-reward, .m-val, .sp-amt, .sp-day, .rw-amt, .lp-amt,   /* rewards */
#over-chips, #double-btn b, .cs-missions,                /* results payouts */
.on-score, .on-gap, .on-reward, .ot-pct, .tc-min,        /* ladder numerals */
.ow-total, .ow-delta, .ow-goal, .ow-foot, .tc-gem, .rf-val,  /* season lane */
.l-score,                       /* board scores feed rank payouts — and the
                                   forward hook quotes the same values */
.d-state, .d-sub, .m-expire, #mission-foot,              /* countdowns */
.ft-rotates, #cont-secs {                                /* timers */
  font-family: var(--num);
  font-variant-numeric: tabular-nums;
}

/* ================================================================== */
/* ROUND: lobby exit + settings + radio stations + results hierarchy  */
/* ================================================================== */

/* ---- hub utility corner: settings gear + mute, top-LEFT (mirrors the
   wallet's top-right anchor so the hub's chrome frames the wordmark) */
#hub-corner {
  position: absolute;
  top: calc(19px + var(--safe-top));
  left: 14px;
  display: flex;
  gap: 8px;
}

#hub-corner button {
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 8, 24, 0.55);
  border: 1px solid rgba(120, 140, 200, 0.25);
  border-radius: 12px;
  width: 42px;
  height: 42px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: rgba(215, 228, 255, 0.92);
  cursor: pointer;
  transition: transform 0.12s, border-color 0.15s, color 0.15s;
}

#hub-corner button:active { transform: scale(0.92); }

#hub-corner button svg { width: 20px; height: 20px; display: block; }

#settings-btn:active svg { transform: rotate(30deg); transition: transform 0.2s; }

/* hub mute mirrors the HUD mute's state language */
#hub-mute-btn .spk-wave { transition: opacity 0.15s; }
#hub-mute-btn .spk-slash { opacity: 0; stroke: var(--magenta); transition: opacity 0.15s; }
#hub-mute-btn.muted .spk-wave { opacity: 0; }
#hub-mute-btn.muted .spk-slash { opacity: 1; }
#hub-mute-btn.muted { color: rgba(198, 206, 232, 0.78); }

/* ---- results LOBBY button: secondary neutral under DRIVE AGAIN */
.over-buttons { gap: 10px; }

/* stacked layout (DRIVE AGAIN full-width, LOBBY + SHARE beneath): every
   label holds one line down to 360px with room to spare */
.over-buttons button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 13px 10px;
  font-size: clamp(11px, 3.2vw, 14px);
  letter-spacing: 0.08em;
  white-space: nowrap;
}

/* the primary reads a size up from its two neighbours */
#restart-btn { font-size: clamp(13px, 3.9vw, 16px); letter-spacing: 0.12em; }

#lobby-btn,
#share-btn {
  background: rgba(18, 16, 36, 0.6);
  border: 1px solid rgba(120, 140, 200, 0.35);
  box-shadow: inset 0 0 12px rgba(0, 0, 0, 0.4);
  color: rgba(212, 224, 248, 0.9);
}

#lobby-btn svg {
  width: 15px;
  height: 15px;
  flex: none;
  fill: currentColor;
}

/* pause LOBBY shares the house glyph */
#pause-quit-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}

#pause-quit-btn svg { width: 14px; height: 14px; flex: none; fill: currentColor; }

/* --------------------------------------------------------- settings */
#settings-screen {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.settings-center {
  display: flex;
  justify-content: center;
  padding: calc(66px + var(--safe-top)) 14px calc(16px + var(--safe-bottom));
  max-height: 100%;
  overflow-y: auto;
  touch-action: pan-y;
  /* THE SCROLL CONTAINER MUST TAKE POINTER EVENTS. .layer sets
     pointer-events: none and only #settings-panel re-enabled them -- so the
     element that actually scrolls could not receive a touch, and the panel
     was unscrollable on device no matter how tall it got. Invisible while the
     panel fitted on screen; a hard wall the moment it did not. */
  pointer-events: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}

/* balances the BACK button so the SETTINGS title sits centered */
.panel-top-spacer { width: 86px; flex: none; }

#settings-panel {
  pointer-events: auto;
  width: min(94vw, 410px);
  margin: auto 0;
  padding: 11px clamp(14px, 4vw, 20px) 11px;
  border-radius: 20px;
  background: linear-gradient(180deg, rgba(16, 12, 34, 0.8), rgba(10, 7, 22, 0.88));
  border: 1px solid rgba(0, 229, 255, 0.3);
  box-shadow:
    0 0 40px rgba(0, 229, 255, 0.1),
    0 14px 44px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(160, 230, 255, 0.12),
    inset 0 0 34px rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  animation: panelIn 0.45s cubic-bezier(0.2, 1.4, 0.4, 1);
}

.set-sub {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin: 9px 2px 6px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: 0.26em;
  color: rgba(190, 230, 255, 0.85);
  text-shadow: 0 0 8px rgba(0, 229, 255, 0.35);
}

.set-sub:first-child { margin-top: 2px; }

.set-sub small {
  font-size: 7.5px;
  letter-spacing: 0.16em;
  color: rgba(170, 185, 220, 0.5);
}

.set-row {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 8px 12px;
  border-radius: 13px;
  background: rgba(22, 18, 44, 0.55);
  border: 1px solid rgba(120, 140, 200, 0.22);
}

.set-row + .set-row { margin-top: 8px; }

.set-ico {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  color: #9beeff;
  background: rgba(0, 229, 255, 0.09);
  border: 1px solid rgba(0, 229, 255, 0.3);
  box-shadow: inset 0 0 10px rgba(0, 229, 255, 0.08);
}

.set-ico svg { width: 19px; height: 19px; }

/* TAPPABLE ICONS ARE 44x44, FOR REAL.
   The MUSIC and SFX speakers are controls (tap = mute/full for that bus) while
   the bell and haptics icons beside them are decoration. Made interactive
   without being made bigger, they sat at 36x36 -- the only sub-44 targets in
   the whole app per tools/touch-audit.mjs.
   FIRST ATTEMPT WAS A TRANSPARENT ::after AT inset: -4px, on the theory that
   the drawn box should stay 36 to match its decorative neighbours. The audit
   hit-tested that at 42x42, not 44: the walk works in whole CSS pixels from a
   probe origin and loses one each side to the fractional box position. Padding
   an overlay until a hit-test agrees is fighting the measurement rather than
   the problem.
   So the box is genuinely 44. The rows are ~90px tall (they hold a label and a
   slider), so nothing reflows -- and the two controls now being visibly larger
   than the two decorations is an AFFORDANCE, not a rhythm break. The glyph
   inside stays 19px, so the icon reads the same weight. */
.set-ico.tappable {
  position: relative;
  width: 44px;
  height: 44px;
  border-radius: 12px;
  /* a shade stronger than the decorative plate, so "this one does something"
     survives being seen for the first time */
  background: rgba(0, 229, 255, 0.13);
  border-color: rgba(0, 229, 255, 0.42);
}

/* it is a control, so it says so on press and on keyboard focus */
.set-ico.tappable:active { transform: scale(0.94); }
.set-ico.tappable:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 2px;
}

.set-main { flex: 1; min-width: 0; }

.set-row-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.set-name {
  font-family: var(--display);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.18em;
  color: #eaf2ff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}

.set-name em {
  font-style: normal;
  margin-left: 5px;
  color: #9beeff;
  text-shadow: 0 0 8px rgba(0, 229, 255, 0.55);
}

.set-val {
  font-family: var(--num);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: 11.5px;
  color: #ffd376;
  text-shadow: 0 0 8px rgba(255, 179, 0, 0.45);
}

.set-hint {
  margin-top: 4px;
  font-family: var(--display);
  font-weight: 600;
  font-size: 7.5px;
  letter-spacing: 0.14em;
  color: rgba(170, 185, 220, 0.55);
}

#quality-hint { margin: 7px 4px 0; }

/* volume sliders: skewed neon track with a lit fill (via --fill, set by
   ui.js) and a glowing thumb — matches the nitro bar's instrument language */
.vol-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 22px;
  margin-top: 7px;
  background: transparent;
  cursor: pointer;
  --fill: 100%;
}

.vol-slider::-webkit-slider-runnable-track {
  height: 8px;
  border-radius: 5px;
  background:
    linear-gradient(90deg, rgba(0, 229, 255, 0.85), rgba(0, 229, 255, 0.55)) 0 0 / var(--fill) 100% no-repeat,
    rgba(10, 8, 24, 0.8);
  border: 1px solid rgba(120, 140, 200, 0.3);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.6), 0 0 10px rgba(0, 229, 255, 0.12);
}

.vol-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  margin-top: -6px;
  border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, #ffffff, #9beeff 55%, #19b6cf);
  border: 1px solid rgba(220, 250, 255, 0.9);
  box-shadow: 0 0 12px rgba(0, 229, 255, 0.75), 0 1px 4px rgba(0, 0, 0, 0.6);
}

.vol-slider::-moz-range-track {
  height: 8px;
  border-radius: 5px;
  background: rgba(10, 8, 24, 0.8);
  border: 1px solid rgba(120, 140, 200, 0.3);
}

.vol-slider::-moz-range-progress {
  height: 8px;
  border-radius: 5px;
  background: linear-gradient(90deg, rgba(0, 229, 255, 0.85), rgba(0, 229, 255, 0.55));
}

.vol-slider::-moz-range-thumb {
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, #ffffff, #9beeff 55%, #19b6cf);
  border: 1px solid rgba(220, 250, 255, 0.9);
  box-shadow: 0 0 12px rgba(0, 229, 255, 0.75);
}

/* graphics quality selector: the garage-tab plate language */
#quality-row {
  display: flex;
  gap: 7px;
}

.qbtn {
  pointer-events: auto;
  cursor: pointer;
  flex: 1 1 0;
  min-height: 36px;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(8.5px, 2.4vw, 10.5px);
  letter-spacing: 0.1em;
  color: rgba(175, 195, 230, 0.68);
  background: rgba(22, 18, 44, 0.55);
  border: 1px solid rgba(120, 140, 200, 0.22);
  border-radius: 9px;
  transform: skewX(-6deg);
  transition: color 0.15s, border-color 0.15s, box-shadow 0.15s, background 0.15s;
}

.qbtn.active {
  color: #fff;
  text-shadow: 0 0 9px rgba(0, 229, 255, 0.85);
  border-color: rgba(0, 229, 255, 0.75);
  background: linear-gradient(180deg, rgba(0, 229, 255, 0.17), rgba(0, 90, 130, 0.2));
  box-shadow: 0 0 15px rgba(0, 229, 255, 0.32), inset 0 0 11px rgba(0, 229, 255, 0.13);
}

/* haptics toggle pill */
.toggle-pill {
  pointer-events: auto;
  cursor: pointer;
  flex: none;
  position: relative;
  width: 46px;
  height: 24px;
  border-radius: 999px;
  background: rgba(10, 8, 24, 0.8);
  border: 1px solid rgba(120, 140, 200, 0.35);
  box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.6);
  transition: background 0.18s, border-color 0.18s, box-shadow 0.18s;
}

.toggle-pill span {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #6b7ba0;
  transition: transform 0.18s cubic-bezier(0.2, 1.2, 0.4, 1), background 0.18s, box-shadow 0.18s;
}

.toggle-pill.on {
  background: linear-gradient(180deg, rgba(0, 229, 255, 0.35), rgba(0, 110, 150, 0.4));
  border-color: rgba(0, 229, 255, 0.75);
  box-shadow: 0 0 12px rgba(0, 229, 255, 0.3), inset 0 0 8px rgba(0, 229, 255, 0.15);
}

.toggle-pill.on span {
  transform: translateX(22px);
  background: radial-gradient(circle at 40% 35%, #ffffff, #9beeff 60%, #2cc8e0);
  box-shadow: 0 0 10px rgba(0, 229, 255, 0.8);
}

/* long boolean labels (notifications row) stay on one line beside the pill */
.set-name.long {
  font-size: 9px;
  letter-spacing: 0.12em;
}

/* -------------------------------------- support & legal (settings) */
/* RESTORE PURCHASES reuses the .set-row plate as a tappable button */
button.set-row {
  pointer-events: auto;
  cursor: pointer;
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
}

button.set-row:active { transform: scale(0.985); }

.lr-chev {
  flex: none;
  display: flex;
  color: rgba(155, 238, 255, 0.65);
}

.lr-chev svg { width: 14px; height: 14px; }

/* compact chevron link rows: privacy / terms / support (stub links) */
#legal-links {
  margin-top: 7px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.legal-link {
  pointer-events: auto;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 30px;
  padding: 0 12px;
  border-radius: 11px;
  background: rgba(22, 18, 44, 0.4);
  border: 1px solid rgba(120, 140, 200, 0.18);
  font-family: var(--display);
  font-weight: 700;
  font-size: 9.5px;
  letter-spacing: 0.18em;
  color: rgba(212, 226, 250, 0.85);
}

.legal-link:active { transform: scale(0.985); }

.legal-link svg {
  flex: none;
  width: 13px;
  height: 13px;
  color: rgba(155, 238, 255, 0.55);
}

/* credits line */
#settings-credits {
  margin-top: 9px;
  padding-top: 8px;
  border-top: 1px solid rgba(120, 140, 200, 0.16);
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 8px;
  font-family: var(--display);
  font-weight: 600;
  font-size: 8px;
  letter-spacing: 0.18em;
  color: rgba(170, 185, 220, 0.55);
}

#settings-credits .logo-mini {
  font-weight: 900;
  font-style: italic;
  font-size: 10px;
  background: linear-gradient(90deg, #6fe6ff, #ff56ad);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 0 6px rgba(0, 229, 255, 0.4));
}

/* ------------------------------------------------ gem-spend confirm */
#confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(6, 4, 14, 0.62);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  transition: opacity 0.2s ease;
}

.confirm-panel {
  pointer-events: auto;
  text-align: center;
  width: min(84vw, 320px);
  padding: 22px clamp(18px, 5vw, 26px) 18px;
  border-radius: 20px;
  background: rgba(18, 12, 36, 0.92);
  border: 1px solid rgba(160, 100, 255, 0.55);
  box-shadow:
    0 0 50px rgba(160, 100, 255, 0.22),
    0 18px 50px rgba(0, 0, 0, 0.6),
    inset 0 0 34px rgba(0, 0, 0, 0.45);
  animation: panelIn 0.3s cubic-bezier(0.2, 1.4, 0.4, 1);
}

.cf-title {
  font-family: var(--display);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  color: rgba(215, 200, 255, 0.8);
  text-shadow: 0 0 8px rgba(160, 100, 255, 0.4);
}

#cf-item {
  margin-top: 9px;
  font-family: var(--display);
  font-weight: 900;
  font-style: italic;
  font-size: clamp(16px, 5vw, 20px);
  letter-spacing: 0.1em;
  color: #fff;
  text-shadow: 0 0 14px rgba(160, 100, 255, 0.7);
}

#cf-price {
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

.cf-cost {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 16px;
  border-radius: 999px;
  font-family: var(--num);
  font-variant-numeric: tabular-nums;
  font-weight: 800;
  font-size: 16px;
  color: #f3ecff;
  background: linear-gradient(180deg, rgba(150, 90, 255, 0.35), rgba(70, 30, 140, 0.4));
  border: 1px solid rgba(190, 140, 255, 0.65);
  box-shadow: 0 0 16px rgba(150, 90, 255, 0.3);
  text-shadow: 0 1px 2px rgba(40, 10, 90, 0.7);
}

.cf-cost svg { width: 15px; height: 15px; fill: #d9b8ff; filter: drop-shadow(0 0 5px rgba(180, 120, 255, 0.8)); }

.cf-bal {
  font-family: var(--num);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: 10px;
  letter-spacing: 0.1em;
  color: rgba(190, 205, 235, 0.65);
}

.cf-buttons {
  margin-top: 16px;
  display: flex;
  gap: 10px;
}

.cf-buttons button {
  pointer-events: auto;
  cursor: pointer;
  flex: 1 1 0;
  min-height: 46px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 11.5px;
  letter-spacing: 0.14em;
  text-indent: 0.14em;
  color: #fff;
  border: none;
  border-radius: 12px;
  transform: skewX(-6deg);
  transition: transform 0.12s, filter 0.12s;
}

.cf-buttons button:active { transform: skewX(-6deg) scale(0.94); filter: brightness(1.3); }

#cf-cancel {
  color: rgba(205, 218, 245, 0.85);
  background: rgba(18, 16, 36, 0.7);
  border: 1px solid rgba(120, 140, 200, 0.3);
  box-shadow: inset 0 0 12px rgba(0, 0, 0, 0.4);
}

#cf-buy {
  font-weight: 900;
  background: linear-gradient(180deg, #c9a1ff, #8f45ff 55%, #5b21b8);
  border: 1px solid rgba(220, 195, 255, 0.75);
  box-shadow: 0 0 18px rgba(150, 90, 255, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.35);
}

/* ------------------------------------------------- RADIO stations */
#radio-grid {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.station {
  pointer-events: auto;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 11px;
  border-radius: 13px;
  text-align: left;
  /* the RADIO shelf keys off its own violet dial icon instead of sitting in
     neutral slate — four flat gray rows in the middle of the store were a
     big part of what read as 'the store went uniformly gray' */
  background:
    linear-gradient(100deg, rgba(150, 90, 255, 0.11), transparent 58%),
    rgba(22, 18, 44, 0.58);
  border: 1px solid rgba(160, 100, 255, 0.24);
  transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}

.station.playing {
  border-color: rgba(0, 229, 255, 0.6);
  background: linear-gradient(180deg, rgba(0, 229, 255, 0.1), rgba(0, 70, 100, 0.14));
  box-shadow: 0 0 16px rgba(0, 229, 255, 0.2), inset 0 0 12px rgba(0, 229, 255, 0.08);
}

.rs-ico {
  position: relative;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 11px;
  color: #d9b8ff;
  background: rgba(150, 90, 255, 0.12);
  border: 1px solid rgba(160, 100, 255, 0.4);
}

.rs-ico > svg { width: 20px; height: 20px; }

.station.playing .rs-ico {
  color: #9beeff;
  background: rgba(0, 229, 255, 0.1);
  border-color: rgba(0, 229, 255, 0.5);
}

/* animated EQ bars replace the static glyph on the audible station */
.rs-eq {
  position: absolute;
  inset: 0;
  display: none;
  align-items: flex-end;
  justify-content: center;
  gap: 2.5px;
  padding-bottom: 9px;
  background: rgba(6, 10, 20, 0.85);
  border-radius: 10px;
}

.station.playing .rs-eq { display: flex; }
.station.playing .rs-ico > svg { opacity: 0; }

.rs-eq i {
  width: 3.5px;
  border-radius: 2px;
  background: linear-gradient(180deg, #9beeff, #00e5ff);
  box-shadow: 0 0 6px rgba(0, 229, 255, 0.7);
  animation: eqBounce 0.9s ease-in-out infinite;
}

.rs-eq i:nth-child(1) { height: 10px; animation-delay: -0.2s; }
.rs-eq i:nth-child(2) { height: 17px; animation-delay: -0.55s; }
.rs-eq i:nth-child(3) { height: 13px; animation-delay: -0.8s; }
.rs-eq i:nth-child(4) { height: 19px; animation-delay: -0.35s; }

@keyframes eqBounce {
  0%, 100% { transform: scaleY(0.45); }
  50% { transform: scaleY(1); }
}

.rs-main { flex: 1 1 0; min-width: 0; }

.rs-name {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-family: var(--display);
  font-weight: 700;
  /* a hair tighter so 'SUNSET DRIVE' + its NOW PLAYING pill both fit the
     row's text column at 360px instead of one of them being cut */
  font-size: 10.5px;
  letter-spacing: 0.1em;
  color: #eaf2ff;
  white-space: nowrap;
}

/* the shrink point of the row: the NAME gives way, never the NOW PLAYING /
   ON AIR plates, so those two can never share a pixel */
.rs-title {
  min-width: 0;
  flex: 0 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
}

.rs-now {
  flex: none;
  max-width: 100%;
  padding: 2.5px 6px;
  border-radius: 999px;
  font-size: 6px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-indent: 0.06em;
  color: #052028;
  background: linear-gradient(180deg, #9beeff, #00cbe8);
  box-shadow: 0 0 10px rgba(0, 229, 255, 0.55);
  animation: pulse 1.6s ease-in-out infinite;
}

.rs-sub {
  margin-top: 3px;
  font-family: var(--display);
  font-weight: 600;
  font-size: 7.5px;
  letter-spacing: 0.14em;
  color: rgba(170, 185, 220, 0.6);
  /* one line: 'CLASSIC SYNTHWAVE · 108 BPM' wrapping to two made the first
     station row a head taller than the three under it */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.rs-equip {
  pointer-events: auto;
  cursor: pointer;
  flex: none;
  padding: 8px 13px;
  border-radius: 10px;
  font-family: var(--display);
  font-weight: 800;
  font-size: 10px;
  letter-spacing: 0.14em;
  text-indent: 0.07em;
  color: #d8f6ff;
  text-shadow: 0 0 8px rgba(0, 229, 255, 0.55);
  background: rgba(8, 14, 30, 0.6);
  border: 1px solid rgba(0, 229, 255, 0.5);
  box-shadow: 0 0 14px rgba(0, 229, 255, 0.18), inset 0 0 12px rgba(0, 229, 255, 0.07);
  transition: transform 0.12s, filter 0.12s;
}

.rs-equip:active { transform: scale(0.94); filter: brightness(1.35); }

/* chips-priced plate variant (station shelf): amber chips language */
.price-tag.chips-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: #2a1c02;
  background: linear-gradient(180deg, #ffe9b0, #ffb300 55%, #b87a06);
  border: 1px solid rgba(255, 226, 160, 0.8);
  box-shadow: 0 0 14px rgba(255, 179, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.5);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
}

.price-tag.chips-tag svg { width: 11px; height: 11px; fill: #5a3c05; }

/* purchase / deny flourish parity with the other store tiles */
.station.bought { animation: cardPop 0.5s cubic-bezier(0.2, 1.6, 0.4, 1); }
.station.deny { animation: cardShake 0.4s ease; }

/* ------------------------------------- results: itemized earn rows */
/* prestige rows can carry a one-time gem drip beside the chips amount */
.rw-amt.rw-gems {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: #d9b8ff;
  text-shadow: 0 0 10px rgba(160, 100, 255, 0.7);
}

.rw-amt.rw-gems svg { width: 11px; height: 11px; fill: #b06bff; filter: drop-shadow(0 0 4px rgba(160, 100, 255, 0.7)); }

/* ------------------------------- results: collapsed mission strip */
/* one-line summary so DRIVE AGAIN / LOBBY stay above the fold; tapping
   expands the shared mission rows (.om-body) in place */
#over-missions { margin-top: 10px; }

.om-strip {
  pointer-events: auto;
  cursor: pointer;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 11px;
  border-radius: 12px;
  background: rgba(22, 18, 44, 0.55);
  border: 1px solid rgba(120, 140, 200, 0.25);
  transition: border-color 0.15s, box-shadow 0.15s;
}

.om-strip.hot {
  border-color: rgba(255, 179, 0, 0.55);
  box-shadow: 0 0 14px rgba(255, 179, 0, 0.16), inset 0 0 10px rgba(255, 179, 0, 0.06);
}

.om-icons {
  display: inline-flex;
  gap: 4px;
  flex: none;
}

.om-icons i {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 6px;
  background: rgba(10, 8, 24, 0.7);
  border: 1px solid rgba(120, 140, 200, 0.25);
  color: rgba(190, 210, 245, 0.8);
}

.om-icons i svg { width: 12px; height: 12px; }

.om-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  font-family: var(--display);
  font-weight: 700;
  font-size: 9px;
  letter-spacing: 0.16em;
  color: #eaf2ff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}

.om-claim {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 9px;
  border-radius: 999px;
  font-family: var(--num);
  font-variant-numeric: tabular-nums;
  font-weight: 800;
  font-size: 10.5px;
  color: #2a1c02;
  background: linear-gradient(180deg, #ffe9b0, #ffb300 60%, #cf8d04);
  border: 1px solid rgba(255, 226, 160, 0.8);
  box-shadow: 0 0 12px rgba(255, 179, 0, 0.45);
  animation: pulse 1.4s ease-in-out infinite;
}

.om-claim svg { width: 10px; height: 10px; fill: #5a3c05; }

.om-view {
  flex: none;
  font-family: var(--display);
  font-weight: 700;
  font-size: 8px;
  letter-spacing: 0.18em;
  color: rgba(170, 185, 220, 0.6);
}

.om-chev {
  flex: none;
  display: flex;
  color: rgba(170, 185, 220, 0.7);
  transition: transform 0.2s ease;
}

.om-chev svg { width: 13px; height: 13px; }

.om-strip.open .om-chev { transform: rotate(180deg); }

.om-body { display: none; }

#over-missions.open .om-body { display: block; animation: sheetIn 0.25s ease; }

#over-missions .m-caption { margin: 8px 2px 6px; }

/* ------------------------------- ranks: IDENTITY STRIP over the header
   column. The tier name worn as a TITLE (the identity layer of the climb):
   one centered line, shield tinted by the shared --pt tier hooks above,
   name at display weight. Unranked shows the first name to earn, dimmed. */
#ranks-identity {
  display: flex;
  flex-wrap: wrap;              /* the long unranked line may break, never overflow */
  align-items: center;
  justify-content: center;
  gap: 5px 7px;
  margin: 0 2px 9px;
  padding: 7px 10px;
  border-radius: 11px;
  border: 1px solid rgba(var(--pt-rgb, 120, 140, 200), 0.35);
  background: rgba(var(--pt-rgb, 20, 18, 40), 0.08);
}

#ranks-identity.hidden { display: none; }   /* no ghost gap pre-render */

#ranks-identity svg {
  width: 12px;
  height: 12px;
  flex: none;
  fill: var(--pt, rgba(150, 162, 194, 0.6));
  filter: drop-shadow(0 0 6px rgba(var(--pt-rgb, 120, 140, 200), 0.6));
}

/* face/weight/tracking come from the TYPE PAIR labels group at the end of
   the sheet (.ri-you rides it): only size and color are stated here */
.ri-you {
  font-size: 7px;
  color: rgba(200, 212, 240, 0.7);
}

.ri-name {
  font-family: var(--display);
  font-weight: 900;
  font-style: italic;
  font-size: 11.5px;
  letter-spacing: 0.16em;
  text-indent: 0.08em;
  white-space: nowrap;
  color: var(--pt, rgba(190, 205, 235, 0.8));
  text-shadow: 0 0 12px rgba(var(--pt-rgb, 120, 140, 200), 0.55), 0 1px 3px rgba(0, 0, 0, 0.8);
}

/* unranked: the strip stays quiet — an aspiration, not an award */
#ranks-identity.unranked { opacity: 0.75; }
#ranks-identity.unranked .ri-name { font-size: 9px; }

/* ------------------------------- ranks: CAREER TIER header + column */
#ranks-career {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 3px 10px;
  margin: 0 2px 7px;
}

.rc-label {
  font-family: var(--display);
  font-weight: 800;
  font-size: 9.5px;
  letter-spacing: 0.18em;
  color: #ffd376;
  text-shadow: 0 0 10px rgba(255, 179, 0, 0.45), 0 1px 3px rgba(0, 0, 0, 0.8);
  white-space: nowrap;
}

.rc-next {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 7.5px;
  letter-spacing: 0.14em;
  color: rgba(190, 205, 235, 0.65);
  white-space: nowrap;
}

.rc-next svg { width: 9px; height: 9px; fill: rgba(190, 205, 235, 0.55); }

/* micro progress fill toward the next tier gate, spanning both columns */
.rc-bar {
  grid-column: 1 / -1;
  height: 3px;
  border-radius: 2px;
  background: rgba(10, 8, 24, 0.8);
  border: 1px solid rgba(120, 140, 200, 0.2);
  overflow: hidden;
}

.rc-bar i {
  display: block;
  height: 100%;
  transform-origin: left;
  background: linear-gradient(90deg, #ffb300, #ffd376);
  box-shadow: 0 0 8px rgba(255, 179, 0, 0.7);
}

/* board column header: names the score lane per period ('WEEK SCORE') */
.l-head {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 2px 10px 5px 8px;   /* mirrors .lrow padding so columns align */
  font-family: var(--display);
  font-weight: 700;
  font-size: 7px;
  letter-spacing: 0.2em;
  color: rgba(170, 185, 220, 0.5);
}

.l-head .l-rank {
  width: 30px;
  flex: none;
  text-align: center;
  background: none;
  border: none;
  box-shadow: none;
  color: rgba(170, 185, 220, 0.5);
  font-size: 7px;
  font-style: normal;
}

.l-head .l-name { flex: 1; color: rgba(170, 185, 220, 0.5); font-size: 7px; }

.lh-score { flex: none; }

/* ==================================================================== */
/* REDUCED MOTION — photosensitivity safe mode (settings ACCESSIBILITY). */
/* game.js toggles body.reduced-motion off the persisted flag            */
/* (meta.settings.reducedMotion; boot default honors the OS              */
/* prefers-reduced-motion hint). Strobe / flash / pulse loops stop —     */
/* one-shot fades, slides and layout transitions stay. The render-side   */
/* twin is the uReduced hook in rendering.js (nitro ray flicker), and    */
/* updateCamera damps the shake off the same setting.                    */

/* the whole HUD + full-frame FX layers: nitro flash/ignite strobes,
   speedo surge, drift hum, combo hot-pulse, popup pulses, warp spokes,
   streak bars, crash flash and the pulsing boost vignette all live here */
body.reduced-motion #hud,
body.reduced-motion #hud *,
body.reduced-motion #nitro-fx,
body.reduced-motion #nitro-fx *,
body.reduced-motion #flash,
body.reduced-motion #flash *,
body.reduced-motion #vignette-pulse,
body.reduced-motion #popups *,
body.reduced-motion .popup-anchor * {
  animation: none !important;
}

/* boost vignette holds a steady dim rim instead of pulsing */
body.reduced-motion #vignette-pulse.on { opacity: 0.5; }

/* wreck fire flicker + rising embers behind the results card */
body.reduced-motion #over-fx,
body.reduced-motion #over-fx * {
  animation: none !important;
}

/* menu pulse loops: claim buttons, breathing claimable cards, the ready
   daily row, the hot results mission strip */
body.reduced-motion .m-claim,
body.reduced-motion .mcard.claimable,
body.reduced-motion #daily-row.ready,
body.reduced-motion #evening-row.ready,
body.reduced-motion #commit-row.ready,
body.reduced-motion .d-claim,
body.reduced-motion .om-strip.hot .om-claim,
body.reduced-motion #bonus-badge,
/* the garage try-on bar's breathing PREVIEW chips (bar + card tag) */
body.reduced-motion .gpv-tag,
body.reduced-motion .gtag.preview {
  animation: none !important;
}

/* =====================================================================
   COMPLIANCE STATES — a build that cannot charge must not look like one
   that can. These are deliberate, designed states, not dimmed prices:
   nothing here shows a dollar amount, and nothing here invites a tap that
   would end in a payment sheet that does not exist.
   ===================================================================== */

/* the APP STORE ONLY plate that replaces every real-money price tag when
   PurchaseProvider has no native bridge (ui.js _usdPlate).
   HONEST, NOT DEAD. Round 29 painted this plate — and with it every hero
   tile on the shelf — flat slate, and since the web build has no StoreKit
   bridge that meant the WHOLE store rendered gray (art 8.4 -> 6.5). The
   compliance requirement is that it must not read as a price or invite a
   payment sheet; it never said the shelf has to lose its neon. So: cyan
   'state' language (the same cyan the game uses for locked/info chrome),
   never the green of a live $-plate and never the violet of a gem price. */
.price-tag.unavail {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 10px;
  font-size: 8px;
  letter-spacing: 0.13em;
  color: #a9dcf2;
  background: linear-gradient(180deg, rgba(13, 30, 48, 0.88), rgba(9, 18, 34, 0.92));
  border: 1px solid rgba(0, 229, 255, 0.26);
  box-shadow: inset 0 1px 0 rgba(190, 240, 255, 0.09);
  text-shadow: 0 0 6px rgba(0, 229, 255, 0.3);
}

.price-tag.unavail svg { fill: #6fd4ee; filter: none; }

/* the gem shelf without prices: quantities stay (they are true) and the
   value-badge chrome goes — but the VIOLET stays. A gem pack is the store's
   premium tier whether or not this build can charge for it, and dimming the
   whole grid to 0.82 gray is what flattened the shelf's hierarchy. */
.gpack.unavail {
  border-color: rgba(160, 100, 255, 0.34);
  box-shadow: 0 0 14px rgba(150, 90, 255, 0.1), inset 0 0 16px rgba(0, 0, 0, 0.35);
}

.gpack.unavail .gp-amt { color: #ecdfff; }

/* the flagship pack keeps its amber halo even with no POPULAR badge to
   carry — the badge was a value CLAIM (correctly dropped without prices),
   the halo is just hierarchy, and without it the four packs read as one
   undifferentiated gray block */
.gpack.hot.unavail {
  border-color: rgba(255, 179, 0, 0.5);
  box-shadow: 0 0 16px rgba(255, 179, 0, 0.13), inset 0 0 16px rgba(0, 0, 0, 0.35);
}

/* the two real-money hero tiles keep their full paint in every build: the
   blanket opacity knock-down here was the single biggest cause of the
   'uniformly gray store' note */
#store-panel.no-iap #starter-tile,
#store-panel.no-iap #circuitpass-tile { opacity: 1; }

/* the footer is a statement of fact in both states — never a confession */
.store-foot { line-height: 1.55; padding: 0 10px; }

/* a settings row whose native seam is missing: readable, obviously inert */
.set-row.disabled { opacity: 0.55; }
.set-row.disabled .toggle-pill { pointer-events: none; filter: grayscale(0.8); }
.set-row.disabled .set-name em { color: rgba(180, 195, 225, 0.7); }

/* an unconfigured legal URL removes its row outright (see APP_LINKS) —
   .hidden alone only makes it invisible, which would leave a dead gap */
.legal-link.hidden,
#legal-links.hidden { display: none; }

/* ---------------------------------------------------------------------
   AD-FREE PASS: the three rewarded buttons relabel themselves when the
   pass is owned, so the player sees what they bought BEFORE the tap.
   Cyan/bolt instead of the amber play triangle.
   --------------------------------------------------------------------- */
#cont-pass.freebie {
  background: linear-gradient(180deg, #c8fbff, #47e6ff 55%, #12a6c8);
  border-color: rgba(200, 250, 255, 0.8);
}

/* MUST be display:none, not the global .hidden. `.hidden` sets opacity and
   visibility only, so the button KEEPS ITS BOX — measured 146x48 plus the 9px
   flex gap still sitting in a centred .cont-buttons row, which shoved the
   gems button ~78px off centre for every player without the pass. The revive
   card is the most time-pressured screen in the game; it does not get to be
   lopsided. */
#cont-pass.hidden { display: none; }

#double-btn.freebie svg { fill: #05323c; }

#double-btn.freebie em {
  font-style: normal;
  font-size: 8px;
  letter-spacing: 0.16em;
  opacity: 0.72;
}

/* =====================================================================
   FIRST-RUN EXPERIENCE
   Four beats, first boot only. The rule for all of them: they sit IN the
   world the player is already driving in — no modal, no dimmer, no OK
   button — and each one retires itself the moment the player does the
   thing it names.
   ===================================================================== */

/* ---- in-run coach chips, floating over the road ahead of the car ---- */
#ftue-coach {
  position: absolute;
  left: 0; right: 0;
  /* clear of the hero car's roofline — the coach floats on the road AHEAD
     of the player, never over the metal they are trying to look at */
  bottom: 44vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  pointer-events: none;
  z-index: 3;
}

#ftue-coach.hidden { display: none; }

.fc-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px 7px 11px;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(10px, 3vw, 12px);
  letter-spacing: 0.17em;
  color: #dcf8ff;
  text-shadow: 0 0 9px rgba(0, 229, 255, 0.6), 0 1px 3px rgba(0, 0, 0, 0.9);
  /* Deliberately light: on a real phone these sit at eye level down the road
     and the player is trying to see traffic THROUGH them. The plate reads as
     a tint, not a panel — legibility comes from the text shadow, not the fill. */
  background: rgba(6, 12, 28, 0.30);
  border: 1px solid rgba(0, 229, 255, 0.34);
  box-shadow: 0 0 18px rgba(0, 229, 255, 0.12), 0 6px 20px rgba(0, 0, 0, 0.3),
              inset 0 0 14px rgba(0, 229, 255, 0.05);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  opacity: 0;
  transform: translateY(9px);
  animation: fcIn 0.5s cubic-bezier(0.2, 0.9, 0.25, 1) forwards;
}

/* the NITRO chip is the second beat — it arrives a touch later so the two
   read as a sequence rather than a block of text */
#fc-nitro { animation-delay: 0.55s; }

.fc-chip.hidden { display: none; }

.fc-chip .fc-ico {
  display: inline-flex;
  width: 15px;
  height: 15px;
  flex: none;
  color: var(--cyan, #00e5ff);
}

.fc-chip .fc-ico svg { width: 100%; height: 100%; fill: currentColor; }
#fc-steer .fc-ico svg { fill: none; }

.fc-chip .fc-txt b {
  font-weight: 900;
  color: #7ff0ff;
  padding: 0 1px;
}

/* the tick that replaces the glyph the instant the verb happens */
.fc-chip .fc-check {
  display: none;
  width: 14px;
  height: 14px;
  flex: none;
  color: #3ee87f;
}

.fc-chip .fc-check svg { width: 100%; height: 100%; }

.fc-chip.done {
  color: #cdf6dd;
  border-color: rgba(62, 232, 127, 0.6);
  box-shadow: 0 0 18px rgba(62, 232, 127, 0.25), 0 6px 20px rgba(0, 0, 0, 0.5);
  text-shadow: 0 0 9px rgba(62, 232, 127, 0.55), 0 1px 3px rgba(0, 0, 0, 0.9);
  animation: none;
  opacity: 1;
  transform: none;
}

.fc-chip.done .fc-ico { display: none; }
.fc-chip.done .fc-check { display: inline-flex; }

/* collapse out — height and margin go with it so the survivor re-centres */
.fc-chip.gone {
  opacity: 0;
  transform: translateY(-8px) scale(0.94);
  transition: opacity 0.42s ease, transform 0.42s ease;
  pointer-events: none;
}

@keyframes fcIn {
  from { opacity: 0; transform: translateY(9px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ---- first CONTINUE? offer: one line, under the stake ---- */
#cont-first {
  margin: 6px auto 2px;
  max-width: 88%;
  text-align: center;
  font-family: var(--display);
  font-weight: 700;
  font-size: 8.5px;
  letter-spacing: 0.15em;
  line-height: 1.6;
  color: rgba(196, 214, 244, 0.78);
}

#cont-first.hidden { display: none; }

/* ---- first results card: where the welcome CHIPS spend ---- */
.rw-row.welcome { color: #cdf6dd; text-shadow: 0 0 9px rgba(62, 232, 127, 0.45), 0 1px 3px rgba(0, 0, 0, 0.8); }
.rw-row.welcome svg { fill: #3ee87f; filter: drop-shadow(0 0 4px rgba(62, 232, 127, 0.7)); }

#over-ftue {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  margin: 7px auto 0;
  padding: 6px 11px;
  max-width: 96%;
  border-radius: 10px;
  background: rgba(0, 229, 255, 0.07);
  border: 1px solid rgba(0, 229, 255, 0.26);
  font-family: var(--display);
  font-weight: 700;
  font-size: 8.5px;
  letter-spacing: 0.13em;
  line-height: 1.5;
  color: rgba(206, 240, 252, 0.9);
  text-align: left;
}

#over-ftue.hidden { display: none; }

#over-ftue svg {
  width: 14px;
  height: 14px;
  flex: none;
  fill: #7ff0ff;
  filter: drop-shadow(0 0 5px rgba(0, 229, 255, 0.7));
}

#over-ftue b { color: #7ff0ff; font-weight: 900; }

/* ---- hub: point at the GARAGE while the welcome CHIPS sit unspent ---- */
#ftue-garage-tip {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 9px);
  /* the CTA buttons are skewX(-8deg); counter it so the tip sits square */
  transform: translateX(-50%) skewX(8deg);
  white-space: nowrap;
  padding: 4px 9px;
  border-radius: 8px;
  font-family: var(--display);
  font-weight: 800;
  font-size: 8px;
  letter-spacing: 0.16em;
  color: #06202a;
  background: linear-gradient(180deg, #c8fbff, #47e6ff 60%, #12a6c8);
  border: 1px solid rgba(210, 250, 255, 0.8);
  box-shadow: 0 0 16px rgba(0, 229, 255, 0.5), 0 3px 10px rgba(0, 0, 0, 0.5);
  text-shadow: none;
  animation: ftueBob 1.6s ease-in-out infinite;
}

#ftue-garage-tip.hidden { display: none; }

/* the little caret pointing down into the button */
#ftue-garage-tip::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 100%;
  width: 7px;
  height: 7px;
  margin-left: -3.5px;
  margin-top: -4px;
  background: #2fc9e6;
  border-right: 1px solid rgba(210, 250, 255, 0.8);
  border-bottom: 1px solid rgba(210, 250, 255, 0.8);
  transform: rotate(45deg);
}

#garage-btn.ftue-point {
  border-color: rgba(0, 229, 255, 0.95);
  box-shadow: 0 0 22px rgba(0, 229, 255, 0.45), 0 4px 18px rgba(0, 0, 0, 0.45),
              inset 0 0 14px rgba(0, 229, 255, 0.14);
}

@keyframes ftueBob {
  0%, 100% { transform: translateX(-50%) skewX(8deg) translateY(0); }
  50%      { transform: translateX(-50%) skewX(8deg) translateY(-3px); }
}

/* photosensitivity safe mode: the coach and the pointer stop moving but
   stay fully legible — onboarding must never be the thing that is lost */
body.reduced-motion .fc-chip {
  animation: none !important;
  opacity: 1;
  transform: none;
}

body.reduced-motion #ftue-garage-tip {
  animation: none !important;
  transform: translateX(-50%) skewX(8deg);
}

.price-tag.unavail svg {
  width: 10px;
  height: 10px;
  flex: none;
  color: rgba(160, 180, 220, 0.8);
}

/* ---- SETTINGS RADIO PICKER + PERFORMANCE MODS ROW
   The store sells stations; this is where an owner picks one. Two columns so
   four stations fit without scrolling the panel, and locked rows stay visible
   (showing their price) so the shelf is discoverable without this row
   pretending to be a shop. */
#station-picker {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* EQUAL HEIGHT ACROSS ALL ROWS, not just within one. Grid stretches cells
     inside a row by default, so row 1 matched itself and row 2 matched
     itself -- and the two rows were different heights, because the blurbs
     wrap to a different number of lines ("CLASSIC SYNTHWAVE · 108 BPM" takes
     three lines in a half-width cell, "8" takes one). 1fr rows make every
     card the tallest card, so the 2x2 reads as a grid rather than four
     differently-sized tiles. */
  grid-auto-rows: 1fr;
  gap: 6px;
  margin: 4px 0 6px;
}

/* break the blurb evenly instead of leaving a one-word orphan line */
.stn-btn .stn-sub { text-wrap: balance; }

.stn-btn {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 8px 10px;
  border-radius: 10px;
  background: rgba(10, 16, 34, 0.5);
  border: 1px solid rgba(120, 140, 200, 0.2);
  color: #cfe0ff;
  font-family: var(--display);
  text-align: left;
  min-height: 44px;
}

.stn-btn .stn-name {
  font-weight: 800;
  font-size: 11px;
  letter-spacing: 0.1em;
}

.stn-btn .stn-sub {
  font-size: 10px;
  letter-spacing: 0.06em;
  opacity: 0.62;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.stn-btn .stn-sub svg { width: 10px; height: 10px; }

/* CURRENCY COLOUR, the same lanes the wallet pills and every price plate use:
   amber = CHIPS (earned), violet = GEMS (premium). Without this the two
   glyphs are the same silhouette at 10px and the shelf's one gem-priced
   station read as costing a handful of chips. Opacity is lifted off the
   parent's 0.62 so a price is never the dimmest thing on its own row. */
/* The PARENT carries the dim, and opacity COMPOUNDS -- a child cannot be less
   transparent than its parent -- so the lift has to happen on .stn-sub itself.
   A locked station's sub IS the price (the blurb is replaced by it), so the
   locked state is exactly the right hook. */
.stn-btn.locked .stn-sub { opacity: 1; }
.stn-btn .stn-cost { display: inline-flex; align-items: center; gap: 4px; }
.stn-btn .stn-cost.chips { color: var(--amber); }
.stn-btn .stn-cost.chips svg { fill: var(--amber); }
.stn-btn .stn-cost.gems { color: #c58bff; }
.stn-btn .stn-cost.gems svg { fill: #a855f7; }
.stn-btn .stn-cost.gems svg .gem-core { fill: #e9d5ff; }

/* equipped: the cyan lane the rest of the panel uses for "this is live" */
.stn-btn.on {
  background: rgba(0, 229, 255, 0.09);
  border-color: rgba(0, 229, 255, 0.5);
  color: #dff8ff;
  box-shadow: 0 0 14px rgba(0, 229, 255, 0.14);
}

/* locked reads as a price tag, not a dead control */
.stn-btn.locked { opacity: 0.5; }
.stn-btn.locked .stn-sub { opacity: 0.9; color: var(--amber); }


/* canvas reveal after the splash hands over (main.js hideSplash) */
#game-canvas { opacity: 0; transition: opacity 0.45s ease; }
body.booted #game-canvas { opacity: 1; }
:root.shot-mode #game-canvas { opacity: 1 !important; transition: none !important; }

/* ---- BOOT SCREEN (index.html #boot)
   The key art, full-bleed. See the note in index.html for why this is not the
   native launch image: that asset is a 2732x2732 SQUARE and iOS aspect-fills
   it, so a 9:16 poster loses the outer half of its width on a phone.
   The background colour is set on the container and NOT left to the image, so
   the screen is correct on the first paint -- before the 400 KB jpeg has
   decoded -- and matches the native splash colour exactly (#0a0614), making
   the native-to-web handover invisible rather than a flash. */
#boot {
  position: fixed;
  inset: 0;
  z-index: 900;
  background: var(--ink);
  opacity: 1;
  transition: opacity 0.5s ease;
}

/* COVER, not contain. The art is 9:16 and phones run anywhere from 9:16 to
   9:21, so contain would letterbox on every device it ships to. Cover crops
   the long axis instead, and the composition survives it: the logo sits in
   the upper third and the car in the lower third, both well inside a centred
   9:21 crop. */
#boot-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* Indeterminate sweep, not a progress bar. Nothing here knows a real
   percentage -- shader warmup and the first frames are not measurable ahead of
   time -- and a fake bar that jumps to 100% is worse than an honest pulse. */
#boot-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--safe-bottom) + 32px);
  height: 2px;
  margin: 0 48px;
  overflow: hidden;
  border-radius: 9999px;
  background: rgba(245, 245, 247, 0.14);
}

#boot-bar i {
  display: block;
  width: 40%;
  height: 100%;
  border-radius: 9999px;
  background: linear-gradient(90deg, var(--cyan), var(--magenta));
  animation: boot-sweep 1.15s ease-in-out infinite;
}

@keyframes boot-sweep {
  0%   { transform: translateX(-110%); }
  100% { transform: translateX(360%); }
}

/* Determinate while the shader warm-up runs: main.js writes the width from
   Game.warmProgress and parks the sweep, so the bar reports real work instead
   of implying it. Width is set inline; this only stops the sweep moving it. */
#boot.warming #boot-bar i {
  animation: none;
  transform: none;
  width: 0;
  transition: width 0.25s ease-out;
}

/* The reveal. Fades out and then stops being a layer at all: `visibility`
   rides the same transition so the node cannot sit invisible over the game
   swallowing taps, which is exactly the orphaned-layer state the layout audit
   exists to catch. */
body.booted #boot {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.5s ease, visibility 0s linear 0.5s;
}

/* the capture rig must never photograph the boot screen */
:root.shot-mode #boot { display: none !important; }

@media (prefers-reduced-motion: reduce) {
  #boot-bar i { animation: none; width: 100%; }
}

/* ---- FRAME RATE ROW
   Shares .qbtn with GRAPHICS QUALITY but needs its own container: the quality
   row carries four long words (AUTO/HIGH/BALANCED/BATTERY) and was tuned to
   squeeze them, while these are short numbers that were left looking pinched
   in the same cells. Wider gap, taller target, larger type -- and 44px min
   height so every one clears Apple's touch minimum on its own. */
#fps-row {
  display: flex;
  gap: 8px;
}

#fps-row .qbtn {
  min-height: 44px;
  font-size: clamp(11px, 3.4vw, 14px);
  letter-spacing: 0.06em;
  padding-inline: 4px;
}

/* ---- GARAGE: ACTIVE-LEVEL STEPPER on each upgrade track
   Purchased level is permanent (the pips); this sets how much of it applies.
   Sits under the pips so the two readings never compete: pips = what you own,
   ACTIVE = what is running. */
.u-active {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 5px;
  /* the card already carries name + per-level + bar + pips; the stepper is a
     fourth row in a 99px box, so everything here is deliberately tight */
  min-height: 28px;
  /* NOWRAP, DELIBERATELY. Wrapping was tried as a safety net and made things
     worse: this is the fourth row in the card, so a second line renders past
     the card's bottom edge and gets clipped in half. An overlap traded for a
     clip is not a fix. The row is kept to four short children instead (see
     ui.js _renderPerf), so it fits with room to spare and has nothing to
     wrap. If a future label does not fit here, shorten the label. */
  flex-wrap: nowrap;
}

.u-active .ua-label {
  font-family: var(--display);
  font-size: 10px;
  letter-spacing: 0.1em;
  opacity: 0.8;
  white-space: nowrap;
}

.u-active .ua-val {
  font-family: var(--display);
  font-weight: 800;
  font-size: 12px;
  letter-spacing: 0.06em;
  min-width: 42px;
  text-align: center;
  color: var(--cyan);
  white-space: nowrap;
}

/* ACTIVE label: it sat at 0.55 opacity against a dark card and read as a
   rendering fault rather than a label. Lifted, and allowed to shrink before
   the numbers do -- the value is the thing being read. */
.u-active .ua-label {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.u-active .ua-btn {
  pointer-events: auto;
  cursor: pointer;
  width: 30px;
  height: 30px;
  border-radius: 8px;
  background: rgba(10, 16, 34, 0.6);
  border: 1px solid rgba(0, 229, 255, 0.3);
  color: #cfe8ff;
  font-family: var(--display);
  font-weight: 800;
  font-size: 14px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.u-active .ua-btn:active { background: rgba(0, 229, 255, 0.18); }

/* level 0: the stepper is present but inert, so the mechanism is discoverable
   before the first purchase instead of appearing from nowhere after it */
.u-active.empty .ua-val { color: rgba(185, 200, 232, 0.55); }
.u-active.empty .ua-btn { opacity: 0.35; }

/* REDUCED MOTION (photosensitivity gate) takes the movement out of screen
   changes as well -- a scale+blur push is exactly the kind of full-frame
   motion that setting exists to remove. The cross-fade stays, so screens
   still change legibly. */
body.reduced-motion .layer {
  transition: opacity 0.3s ease !important;
  transform: none !important;
  filter: none !important;
}

/* ---- SHELF SCROLL AFFORDANCE
   iOS hides overlay scrollbars until you are already scrolling, which is the
   wrong moment to discover a list is longer than the box. With 37 cosmetics,
   a player who cannot see that the grid continues simply never finds most of
   the catalogue. ui.js tags the container; these paint the edges. */
#garage-items,
#store-panel { position: relative; }

#garage-items.more-below::after,
#store-panel.more-below::after {
  content: '';
  position: sticky;
  bottom: 0;
  left: 0;
  display: block;
  height: 34px;
  margin-top: -34px;
  pointer-events: none;
  background: linear-gradient(to top,
    rgba(8, 6, 18, 0.92) 0%,
    rgba(8, 6, 18, 0.55) 45%,
    rgba(8, 6, 18, 0) 100%);
}

#garage-items.more-above::before,
#store-panel.more-above::before {
  content: '';
  position: sticky;
  top: 0;
  left: 0;
  display: block;
  height: 22px;
  margin-bottom: -22px;
  pointer-events: none;
  z-index: 2;
  background: linear-gradient(to bottom,
    rgba(8, 6, 18, 0.85) 0%,
    rgba(8, 6, 18, 0) 100%);
}

/* ---- RESET PROGRESS (settings, index.html #reset-btn)
   A destructive row, so it reads as one: crimson icon plate instead of the
   cyan every other row uses, and an ARMED state that is unmistakably a
   different thing from the resting state. Two taps, 4s to change your mind.
   No native confirm() anywhere near it -- a JS dialog blocks the WKWebView's
   entire event loop, which is the one failure mode a delete button must not
   have. */
#reset-btn .set-ico.danger {
  color: #ff8fa3;
  background: rgba(255, 47, 74, 0.10);
  border-color: rgba(255, 47, 74, 0.34);
  box-shadow: inset 0 0 10px rgba(255, 47, 74, 0.08);
}

#reset-btn.armed {
  border-color: rgba(255, 47, 74, 0.7);
  background: rgba(255, 47, 74, 0.12);
}

#reset-btn.armed .set-name { color: #ffd7de; }
#reset-btn.armed .set-hint { color: #ff9fb0; opacity: 0.95; }

#reset-btn.armed .set-ico.danger {
  color: #fff0f3;
  background: rgba(255, 47, 74, 0.28);
  border-color: rgba(255, 47, 74, 0.8);
}

/* ---- BOOT SCREEN CTA (index.html #boot-cta)
   Replaces the progress sweep once the game is ready. It exists to be the
   audio unlock (see the note in index.html), so it must be a real, obvious,
   comfortably-tappable target rather than a subtle hint -- a player who does
   not press it never hears the soundtrack. */
#boot-cta {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--safe-bottom) + 48px);
  min-width: 200px;
  min-height: 48px;                /* comfortably over Apple's 44pt minimum */
  padding: 12px 32px;
  border-radius: 9999px;
  font: 900 16px/1 var(--display);
  letter-spacing: 0.18em;
  text-indent: 0.18em;             /* balance the tracking on the last glyph */
  color: #1a0512;
  background: linear-gradient(180deg, #ff7ab8, var(--magenta));
  border: 1px solid rgba(255, 200, 226, 0.9);
  box-shadow: 0 0 24px rgba(255, 45, 149, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.45);
  cursor: pointer;
  /* it fades IN, so the art is not competing with a button while loading */
  animation: boot-cta-in 0.4s ease both;
}

#boot-cta:active { transform: translateX(-50%) scale(0.96); }

#boot-cta:focus-visible { outline: 3px solid #dff8ff; outline-offset: 3px; }

@keyframes boot-cta-in {
  from { opacity: 0; transform: translateX(-50%) translateY(8px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

/* the sweep and the button are mutually exclusive: the bar says "working",
   the button says "ready", and showing both says neither */
#boot.ready #boot-bar { display: none; }
#boot-cta.hidden { display: none; }

@media (prefers-reduced-motion: reduce) {
  #boot-cta { animation: none; }
}

/* ==================================================================== */
/* TYPE PAIR (AAA critique, UI round): the italic display face had been  */
/* doing every job on screen. It keeps the jobs it is good at (screen    */
/* titles, hero numerals, buttons, HUD instruments); everything that is  */
/* read rather than shouted moves to the neutral --ui-sans at medium     */
/* weight, normal style, tracking capped at 0.04em. ALL-CAPS strings     */
/* stay ALL-CAPS, they just stop wearing the racing italic.              */
/* Placed at the end of the sheet so it wins the cascade over each       */
/* target's original var(--display) declaration at equal specificity.    */

/* labels: settings rows, mission titles, racer names, results stat
   captions, bonus day-chip day labels, the ranks identity eyebrow and the
   commit plates' RUNS caption */
.set-name,
.m-label,
.l-name,
.l-head .l-name,
.stat-label,
.ri-you,
.wc-opt small,
.spip .sp-day {
  font-family: var(--ui-sans);
  font-style: normal;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-indent: 0;
}

/* sub-copy and instructions: store tile blurbs and kind lines, settings
   hints, mission captions, legal/footnote text */
.set-hint,
.m-caption,
.bsub-count,
.ra-sub,
.stn-btn .stn-sub,
.rs-sub,
.ex-kind,
.v-kind,
.ckind,
.perk-sub,
.ft-sub,
.store-foot,
#doc-screen .doc-foot {
  font-family: var(--ui-sans);
  font-style: normal;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-indent: 0;
}

/* Store shelf arrival highlight. Set by ui.js focusStoreShelf when the player
   is sent to the store from a BLOCKED SPEND (out of gems at the revive offer,
   an unaffordable garage item). The scroll alone puts the shelf on screen; the
   ring is what makes the eye land on it rather than near it. Removed after
   1.4s so it never becomes permanent chrome. Reduced-motion users keep the
   ring and lose the pulse, since the ring is the information. */
.shelf-focus {
  animation: shelfFocus 1.4s ease-out 1;
  border-radius: 20px;
}
@keyframes shelfFocus {
  0%   { box-shadow: 0 0 0 0 rgba(0, 229, 255, 0); }
  18%  { box-shadow: 0 0 0 3px rgba(0, 229, 255, 0.55), 0 0 24px rgba(0, 229, 255, 0.28); }
  100% { box-shadow: 0 0 0 0 rgba(0, 229, 255, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .shelf-focus { animation: none; box-shadow: 0 0 0 3px rgba(0, 229, 255, 0.45); }
}

/* Wallet pill acting as the universal store entry (game.js binds it on the
   title and results cards). The affordance has to read without adding chrome
   to a status element: a faint outline lift plus a press state, and a real
   focus ring for keyboard and switch-control users. Hit area is already well
   past 44pt because the pill carries two balances. */
/* pointer-events MUST be re-enabled here. The parent .layer sets
   pointer-events:none so open road still steers and taps through the HUD,
   and every interactive child opts back in. Without this line the wallet
   pill's click handler is bound to an element that can never receive a
   pointer event: the touch audit caught exactly that, reporting the pill as
   "INERT by design - pointer-events:none" after the binding shipped. */
.chips-pill.pill-shop {
  pointer-events: auto;
  cursor: pointer;
  transition: box-shadow 140ms ease, transform 140ms ease;
  position: absolute;   /* already absolute; restated so ::after anchors here */
}

/* 44pt HIT TARGET WITHOUT RESIZING THE PILL. Measured at 121x36, so the
   visual is 8px short of Apple's minimum, and the pill's proportions are
   tuned against the wallet numerals -- growing the box would change a status
   element the whole HUD is balanced around. An invisible ::after extends only
   the TOUCH area.
   -7px vertical, not -4px: an earlier control in this project measured 42x42
   from a -4px inset because the audit's hit-walk loses a pixel per side. 36
   plus 14 is 50, which clears 44 even after that loss, and the extension is
   vertical only so it cannot reach the settings gear on the opposite corner. */
.chips-pill.pill-shop::after {
  content: '';
  position: absolute;
  inset: -7px -2px;
  border-radius: 999px;
}
.chips-pill.pill-shop:hover { box-shadow: 0 0 0 1px rgba(0, 229, 255, 0.35); }
.chips-pill.pill-shop:active { transform: scale(0.97); }
.chips-pill.pill-shop:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px rgba(0, 229, 255, 0.75);
}
@media (prefers-reduced-motion: reduce) {
  .chips-pill.pill-shop { transition: none; }
  .chips-pill.pill-shop:active { transform: none; }
}
