/* Display — big shared screen. "Sticker Bash" look.
   Tokens + reusable bits live in /shared/theme.css; this file is layout +
   display-only composition. Class/ID names are contracted with display/main.js
   and SceneRenderer.js — don't rename without updating those. */

html, body {
  margin: 0; height: 100%;
  background: var(--paper);
  color: var(--ink);
  overflow: hidden;
}

/* ---- Lobby ---- */
/* Sticker chrome on ONE poster grid over the full viewport (paper stage OR
   live 3D preview behind — the layout is identical in both states): a content
   band where the join ticket (wordmark masthead + QR + scan hint) and the cup
   slot CENTRE on the same line, and a footer band (seat dock). Shared
   gutters, one alignment line per band — the hand-slapped tilts stay playful
   because the grid beneath is straight. */
#lobby {
  position: relative;
  z-index: 1;                 /* float the lobby over the #scene backdrop */
  height: 100%;
  display: grid;
  grid-template-columns: clamp(270px, 27vw, 430px) 1fr clamp(190px, 17vw, 270px);
  grid-template-rows: 1fr auto;
  grid-template-areas:
    "ticket tagline cup"
    "dock   dock    dock";
  column-gap: clamp(16px, 2vw, 36px);
  /* generous side gutters: the rails frame the centre without hugging the
     bezel (the middle stays open for the tagline / live track preview) */
  padding:
    clamp(14px, 2.4vh, 26px) clamp(32px, 4.5vw, 88px)
    clamp(16px, 3vh, 32px);
}

/* Left rail — the join ticket with the scan hint hanging under it. The rail
   is the grid item; the hint is absolute so its pre/post-pick toggle never
   moves the ticket. */
.ticket-rail {
  grid-area: ticket; align-self: center;
  position: relative;
  width: 100%;
  max-width: 58vh;                    /* never taller than the band on squat windows */
}
/* Join ticket — the wordmark masthead over QR + URL, one sticker card.
   A <button>: the whole ticket copies the join link. */
.ticket {
  width: 100%;                        /* the QR is the lobby's MAIN element */
  display: flex; flex-direction: column; align-items: center; gap: 0.55rem;
  padding: 0.85rem 0.85rem 1rem;
  font: inherit; color: var(--ink); text-align: center;
  background: var(--surface);
  border: 3px solid var(--ink); border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  transform: rotate(-1.2deg);
  cursor: pointer;
  transition: box-shadow 0.08s ease, transform 0.08s ease;
}
.ticket:hover { box-shadow: 8px 8px 0 var(--shadow-ink); }
.ticket:active { transform: rotate(-1.2deg) translate(3px, 3px); box-shadow: 4px 4px 0 var(--shadow-ink); }
.ticket:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
/* the in-card wordmark: the kit badge's white die-cut edge would vanish on the
   white card, so here it's the plain ink/red lockup — same face, same tilt */
.ticket__wordmark {
  font-size: clamp(25px, 2.5vw, 38px);   /* .l2 scales along at 1.24em */
  -webkit-text-stroke-width: 0;
  filter: none;
  margin: 0.1rem 0 0.15rem;
}
/* Reserve a square box up front (aspect-ratio, not the canvas' intrinsic size)
   so the ticket holds its final footprint before the async QR fetch resolves
   (a blank <canvas> is 300×150 — without this the card would pop taller). */
#qr { display: block; width: 100%; aspect-ratio: 1 / 1; border-radius: 6px; image-rendering: pixelated; }
/* The URL must stay readable in full — it's the type-it-in fallback for anyone
   who can't scan. So it WRAPS rather than truncating: an ellipsis eats the tail,
   and the tail is the room code (preview hosts like
   tinytrack-<branch>.couchpad.games are long enough to lose it entirely).
   `anywhere` because a host has no spaces to break at. */
.ticket__url {
  max-width: 100%;
  font-family: var(--font-display); font-weight: 600;
  /* smaller than the code below it: the host is the boring half of the URL, and
     a preview host (up to ~76 chars) needs the room to wrap in few lines */
  font-size: clamp(13px, 1.15vw, 17px);
  line-height: 1.25;
  color: var(--ink);
  overflow-wrap: anywhere;
}
/* the room code — its own line under the host, in the accent colour, and the
   biggest text on the ticket: it's the only part that changes per party and the
   only part worth reading from the sofa. Always broken out (not just when the
   host is long) so it lands in the same place on every screen. */
.ticket__cd {
  display: block;
  font-size: 1.45em;
  color: var(--accent); font-weight: 700; letter-spacing: 0.04em;
}

/* Cup slot (right rail) — empty pre-pick, the race card post-pick
   (renderCupSlot fills it). Centre-aligned with the join ticket. */
.cup-slot {
  grid-area: cup; align-self: center; justify-self: end;   /* centre-aligned with the ticket */
  width: 100%;
  max-width: 44vh;                    /* same squat-window guard as the ticket */
  display: flex; justify-content: center;
}
/* the pick sits on its own white sticker card (mirroring the join ticket's
   surface, leaning the other way) so it doesn't dangle in the air */
.cup-slot__pick {
  width: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 0.65rem;
  padding: 0.9rem 0.8rem 1rem;
  background: var(--surface);
  border: 3px solid var(--ink); border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  transform: rotate(1.2deg);
}
.cup-sticker {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.2rem, 1.8vw, 1.7rem); line-height: 1.05;
  color: #fff; text-align: center;
  background: var(--red); border: 3px solid var(--ink); border-radius: var(--r);
  padding: 0.45em 0.75em;
  transform: rotate(-2deg);
  box-shadow: var(--shadow-card);
}
/* the picked circuits as mini schematics — a 2×2 grid for a cup's four races
   (numbered), one full-width map for an exact pick / the random draw */
.cup-maps { width: 100%; display: grid; grid-template-columns: repeat(2, 1fr); gap: 7px; }
.cup-maps--one { grid-template-columns: 1fr; }
.cup-maps--one .track-map { max-width: 72%; margin: 0 auto; }
.cup-maps__tile { position: relative; }
.cup-maps__tile .track-map { border: 2.5px solid var(--ink); }
.cup-maps__n {
  position: absolute; top: -6px; left: -6px;
  min-width: 1.5em; text-align: center;
  font-family: var(--font-display); font-weight: 600; font-size: 0.72rem; line-height: 1.5em;
  color: #fff; background: var(--ink); border-radius: var(--r-pill);
  padding: 0 0.3em;
}
.cup-races {
  font-family: var(--font-display); font-weight: 600; font-size: 0.78rem;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink);
  background: var(--surface); border: 2.5px solid var(--ink); border-radius: var(--r-pill);
  padding: 0.3em 0.95em;
  box-shadow: var(--shadow-pop);
}
/* difficulty tendency — 4 pips, the first N filled RED (celebration colour;
   never the old green→amber ramp) */
.cup-meter { display: inline-flex; gap: 6px; }
.cup-meter i {
  width: 11px; height: 11px; border-radius: 50%;
  border: 2px solid var(--ink); background: var(--surface);
}
.cup-meter i.is-on { background: var(--red); }

/* Scan hint — a quiet white sticker hanging centred under the ticket for the
   whole lobby; absolute, so it never affects the card's position. */
.tagline {
  position: absolute; top: calc(100% + 14px); left: 50%;
  transform: translateX(-50%) rotate(-2deg);
  width: max-content; max-width: 120%;
  text-align: center;
  color: var(--ink); background: var(--surface);
  border: 3px solid var(--ink); border-radius: var(--r);
  padding: 0.55em 1.1em;
  box-shadow: var(--shadow-pop);
}
.tagline__main {
  font-family: var(--font-display); font-weight: 600;
  font-size: clamp(16px, 1.7vw, 25px); line-height: 1.1;
}

/* Seat dock (bottom-centre) — the joined headline over a horizontal row of
   sticker seat chips. Room is capped at 4 players, so the dock never wraps. */
.lobby-foot {
  grid-area: dock;
  display: flex; flex-direction: column; align-items: center; gap: 0.8rem;
}
#players { display: flex; justify-content: center; gap: clamp(0.6rem, 1.1vw, 1.1rem); }
/* Seat card — a sticker: the kart render on top, the player's name (in their
   livery colour) beneath. Ready check / host star pinned to the top-right
   corner. Neighbours lean opposite ways, like stickers slapped down one after
   another. Every card is the same height whatever it holds — see .seat__name. */
.seat {
  position: relative;
  width: clamp(100px, 9.5vw, 134px);
  display: flex; flex-direction: column; align-items: center; gap: 0.1rem;
  padding: 0.45rem 0.5rem 0.55rem;
  background: var(--surface);
  border: 3px solid var(--ink);
  border-radius: var(--r);
  box-shadow: var(--shadow-pop);
}
.seat:nth-child(odd)  { transform: rotate(-1.4deg); }
.seat:nth-child(even) { transform: rotate(1.4deg); }
/* The name box ALWAYS reserves both lines (--name-lines), so the card's height is
   the same from the empty "Open" state through to a two-line name — the dock
   can't lift when someone called "Alexandra Smith" joins. Shorter names (and
   "Open") centre in the reserved box rather than hugging the render above. */
.seat__name {
  --name-lh: 1.15;
  --name-lines: 2;
  max-width: 100%;
  min-height: calc(var(--name-lh) * var(--name-lines) * 1em);
  display: flex; justify-content: center; align-items: center;
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1rem, 1.3vw, 1.3rem); line-height: var(--name-lh); color: var(--c, var(--ink));
}
/* Names break at the SPACE — "Alexandra Smith" stacks instead of clipping to
   "Alexandra …". `anywhere` only kicks in for a name with no space to break at
   (16 chars max, per the controller's input): it breaks mid-word rather than
   hard-clipping, since the clamp ellipsises across lines, not within one. */
.seat__label {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: var(--name-lines);
  overflow: hidden;
  overflow-wrap: anywhere;
  text-align: center;
}
.seat--off { opacity: 0.5; }
/* readiness check — a green circle checkmark on the card's top-right corner;
   present (invisible) on every taken seat and revealed once the player's
   "I'm ready" lands, so the card layout never shifts. */
.seat__ready {
  visibility: hidden;
  position: absolute; top: -0.6rem; right: -0.55rem;
  width: 1.6rem; aspect-ratio: 1;
  display: grid; place-items: center;
  color: #fff; background: var(--green);
  border: 2.5px solid var(--ink); border-radius: 50%;
}
.seat__ready svg { width: 70%; height: 70%; }
.seat--ready .seat__ready { visibility: visible; }
/* host marker — a gold star with an ink stroke, same corner as the ready check
   (mutually exclusive: the host never readies). */
.seat__host {
  position: absolute; top: -0.9rem; right: -0.7rem;
  width: 1.9rem; aspect-ratio: 1;
  display: grid; place-items: center;
}
.seat__host svg {
  width: 100%; height: 100%;
  fill: #ffc83d; stroke: var(--ink); stroke-width: 1.7;
  stroke-linejoin: round; paint-order: stroke fill;
}
/* open seat — a dashed placeholder card of the SAME footprint: the empty
   .seat__open square stands in for the kart render, "Open" sits where the
   name goes */
.seat--open {
  background: color-mix(in srgb, #fff 50%, transparent);
  border: 3px dashed rgba(42, 39, 53, 0.45);
  box-shadow: none;
}
.seat--open .seat__name { color: var(--ink-3); }
/* matches .carthumb's box (theme.css) so an open seat is the same size as a taken one */
.seat--open .seat__open { width: 100%; aspect-ratio: 5 / 4; }

/* Build/version badge — preview builds only (the server leaves it empty in
   production, and :empty hides it). Pinned to the welcome board's bottom-right
   corner, deliberately quiet. Not uppercased so the commit hash stays
   lowercase hex. */
.version-badge {
  position: absolute; right: clamp(0.7rem, 1.4vw, 1.4rem); bottom: clamp(0.7rem, 1.4vh, 1.3rem);
  z-index: 2;
  font-weight: 700; font-size: clamp(0.7rem, 0.95vw, 0.88rem);
  letter-spacing: 0.02em; font-variant-numeric: tabular-nums;
  color: var(--ink-3);
  background: color-mix(in srgb, var(--surface) 78%, transparent);
  border: 2px solid var(--hairline); border-radius: var(--r-pill);
  padding: 0.25rem 0.7rem;
  pointer-events: none; user-select: none;
}
.version-badge:empty { display: none; }

/* ---- Welcome ---- */
/* Title board at boot: the wordmark at poster scale + NEW GAME, centred over
   the paper diorama. Same stacking slot as #lobby (they swap via .hidden). */
#welcome {
  position: relative; z-index: 1;      /* over the diorama / #scene backdrop */
  height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(1.1rem, 3.6vh, 2.2rem);
  padding-bottom: 7vh;                 /* optical centre: sit above the grass band */
}
.welcome__wordmark {
  font-size: clamp(56px, 9vw, 130px);
  text-align: center;
}
.welcome__tag {
  margin: 0;
  font-weight: 800; color: var(--ink-2);
  font-size: clamp(16px, 1.7vw, 24px);
  transform: rotate(-1deg);
}
.welcome__btn {
  font-size: clamp(22px, 2.6vw, 34px);
  padding: 0.72em 1.5em;
  letter-spacing: 0.03em;
  transform: rotate(1.4deg);
}
/* keep the sticker tilt through the press (the kit's :active would drop it) */
.welcome__btn:active, .welcome__btn.is-pressed { transform: rotate(1.4deg) translateY(var(--btn-sink)); }

/* ---- 3D scene: a persistent backdrop behind BOTH the lobby (orbiting preview)
   and the race (split-screen). The lobby/race sections layer on top. ---- */
/* Diorama backdrop (shown until a track is picked) — pin it to the viewport so it
   shares the #scene stacking context regardless of body height. */
#lobby-diorama { position: fixed; }
/* The 3D backdrop sits OVER the diorama (same z-0, later in the DOM). Visibility is by
   opacity, not display, so we can crossfade the track straight in over the diorama (the
   default background) — no intermediate veil. .hidden is a hard off for non-lobby paths;
   .is-dim drops it to transparent so the diorama beneath shows through (the fade base).
   FADE_MS in main.js mirrors this transition. */
#scene { position: fixed; inset: 0; z-index: 0; opacity: 1; transition: opacity 0.45s ease; }
#scene.is-dim { opacity: 0; }
#scene canvas { display: block; }
/* Track→track crossfade still: a frozen frame of the OUTGOING track, laid over the live
   canvas while the new track rebuilds underneath. Fading it out dissolves one circuit
   straight into the next — never dipping through the diorama. Inside #scene's z-0 stacking
   context (so it stays behind the lobby/race UI); FADE_MS in main.js mirrors this. */
#scene-snap { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1;
  opacity: 1; transition: opacity 0.45s ease; pointer-events: none;
  /* Promote to its own compositor layer so the fade keeps animating off the main thread —
     it has to glide THROUGH setTrack's tens-of-ms rebuild block (which freezes the orbit),
     masking that hitch. main.js commits the fade a frame BEFORE the rebuild for this reason. */
  will-change: opacity; }
#scene-snap.is-fading { opacity: 0; }
/* Race section is now a transparent overlay container over #scene. */
#race { position: fixed; inset: 0; z-index: 2; }
/* Standalone track preview (track gallery "open ↗", not the grid iframe): the
   viewer drives the overview camera. #race sits above the canvas (z-2), so it
   must pass pointer events through for OrbitControls to receive drags. Toggled
   by the track TestHarness (html.cam-free); harmless here since track preview
   shows no race UI of its own. */
html.cam-free #race { pointer-events: none; }
/* Control legend for the free camera (created by the track TestHarness). Pinned
   bottom-centre, never catches pointer events, fades out after its timer. */
.cam-hint {
  position: fixed; left: 50%; bottom: 1.1rem; transform: translateX(-50%);
  z-index: 13; padding: 0.45rem 0.95rem; border-radius: 999px;
  background: rgba(20, 24, 30, 0.55); color: #fff;
  font: 600 0.85rem/1 'Nunito', system-ui, sans-serif; letter-spacing: 0.01em;
  white-space: nowrap; pointer-events: none; backdrop-filter: blur(4px);
  transition: opacity 0.7s ease;
}
.cam-hint.is-faded { opacity: 0; }
/* Diorama vignette: gently darken the frame edges so the eye locks to the centre,
   reinforcing the miniature/tilt-shift focus. Cosmetic only — never catches clicks. */
#scene::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(120% 120% at 50% 45%, transparent 55%, rgba(0, 0, 0, 0.28) 100%);
}

#countdown {
  position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display); font-weight: 700;
  font-size: 26vh; color: var(--ink);
  /* die-cut sticker numerals — ink with the white cut edge, same technique as
     the wordmark (degrades to plain ink text without paint-order) */
  -webkit-text-stroke: 12px #fff;
  paint-order: stroke fill;
  filter: drop-shadow(6px 6px 0 var(--shadow-ink));
  pointer-events: none;
}
/* "GO!" stays up the beat after the start (cars are already moving) and fades
   out over that ~1s window. Added/removed by onCountdownTick on the n=0 beat. */
#countdown.is-go { animation: countdown-go 1s ease-out forwards; }
@keyframes countdown-go {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(1.35); }
}

/* per-viewport (split-screen) label, anchored at each cell's top-left: the
   player's name as a white sticker tag, the item slot beneath it. */
.cell-label {
  position: absolute; display: none; margin: 0.7rem; pointer-events: none;
}
/* name on top, the item slot stacked + left-aligned below it (place + lap
   moved out to the top-right .cell-rank readout) */
.cell-label__row { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; }
.cell-label__name {
  font-family: var(--font-display); font-weight: 700; font-size: clamp(1.5rem, 2vw, 2.2rem);
  line-height: 1;
  color: var(--c, var(--ink));
  background: var(--surface);
  border: 3px solid var(--ink); border-radius: var(--r-sm);
  padding: 0.3em 0.55em;
  transform: rotate(-2deg);
  box-shadow: var(--shadow-pop);
  white-space: nowrap; max-width: 14em; overflow: hidden; text-overflow: ellipsis;
}

/* Place + lap — top-right of the cell: a white sticker badge for the ordinal
   over an ink "LAP 2/3" pill, the pair leaning +1.5°. Positioned by _loop; the
   transform anchors its RIGHT edge to the left coord _loop sets (cell right). */
.cell-rank {
  position: absolute; display: none; transform: translateX(-100%) rotate(1.5deg);
  transform-origin: top right;
  text-align: right; pointer-events: none;
}
/* _loop shows the wrapper with display:block, so the badge + pill right-align
   themselves (display:table shrink-wraps while honouring margin-left:auto). */
.cell-rank__place {
  display: table; margin-left: auto;
  font-family: var(--font-display); font-weight: 700; line-height: 1;
  font-size: clamp(2rem, 2.8vw, 2.9rem);
  color: var(--ink); background: var(--surface);
  border: 3px solid var(--ink); border-radius: var(--r-sm);
  padding: 0.22em 0.45em;
  box-shadow: var(--shadow-pop);
}
.cell-rank__lap {
  display: table; margin-left: auto; margin-top: 0.45rem;
  font-family: var(--font-display); font-weight: 600;
  font-size: clamp(0.9rem, 1.25vw, 1.15rem); letter-spacing: 0.12em; text-transform: uppercase;
  color: #fff; background: var(--ink); border-radius: var(--r-pill);
  padding: 0.32em 0.9em;
  white-space: nowrap;
}

/* Held-item indicator — a FIXED reserved sticker slot (no reflow when empty)
   under the player name so players read what they got at a glance (the
   controller has no item UI; place/lap live in .cell-rank). Dashed ink outline
   while empty; a solid white sticker with the hard drop once an item lands. */
.cell-label__item {
  flex: none; width: clamp(112px, 10vw, 152px); height: clamp(112px, 10vw, 152px); transform-origin: center;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface);
  border-radius: 26px; border: 4px solid var(--ink);
  box-shadow: var(--shadow-pop);
}
.cell-label__item.is-empty {
  background: transparent;
  border: 4px dashed color-mix(in srgb, var(--ink) 45%, transparent);
  box-shadow: none;
}
.cell-label__item svg { width: 70%; height: 70%; display: block; }
/* baked asset icons (e.g. the banana GLB render) — already square + padded */
.cell-label__item img { width: 78%; height: 78%; object-fit: contain; display: block; }
/* roulette spin: a fast scale/tilt flicker while the slot rolls */
.cell-label__item.rolling { animation: cellItemRoll 0.1s linear infinite; }
@keyframes cellItemRoll {
  0%   { filter: brightness(1.2); transform: scale(1.12) rotate(-4deg); }
  50%  { filter: brightness(0.9); transform: scale(0.97) rotate(4deg); }
  100% { filter: brightness(1.2); transform: scale(1.12) rotate(-4deg); }
}
/* landing pop: a big springy scale-in */
.cell-label__item.pop { animation: cellItemPop 0.5s cubic-bezier(0.2, 1.5, 0.35, 1); }
@keyframes cellItemPop {
  0%   { transform: scale(1.8); filter: brightness(1.6); }
  60%  { filter: brightness(1.15); }
  100% { transform: scale(1); filter: none; }
}

/* The per-cell steer indicator is NOT here any more, and neither are the ink
   rules between cells: both are cell-anchored and textless, so the RENDERER
   draws them (native/renderer/materials/voverlay.mat), from the same cell
   rectangles this file's chrome is placed on. The composition moved with them —
   270 x 34 with a 4px border, pill radius, ink on --surface with a 40% centre
   tick and a --c fill, 20 clear of the bottom — but the UNITS did not: every
   number scales with a DAMPED SHARE OF SCREEN HEIGHT now — the geometric mean
   of the cell's height and the screen's, over 1080, times the renderer's
   BAR_SCALE (1.7). A split takes height away so each bar shrinks, but by the
   square root of the share, not proportionally: 459 x 58 at one player, 325 x 41
   at every split. No CSS pixel is left for the renderer to convert,
   deliberately: a TV shell cannot supply a points-per-pixel scale, knowing
   neither the panel's size nor the couch's distance.

   NOTE THE ASYMMETRY, since it is a live design question: .cell-label and
   .cell-rank below are still sized in CSS pixels and do NOT scale with the
   split at all, so a 2x2 cell mixes one element that shrinks with the split
   against three that hold their size.

   The line, so this does not become a habit: cell-anchored AND textless goes to
   the renderer; anything carrying type or sticker chrome stays right here. */

/* per-cell FINISHED overlay — a sticker card centred in a player's split-screen
   cell the moment they cross the line, while the rest of the field is still
   racing. _loop sets left/top to the cell centre; translate centres it. Lives
   in #scene (z-0) like the other cell overlays, so the full results screen
   (#results, in #race z-2) covers it once the race ends. */
.cell-finish {
  position: absolute; display: none; transform: translate(-50%, -50%) rotate(-1.5deg);
  flex-direction: column; align-items: center; gap: 0.35rem; text-align: center;
  padding: clamp(0.7rem, 1.5vw, 1.3rem) clamp(1.1rem, 2.4vw, 2.2rem);
  background: var(--surface);
  border: 3px solid var(--ink); border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  pointer-events: none;
}
.cell-finish__badge {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(0.85rem, 1.4vw, 1.25rem); letter-spacing: 0.16em; text-transform: uppercase;
  color: #fff; background: var(--c, var(--brand));
  padding: 0.2rem 0.9rem; border-radius: var(--r-pill);
  border: 2.5px solid var(--ink);
}
.cell-finish__place {
  font-family: var(--font-display); font-weight: 700; line-height: 1;
  font-size: clamp(2rem, 4vw, 3.4rem); color: var(--ink);
}
.cell-finish__time {
  font-weight: 800; font-size: clamp(0.95rem, 1.4vw, 1.4rem);
  color: var(--ink-2); font-variant-numeric: tabular-nums;
}

/* results overlay — a full-screen paper board over the frozen race */
#results {
  position: fixed; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  background: rgba(255, 246, 235, 0.92); backdrop-filter: blur(10px);
}
#results h2 {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(2.4rem, 6vw, 4.6rem); margin: 0 0 1rem; color: var(--ink);
}
/* podium boards: the title goes full celebration — a red header sticker
   (capped a step below the plain title so it doesn't dwarf the podium) */
#results.is-podium h2 {
  font-size: clamp(2rem, 4.2vw, 3.4rem);
  margin-bottom: 0.5em;
  color: #fff; background: var(--red);
  border: 3px solid var(--ink); border-radius: var(--r-lg);
  padding: 0.25em 0.7em;
  transform: rotate(-2deg);
  box-shadow: var(--shadow-card);
}
#results-list {
  list-style: none; padding: 0; margin: 0; counter-reset: li;
  font-size: 1.5rem; display: flex; flex-direction: column; gap: 0.6rem; min-width: 24rem;
}
/* podium boards: the top three stand on the steps, so the list ranks from 4th */
#results.is-podium #results-list { counter-reset: li 3; }
#results-list li {
  counter-increment: li;
  display: flex; align-items: center; gap: 0.7rem;
  background: var(--surface); border: 3px solid var(--ink);
  border-radius: var(--r-sm); padding: 0.7rem 1.1rem; font-weight: 800;
  box-shadow: var(--shadow-pop);
}
#results-list li::before {
  content: counter(li); font-family: var(--font-display); font-weight: 700;
  color: var(--ink-2); min-width: 1.2em;
}
/* racer names carry their livery colour (no swatch dot) */
#results-list .res-name, .podium__who .res-name { color: var(--c, var(--ink)); }
.res-time { margin-left: auto; color: var(--ink-2); font-weight: 800; font-variant-numeric: tabular-nums; }
/* Late joiners ride along under the field: no rank number, "Next race" status. */
#results-list li.is-joining::before { content: '–'; }
#results-list li.is-joining .res-time { font-style: italic; }
#results-newgame { margin-top: 1.8rem; }

/* ---- cup dressing (intermission + podium) ---- */
.results-sub {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.1rem, 2vw, 1.6rem); color: var(--ink-2);
  margin: -0.4rem 0 1.3rem;
}
/* cup boards trade the race time for points: this race's gain + the cup total */
.res-gain { margin-left: auto; color: var(--brand); font-weight: 800; }
.res-gain.is-zero { color: var(--ink-3); }
.res-pts { color: var(--ink-2); font-weight: 800; font-variant-numeric: tabular-nums; min-width: 3.6em; text-align: right; }
/* "Next up: Gulch — starting in 8…" under the intermission board */
.results-next { margin-top: 1.4rem; font-weight: 800; font-size: clamp(1.1rem, 2vw, 1.5rem); color: var(--ink-2); }
.results-next b { font-family: var(--font-display); font-weight: 700; color: var(--ink); }

/* podium — 2nd | 1st | 3rd rising steps: livery-coloured sticker blocks with
   big rank numerals; the champion pops in like the GO! banner */
.results-podium { display: flex; align-items: flex-end; gap: 1.1rem; margin-bottom: 1.6rem; }
.podium__col { display: flex; flex-direction: column; align-items: center; gap: 0.55rem; }
.podium__col[data-place="1"] { animation: podium-pop 0.45s cubic-bezier(0.2, 1.6, 0.4, 1) both; }
.podium__who { display: flex; align-items: center; gap: 0.5rem; font-weight: 800; font-size: 1.35rem; color: var(--ink); }
.podium__col[data-place="1"] .podium__who { font-size: 1.6rem; }
.podium__pts { font-weight: 800; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.podium__step {
  width: 8rem; display: flex; align-items: center; justify-content: center;
  color: #fff; background: var(--c, var(--surface));
  border: 3px solid var(--ink);
  border-radius: var(--r-sm) var(--r-sm) 0 0; box-shadow: var(--shadow-pop);
  font-family: var(--font-display); font-weight: 700; font-size: 2.2rem;
}
.podium__col[data-place="1"] .podium__step { height: 6rem; }
.podium__col[data-place="2"] .podium__step { height: 4rem; }
.podium__col[data-place="3"] .podium__step { height: 2.9rem; font-size: 1.9rem; }
@keyframes podium-pop {
  from { transform: scale(0.4); opacity: 0; }
  to   { transform: scale(1);   opacity: 1; }
}

/* ---- Corner buttons ---- */
/* The top-right row over the scene: fullscreen (always) + pause (races only,
   .hidden off-race). .icon-btn (theme.css) gives them the toy-button look; we
   only place them here. The row shares the top-right corner with each cell's
   place/lap readout, so it auto-fades once the race chrome goes idle (see
   html.chrome-idle below). The fade is the ROW's transition, which leaves each
   button's own .icon-btn hover/press transitions untouched. */
#corner-btns {
  position: fixed; z-index: 12;
  top: calc(1.1rem + env(safe-area-inset-top));
  right: calc(1.1rem + env(safe-area-inset-right));
  display: flex; gap: 0.6rem;
  transition: opacity 0.4s ease;
}
#corner-btns .icon-btn { width: 3.4rem; height: 3.4rem; }

/* Idle race chrome — main.js sets html.chrome-idle after a few seconds without
   a pointer move / tap / key press, and ONLY while a race is running (never on
   the welcome board, the lobby, the results, or behind the pause overlay, where
   a vanished cursor would strand a mouse user mid-click). The corner row fades
   off the place/lap readout, and the mouse pointer goes with it: this is a TV
   surface, and an arrow parked over the track is nothing but litter. Any input
   brings both straight back.
   `html.chrome-idle *` (0,1,1) is deliberately more specific than the .btn /
   .icon-btn / .music-credit `cursor: pointer` rules (0,1,0), so hovering one of
   those can't resurrect the arrow regardless of stylesheet order. */
html.chrome-idle, html.chrome-idle * { cursor: none; }
html.chrome-idle #corner-btns { opacity: 0; pointer-events: none; }

/* Pause overlay — same bright frosted glass as the results screen, sits above
   the (frozen) race with a floating card of choices. */
#pause-overlay {
  position: fixed; inset: 0; z-index: 20;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255, 246, 235, 0.72); backdrop-filter: blur(10px);
}
.pause-card {
  display: flex; flex-direction: column; align-items: center;
  gap: clamp(1.2rem, 2.5vw, 2rem); text-align: center;
  padding: clamp(1.8rem, 4vw, 3rem) clamp(2.2rem, 5vw, 4rem);
}
.pause-card h2 {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(2rem, 5vw, 3.4rem); margin: 0; color: var(--ink);
}
.pause-card__btns { display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center; }
.pause-card__btns .btn { min-width: 9rem; }

/* ---- Device chooser ---- the display URL opened on a phone-sized screen
   (see #device-choice in index.html). Hidden by default; the media query below
   surfaces it on cramped viewports until a choice is made. Size-only triggers
   (no pointer/hover filters): the "run the big screen here OR join on your
   phone" fork applies to any small window, and continuing is one tap away.
   main.js adds html.device-choice-dismissed on "continue" AND at boot on big
   screens / dev surfaces, so a mid-session window resize can never resurface
   the chooser over a live lobby or race. */
#device-choice {
  display: none; position: fixed; inset: 0; z-index: 30;
  align-items: center; justify-content: center;
  padding: 1.2rem;
  background: rgba(255, 246, 235, 0.85); backdrop-filter: blur(10px);
}
.device-card {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 1rem; padding: 2rem 1.6rem; max-width: 26rem;
}
.device-card h1 {
  font-family: var(--font-display); font-weight: 700;
  font-size: 2.2rem; line-height: 0.98; margin: 0; color: var(--ink);
}
.device-card h1 span { color: var(--brand); }
.device-card p { margin: 0; color: var(--ink-2); font-weight: 700; }
@media (max-width: 700px), (max-height: 500px) {
  html:not(.device-choice-dismissed) #device-choice { display: flex; }
}

/* ---- Motion: sticker slap-ins (keyframes in theme.css) ----
   Lobby chrome slaps on in a quick stagger every time #lobby is (re)shown
   (display:none → shown restarts CSS animations), the cup sticker pops as the
   pick lands (.cup-slot__pick loses .hidden), and the results board's pieces
   land one after another on reveal. Countdown digits are re-triggered from JS
   (the .slap class) since their element never re-enters the DOM. All of it
   only exists under prefers-reduced-motion: no-preference. */
@media (prefers-reduced-motion: no-preference) {
  .welcome__wordmark, .welcome__tag, .welcome__btn,
  .ticket, .cup-slot__pick, .tagline, .lobby-foot {
    animation: sticker-slap 240ms cubic-bezier(0.2, 1.5, 0.35, 1) both;
  }
  .welcome__tag { animation-delay: 60ms; }
  .welcome__btn { animation-delay: 120ms; }
  .ticket { animation-delay: 40ms; }
  .tagline { animation-delay: 80ms; }
  .cup-slot__pick { animation-delay: 120ms; }
  .lobby-foot { animation-delay: 160ms; }

  #results h2, .results-sub, .results-podium, #results-list li, .results-next, #results-newgame {
    animation: sticker-slap 240ms cubic-bezier(0.2, 1.5, 0.35, 1) both;
  }
  .results-sub, .results-podium { animation-delay: 60ms; }
  #results-list li:nth-child(1) { animation-delay: 100ms; }
  #results-list li:nth-child(2) { animation-delay: 140ms; }
  #results-list li:nth-child(3) { animation-delay: 180ms; }
  #results-list li:nth-child(4) { animation-delay: 220ms; }
  #results-list li:nth-child(n+5) { animation-delay: 260ms; }
  .results-next, #results-newgame { animation-delay: 300ms; }
}

/* ---- Narrow / portrait (e.g. someone opens the display on a phone) ----
   The rail layout is designed for a wide screen; on a narrow viewport stack
   the chrome in one scrolling column instead. (The device chooser covers most
   of these visits anyway.) */
@media (max-width: 860px) {
  html, body { overflow-y: auto; }
  #lobby {
    height: auto; min-height: 100%;
    display: flex; flex-direction: column; align-items: center;
    gap: 1.4rem; padding: 1.4rem 1rem 2rem;
  }
  .ticket-rail { order: 1; width: min(84vw, 400px); max-width: none; }
  /* in the column the hint flows under the card instead of hanging over
     whatever comes next */
  .tagline { position: static; transform: rotate(-2deg); margin: 0.9rem auto 0; width: fit-content; }
  .cup-slot { order: 3; width: min(84vw, 360px); max-width: none; }
  .lobby-foot { order: 4; }
  #players { flex-wrap: wrap; justify-content: center; }
  /* the stage clouds are placed for the wide layout — behind the stacked
     column they poke out from odd corners, so they sit this one out */
  .scene__cloud { display: none; }
}

/* ---- Toast ---- transient confirmation (e.g. "Copied"), bottom-centre.
   Hidden until .is-on is added; fades + slides in, then back out. */
.toast {
  position: fixed; left: 50%; bottom: 2.5rem; transform: translate(-50%, 1rem);
  z-index: 10;
  font-family: var(--font-display); font-weight: 600; font-size: 1rem; color: #fff;
  background: var(--brand);
  border: 3px solid var(--ink);
  padding: 0.6rem 1.3rem; border-radius: var(--r-pill);
  box-shadow: var(--shadow-pop);
  opacity: 0; pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }

/* ---- Sound hint ---- persistent pill while the browser still has audio
   locked (no user gesture yet); see #sound-hint in index.html. */
.sound-hint {
  position: fixed; left: 1rem; bottom: 1rem; z-index: 10;
  font: inherit; font-weight: 800; font-size: 0.95rem; color: #fff;
  background: rgba(42, 39, 53, 0.7);
  border: 0; padding: 0.6rem 1.2rem; border-radius: var(--r-pill);
  box-shadow: var(--shadow-float);
  cursor: pointer;
}

/* ---- Now-playing music credit ---- bottom-left link: the current track +
   artist, linking to its source (also the on-screen CC-BY attribution). A dark
   translucent pill so it reads over the bright 3D scene without grabbing focus.
   Shares the sound-hint's corner, but they never show together (the hint is up
   only while audio is locked; this only during a race). */
.music-credit {
  position: fixed; left: 1rem; bottom: 1rem; z-index: 9;
  max-width: min(46vw, 24rem);
  display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font: inherit; font-weight: 700; font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.92); text-decoration: none;
  background: rgba(42, 39, 53, 0.55);
  padding: 0.4rem 0.9rem; border-radius: var(--r-pill);
  box-shadow: var(--shadow-float);
  opacity: 0.78; transition: opacity 0.2s ease, background 0.2s ease;
}
.music-credit::before { content: "\266A\00a0\00a0"; opacity: 0.9; } /* ♪ */
.music-credit:hover { opacity: 1; background: rgba(42, 39, 53, 0.72); }

/* ---- Reconnect card ---- a QR for a dropped player, centred in their own
   split-screen cell exactly like the FINISHED card. It REUSES .cell-finish for
   placement + chrome (absolute, translate(-50%,-50%), frosted card, livery
   top-border); _loop sets left/top and toggles display. .cell-reconnect only adds
   the spacing the QR content wants. */
.cell-reconnect { gap: 0.5rem; }
.rc-card__head {
  display: flex; align-items: center; max-width: 11rem;
  font-weight: 800; font-size: clamp(0.9rem, 1.1vw, 1.1rem); color: var(--ink);
}
.rc-card__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rc-card__sub {
  font-weight: 700; font-size: 0.72rem; letter-spacing: 0.02em;
  text-transform: uppercase; color: var(--ink-3);
}
.rc-card__qr {
  display: block; width: clamp(96px, 9vw, 132px); aspect-ratio: 1 / 1;
  border-radius: var(--r-sm); image-rendering: pixelated;
}
