/* ==========================================================================
   Kent's Bar Hunt — visual shell
   Hand-written CSS. No framework, no preprocessor, no CDN, no light theme.
   Look: a dive-bar neon sign at 11pm. Near-black ground, one electric cyan
   accent, blood red for punishment, gold for money. Everything is loud.
   Owner: T3. Later tasks add screen-specific rules BELOW this file's
   "SCREEN-SPECIFIC" marker and must reuse the custom properties declared here.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  color-scheme: dark;

  /* Ground + surfaces */
  --ground:       #08080b;   /* page ground, near black */
  --ground-lift:  #101017;   /* raised panel */
  --surface:      #17171f;   /* card */
  --surface-hi:   #21212c;   /* pressed / hovered card */
  --line:         #2e2e3d;   /* hairline border */
  --line-hot:     #45455c;   /* emphasised border */

  /* Ink */
  --ink:          #f6f6fa;   /* primary text */
  --ink-dim:      #a8a8bd;   /* secondary text */
  --ink-faint:    #6c6c85;   /* tertiary / disabled */

  /* Signal colors */
  --accent:       #00e5ff;   /* hot accent: CTAs, routes, reveal meter */
  --accent-deep:  #00a6bd;   /* accent, pressed */
  --accent-ink:   #00181d;   /* text ON accent */
  --danger:       #ff2b45;   /* wrong answers, overtime, penalties */
  --danger-deep:  #b3001a;
  --danger-ink:   #ffffff;   /* text ON danger */
  --gold:         #ffc844;   /* score, points, money */
  --gold-deep:    #c99400;
  --gold-ink:     #241900;   /* text ON gold */
  --warn:         #ff7a1a;   /* 5-minute warning, klaxon */
  --good:         #35e07a;   /* connected, solved */

  /* Glows — used sparingly, they are the whole look */
  --glow-accent:  0 0 28px rgba(0, 229, 255, .45);
  --glow-danger:  0 0 32px rgba(255, 43, 69, .5);
  --glow-gold:    0 0 26px rgba(255, 200, 68, .4);

  /* Type */
  --font-display: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-body:    system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono:    ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --fs-timer:   clamp(4rem,    22vw,  9rem);      /* MM:SS across a table */
  --fs-slam:    clamp(3rem,    15vw,  6rem);      /* takeover headline */
  --fs-display: clamp(2.5rem,  11vw,  4.5rem);    /* screen headline */
  --fs-title:   clamp(1.75rem,  7.5vw, 2.75rem);  /* section title */
  --fs-lead:    clamp(1.25rem,  5vw,   1.75rem);  /* lead paragraph */
  --fs-body:    clamp(1.0625rem, 4.2vw, 1.25rem);
  --fs-small:   clamp(0.8125rem, 3.2vw, 0.9375rem);
  --fs-micro:   clamp(0.6875rem, 2.8vw, 0.8125rem);

  /* Metrics — --tap is the floor for every interactive control */
  --tap:      56px;
  --gap:      12px;   /* minimum spacing between controls (spec floor: 8px) */
  --pad:      20px;
  --radius:   14px;
  --radius-lg: 22px;
  --statusbar-h: 52px;

  /* Motion */
  --dur-instant: 90ms;
  --dur-fast:    160ms;
  --dur-screen:  200ms;   /* screen cross-fade; JS mirrors this as SCREEN_FADE_MS */
  --dur-slam:    320ms;   /* takeover slam-in */
  --dur-slow:    900ms;   /* zoom-crop pull-back (T6) */
  --ease-out:    cubic-bezier(.22, .61, .36, 1);
  --ease-slam:   cubic-bezier(.16, 1.4, .4, 1);

  /* Layers */
  --z-screen:   1;
  --z-status:   50;
  --z-toast:    80;
  --z-takeover: 100;
}

/* --------------------------------------------------------------------------
   2. Reset + mobile gesture hardening
   -------------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

[hidden] { display: none !important; }

html, body {
  height: 100dvh;              /* dynamic viewport unit only — the URL bar lies */
  margin: 0;
  padding: 0;
  overscroll-behavior: none;   /* kills pull-to-refresh mid-round */
  touch-action: manipulation;  /* kills double-tap zoom */
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  -webkit-text-size-adjust: 100%;
  overflow: hidden;
}

body {
  display: flex;
  flex-direction: column;
  background-color: var(--ground);
  background-image:
    radial-gradient(115% 70% at 50% -10%, #1a1a27 0%, rgba(26,26,39,0) 62%),
    radial-gradient(90% 55% at 50% 108%, rgba(0,229,255,.10) 0%, rgba(0,229,255,0) 60%);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.35;
  overscroll-behavior: none;
}

h1, h2, h3, p, ul, ol, figure { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg, canvas { display: block; max-width: 100%; }
a { color: var(--accent); }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

/* --------------------------------------------------------------------------
   3. Status strip — persistent, never hidden by showScreen()
   -------------------------------------------------------------------------- */

.statusbar {
  position: relative;
  z-index: var(--z-status);
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: var(--statusbar-h);
  padding: 6px max(12px, env(safe-area-inset-right)) 6px max(12px, env(safe-area-inset-left));
  padding-top: calc(6px + env(safe-area-inset-top));
  background: linear-gradient(to bottom, #0d0d14, #0a0a10);
  border-bottom: 1px solid var(--line);
}

.statusbar__conn {
  flex: 0 0 auto;
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--ink-faint);
  box-shadow: 0 0 0 3px rgba(255,255,255,.04);
  transition: background-color var(--dur-fast) linear, box-shadow var(--dur-fast) linear;
}
.statusbar__conn[data-state="online"]     { background: var(--good);   box-shadow: 0 0 12px var(--good); }
.statusbar__conn[data-state="connecting"] { background: var(--warn);   box-shadow: 0 0 12px var(--warn); animation: pulse-dot 1.1s ease-in-out infinite; }
.statusbar__conn[data-state="offline"]    { background: var(--danger); box-shadow: 0 0 12px var(--danger); }

@keyframes pulse-dot { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }

.statusbar__title {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-display);
  font-size: var(--fs-small);
  font-weight: 800;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: default;
}

.statusbar__badge,
.statusbar__room {
  flex: 0 0 auto;
  padding: 5px 9px;
  border: 1px solid var(--line-hot);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-dim);
  background: var(--ground-lift);
}
.statusbar__badge[data-role="mc"]        { color: var(--accent-ink); background: var(--accent);  border-color: var(--accent); }
.statusbar__badge[data-role="kent"]      { color: var(--gold-ink);   background: var(--gold);    border-color: var(--gold); }
.statusbar__badge[data-role="spectator"] { color: var(--ink);        background: var(--surface); border-color: var(--line-hot); }
.statusbar__room { color: var(--gold); letter-spacing: .22em; }

/* --------------------------------------------------------------------------
   4. Screens — exactly one visible, cross-faded
   -------------------------------------------------------------------------- */

.screens {
  position: relative;
  z-index: var(--z-screen);
  flex: 1 1 auto;
  min-height: 0;
}

.screen {
  position: absolute;
  inset: 0;
  display: none;              /* the guarantee: nothing shows until showScreen() */
  flex-direction: column;
  gap: var(--gap);
  padding: var(--pad);
  padding-bottom: max(var(--pad), env(safe-area-inset-bottom));
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  opacity: 0;
  transition: opacity var(--dur-screen) var(--ease-out);
}

.screen.is-entering { display: flex; opacity: 0; }
.screen.is-active   { display: flex; opacity: 1; }
.screen.is-leaving  { display: flex; opacity: 0; pointer-events: none; }

/* --------------------------------------------------------------------------
   5. Typography helpers
   -------------------------------------------------------------------------- */

.display,
.title {
  font-family: var(--font-display);
  font-weight: 900;
  line-height: .92;
  letter-spacing: -.03em;
  text-transform: uppercase;
  text-wrap: balance;
}
.display { font-size: var(--fs-display); }
.title   { font-size: var(--fs-title); }

.eyebrow {
  font-family: var(--font-display);
  font-size: var(--fs-small);
  font-weight: 800;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.lead { font-size: var(--fs-lead); color: var(--ink-dim); text-wrap: pretty; }
.muted { color: var(--ink-dim); }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

.score {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--gold);
  text-shadow: var(--glow-gold);
}

.glow-accent { color: var(--accent); text-shadow: var(--glow-accent); }
.glow-danger { color: var(--danger); text-shadow: var(--glow-danger); }

/* --------------------------------------------------------------------------
   6. Controls — every one of them is at least --tap tall
   -------------------------------------------------------------------------- */

button,
[role="button"],
.btn,
input,
select,
textarea {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  font-family: inherit;
}

.btn,
button,
[role="button"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: var(--tap);
  padding: 14px 20px;
  border: 2px solid var(--line-hot);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--fs-body);
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition:
    transform var(--dur-instant) var(--ease-out),
    background-color var(--dur-fast) linear,
    border-color var(--dur-fast) linear,
    box-shadow var(--dur-fast) linear,
    opacity var(--dur-fast) linear;
}

.btn:active,
button:active,
[role="button"]:active { transform: scale(.97); background: var(--surface-hi); }

.btn--block { width: 100%; }

.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  box-shadow: var(--glow-accent);
}
.btn--primary:active { background: var(--accent-deep); border-color: var(--accent-deep); }

.btn--danger {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--danger-ink);
  box-shadow: var(--glow-danger);
}
.btn--danger:active { background: var(--danger-deep); border-color: var(--danger-deep); }

.btn--gold {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--gold-ink);
  box-shadow: var(--glow-gold);
}

.btn--ghost {
  background: transparent;
  border-color: currentColor;
  color: inherit;
  box-shadow: none;
}

.btn--huge {
  min-height: 76px;
  font-size: var(--fs-lead);
  border-radius: var(--radius-lg);
}

.btn[disabled],
button[disabled],
[role="button"][aria-disabled="true"] {
  opacity: .38;
  box-shadow: none;
  cursor: not-allowed;
  pointer-events: none;
}

/* Any stack of controls keeps them comfortably apart (spec floor: 8px). */
.stack { display: flex; flex-direction: column; gap: var(--gap); }
.row   { display: flex; flex-wrap: wrap; gap: var(--gap); }
.grow  { flex: 1 1 auto; }
.spacer { flex: 1 1 auto; min-height: var(--gap); }

.card {
  padding: var(--pad);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

/* --------------------------------------------------------------------------
   7. Takeover card — WRONG / CORRECT / TIME'S UP
   The one component that covers everything, including the status strip.
   -------------------------------------------------------------------------- */

.takeover {
  position: fixed;
  inset: 0;
  z-index: var(--z-takeover);
  display: none;
  align-items: center;
  justify-content: center;
  padding: max(var(--pad), env(safe-area-inset-top)) var(--pad)
           max(var(--pad), env(safe-area-inset-bottom));
  background: var(--to-ground, var(--ground));
  color: var(--to-ink, var(--ink));
  text-align: center;
}
.takeover.is-open { display: flex; animation: takeover-in var(--dur-slam) var(--ease-slam) both; }

.takeover[data-variant="wrong"] {
  --to-ground: var(--danger-deep);
  --to-ink: #fff;
  background-image: radial-gradient(90% 60% at 50% 40%, var(--danger) 0%, var(--danger-deep) 70%);
}
.takeover[data-variant="timeup"] {
  --to-ground: #1a0500;
  --to-ink: #fff;
  background-image: radial-gradient(90% 60% at 50% 40%, var(--warn) 0%, #7a1a00 65%, #1a0500 100%);
}
.takeover[data-variant="correct"] {
  --to-ground: #001d22;
  --to-ink: var(--ink);
  background-image: radial-gradient(90% 60% at 50% 40%, rgba(0,229,255,.35) 0%, #001d22 68%);
}

.takeover__card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  max-width: 34rem;
  width: 100%;
}

.takeover__eyebrow {
  font-family: var(--font-display);
  font-size: var(--fs-small);
  font-weight: 800;
  letter-spacing: .3em;
  text-transform: uppercase;
  opacity: .75;
  min-height: 1em;
}

.takeover__title {
  font-family: var(--font-display);
  font-size: var(--fs-slam);
  font-weight: 900;
  line-height: .88;
  letter-spacing: -.035em;
  text-transform: uppercase;
  text-wrap: balance;
  text-shadow: 0 6px 0 rgba(0,0,0,.35);
}
.takeover[data-variant="correct"] .takeover__title { color: var(--accent); text-shadow: var(--glow-accent); }

.takeover__sub {
  font-size: var(--fs-lead);
  font-weight: 600;
  opacity: .9;
  text-wrap: pretty;
  min-height: 1em;
}

.takeover.is-open .takeover__card { animation: slam var(--dur-slam) var(--ease-slam) both; }
.takeover.is-open[data-variant="wrong"] .takeover__card { animation: slam var(--dur-slam) var(--ease-slam) both, shake 340ms var(--ease-out) 40ms both; }

.takeover__card .btn { margin-top: 6px; opacity: .8; }

@keyframes takeover-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes slam {
  from { transform: scale(1.45); opacity: 0; }
  60%  { transform: scale(.97); opacity: 1; }
  to   { transform: scale(1); opacity: 1; }
}
@keyframes shake {
  0%, 100% { translate: 0 0; }
  20% { translate: -10px 0; }
  40% { translate: 9px 0; }
  60% { translate: -6px 0; }
  80% { translate: 4px 0; }
}

/* --------------------------------------------------------------------------
   8. Reduced motion — collapse everything to instant
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-instant: 1ms;
    --dur-fast:    1ms;
    --dur-screen:  1ms;
    --dur-slam:    1ms;
    --dur-slow:    1ms;
  }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0ms !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
}

/* --------------------------------------------------------------------------
   9. Wide / landscape — the shell still works if someone turns the phone
   -------------------------------------------------------------------------- */

@media (min-width: 620px) {
  :root { --pad: 28px; }
  .screen { align-items: center; }
  .screen > * { width: 100%; max-width: 34rem; }
}

/* ==========================================================================
   SCREEN-SPECIFIC — T6 (puzzle), T7 (correct/travel), T8 (enroute/atbar/timeup),
   T9 (roles/heckles), T10 (mc panel), T11 (finale) append below this line.
   Reuse the tokens in section 1. Do not redefine the palette.
   ========================================================================== */

/* --------------------------------------------------------------------------
   T6 — PUZZLE: zoom-crop reveal, clarity meter, name pool, two-step lock-in
   -------------------------------------------------------------------------- */

[data-screen="puzzle"] {
  gap: 14px;
  /* The pull-back. Slow to leave, quick through the middle, long settle — the
     table should feel it stop, not see a UI state change. */
  --ease-pull: cubic-bezier(.66, .02, .16, 1);
}

.puzzle__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 12px;
}

.puzzle__worth {
  font-size: var(--fs-small);
  color: var(--ink-dim);
}
.puzzle__worth .score { font-size: var(--fs-body); }

/* The crop window. Aspect must match the source art (4:3) or the pull-back
   skews; max-width caps the height without breaking that ratio. */
.puzzle__frame {
  position: relative;
  /* The screen is a flex column: without flex-basis:auto and no shrink, the
     frame gets squashed and aspect-ratio silently loses. A squashed frame means
     object-fit crops the source, and the focus point stops being where the
     config says it is. */
  flex: 0 0 auto;
  width: 100%;
  max-width: min(100%, calc(38svh * 4 / 3));
  aspect-ratio: 4 / 3;
  margin-inline: auto;
  overflow: hidden;
  border: 2px solid var(--line-hot);
  border-radius: var(--radius-lg);
  background: #000;
  box-shadow: 0 18px 44px rgba(0, 0, 0, .6), var(--glow-accent);
  isolation: isolate;
}

/* Vignette. Sells the "peering through a hole" read at 5x. */
.puzzle__frame::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 60px 12px rgba(0, 0, 0, .55);
}

.puzzle__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* Section 2 sets `img { max-width: 100% }`. Without these the scaled image is
     clamped back to the frame and the crop renders as a black panel. */
  max-width: none;
  max-height: none;
  object-fit: cover;
  transform: scale(1);
  transform-origin: 50% 50%;      /* JS overrides with the bar's focus point */
  transition: transform var(--dur-slow) var(--ease-pull);
  will-change: transform;
  backface-visibility: hidden;
  user-select: none;
  -webkit-user-drag: none;
}

/* A new round's opening crop lands instantly; it is not a pull-back. */
.puzzle__frame.is-instant .puzzle__photo { transition: none; }

.clarity {
  display: flex;
  align-items: center;
  gap: 6px;
}

.clarity__pip {
  flex: 1 1 0;
  height: 8px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface-hi);
  transition:
    background-color var(--dur-slow) var(--ease-pull),
    box-shadow var(--dur-slow) var(--ease-pull);
}
.clarity__pip[data-filled="true"] {
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: var(--glow-accent);
}

.puzzle__prompt { margin-top: 2px; }

/* Two columns keeps all six names and both controls on one 390x844 screen.
   auto-fit collapses the empty track, so round six's single survivor goes
   full width on its own. */
.puzzle__pool {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px;
  flex: 0 0 auto;
}

/* A name, not a command: mixed case, so the pool reads as content and the two
   real controls below it read as buttons. */
.choice {
  width: 100%;
  padding: 12px 14px;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 700;
  line-height: 1.15;
  text-wrap: balance;
}

.choice[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  box-shadow: var(--glow-accent);
  opacity: 1;
}

/* Already guessed, still tappable — Kent gets to keep making the mistake. */
.choice[data-wrong="true"] {
  color: var(--ink-faint);
  border-color: var(--line);
  opacity: .55;
  text-decoration: line-through;
  text-decoration-color: var(--danger);
  text-decoration-thickness: 3px;
}
.choice[data-wrong="true"][aria-pressed="true"] {
  color: var(--accent-ink);
  text-decoration-color: var(--danger-deep);
}

/* LOCK IT IN and REVEAL MORE stay under the thumb however long the pool is. */
.puzzle__actions {
  position: sticky;
  bottom: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: auto;
  padding-top: 12px;
  padding-bottom: max(2px, env(safe-area-inset-bottom));
  background: var(--ground);
  box-shadow: 0 -14px 22px 10px var(--ground);
}

.puzzle__cost {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: 0;
  opacity: .78;
}

/* Spectators watch. They see the crop, the meter and Kent's selection; they do
   not get to touch any of it. */
[data-screen="puzzle"][data-live="false"] .puzzle__pool { pointer-events: none; }
[data-screen="puzzle"][data-live="false"] .puzzle__actions { display: none; }

/* --------------------------------------------------------------------------
   T6 — WRONG: resting backdrop under the takeover card
   -------------------------------------------------------------------------- */

[data-screen="wrong"] {
  align-items: center;
  justify-content: center;
  text-align: center;
  background:
    radial-gradient(90% 60% at 50% 40%, rgba(255, 43, 69, .28) 0%, transparent 70%);
}

.wrongbed { display: flex; flex-direction: column; gap: 8px; align-items: center; }

/* --------------------------------------------------------------------------
   T7 — CORRECT: the crop snaps open, confetti, the name slams in
   -------------------------------------------------------------------------- */

[data-screen="correct"] {
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 16px;
  /* The snap is a different gesture from the puzzle's pull-back: fast out of
     the gate, long settle. It should feel like a lid coming off. */
  --dur-snap: 600ms;
  --ease-snap: cubic-bezier(.16, .9, .24, 1);
  background:
    radial-gradient(90% 60% at 50% 42%, rgba(0, 229, 255, .22) 0%, transparent 70%);
}

.correct__eyebrow { color: var(--accent); }

.correct__frame {
  position: relative;
  /* Same flex trap as the puzzle frame: without this, aspect-ratio loses and
     object-fit re-crops the photo away from its focus point. */
  flex: 0 0 auto;
  width: 100%;
  max-width: min(100%, calc(40svh * 4 / 3));
  aspect-ratio: 4 / 3;
  margin-inline: auto;
  overflow: hidden;
  border: 2px solid var(--accent);
  border-radius: var(--radius-lg);
  background: #000;
  box-shadow: 0 18px 44px rgba(0, 0, 0, .6), var(--glow-accent);
}

.correct__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* Section 2's `img { max-width: 100% }` clamps a scaled photo to the frame
     and renders it as a black panel. Same trap T2 documented, same fix. */
  max-width: none;
  max-height: none;
  object-fit: cover;
  transform: scale(1);
  transform-origin: 50% 50%;      /* JS overrides with the bar's focus point */
  transition: transform var(--dur-snap) var(--ease-snap);
  will-change: transform;
  backface-visibility: hidden;
  user-select: none;
  -webkit-user-drag: none;
}

/* The opening crop is inherited from the puzzle, not animated into. */
.correct__frame.is-instant .correct__photo { transition: none; }

.correct__name {
  color: var(--accent);
  text-shadow: var(--glow-accent);
  text-wrap: balance;
}

.correct__name.is-slammed {
  animation: correct-slam calc(var(--dur-slam) * 1.7) var(--ease-slam) 180ms both;
}

@keyframes correct-slam {
  from { opacity: 0; transform: scale(.35) rotate(-4deg); }
  62%  { opacity: 1; transform: scale(1.14) rotate(1.5deg); }
  to   { opacity: 1; transform: scale(1) rotate(0deg); }
}

.correct__points {
  font-size: var(--fs-lead);
  color: var(--ink-dim);
}

/* Full-bleed over the whole screen, above the frame, never in the way of a tap.
   `max-width: none` defeats the 34rem cap section 9 puts on `.screen > *`. */
.correct__confetti {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  z-index: 3;
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   T7 — TRAVEL: the map, the drawn route, the walking party
   -------------------------------------------------------------------------- */

[data-screen="travel"] {
  align-items: center;
  text-align: center;
  gap: 12px;
  /* One easing shared by the route draw and the walk so the party stays on the
     head of the line. screens/travel.js mirrors it for the sampled fallback. */
  --ease-walk: cubic-bezier(.42, 0, .58, 1);
}

/* Section 9 caps `.screen > *` at 34rem on wide screens. Right for portrait
   content, wrong for a full-bleed map. */
[data-screen="travel"] > * { max-width: none; }

/* The map bleeds into this section's padding, which otherwise leaves 20px of
   horizontal scroll to drag around under a thumb. Clips exactly at the padding
   edge, so nothing visible is lost. */
[data-screen="travel"] { overflow-x: hidden; }

.travel__head { flex: 0 0 auto; }
.travel__dest { color: var(--accent); text-shadow: var(--glow-accent); }

/* Aspect matches the artwork (2000x1400) exactly, so the SVG overlay's viewBox
   and the drawn pins share one coordinate system. No border and no padding:
   offset-path resolves against this box, and either would shift the route. */
.travel__map {
  position: relative;
  flex: 0 0 auto;
  /* Bleeds out through the screen's padding: a landscape map on a portrait
     phone is width-bound, and every pixel of width is a pixel of map.
     `align-self: center` (not auto margins) is what keeps the overflow even on
     both sides when the height cap bites in landscape. */
  align-self: center;
  width: calc(100% + var(--pad) * 2);
  aspect-ratio: 2000 / 1400;
  max-width: min(calc(100% + var(--pad) * 2), calc(58svh * 2000 / 1400));
  /* Portrait leaves a lot of room under a 2000x1400 map; centre it rather than
     stranding it under the title with a void below. */
  margin-block: auto;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: #0d1420;
  box-shadow: 0 18px 44px rgba(0, 0, 0, .6);
}

.travel__mapimg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: contain;
  user-select: none;
  -webkit-user-drag: none;
}

.travel__route {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.travel__line {
  stroke: var(--accent);
  /* Map units: at a 350px-wide map this is ~3px on screen. Anything thinner
     disappears against the artwork on a phone in a dark bar. */
  stroke-width: 18;
  stroke-dasharray: 52 36;
  stroke-linecap: round;
  filter: drop-shadow(0 0 10px rgba(0, 229, 255, .75));
}

/* Bars already behind the party. Stamped, never labelled. */
.travel__stamp {
  fill: rgba(255, 200, 68, .14);
  stroke: var(--gold);
  stroke-width: 7;
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation: travel-stamp 460ms var(--ease-slam) both;
}

@keyframes travel-stamp {
  from { opacity: 0; transform: scale(2.4); }
  70%  { opacity: 1; transform: scale(.86); }
  to   { opacity: 1; transform: scale(1); }
}

/* Round one only: the party walks in from off the map. */
.travel__origin {
  fill: rgba(255, 255, 255, .2);
  stroke: rgba(255, 255, 255, .55);
  stroke-width: 5;
}

.travel__pulse,
.travel__pulse2 {
  fill: none;
  stroke: var(--accent);
  stroke-width: 9;
  opacity: 0;
  transform-box: fill-box;
  transform-origin: 50% 50%;
}

[data-screen="travel"][data-arriving="true"] .travel__pulse {
  animation: travel-pulse 900ms var(--ease-out) 3 both;
}
[data-screen="travel"][data-arriving="true"] .travel__pulse2 {
  animation: travel-pulse 900ms var(--ease-out) 300ms 3 both;
}

@keyframes travel-pulse {
  from { opacity: .95; transform: scale(.45); }
  to   { opacity: 0;   transform: scale(2); }
}

/* The party anchor is a zero-size point so it rides the path regardless of
   whether the browser honours offset-anchor. Figures hang off it. */
.travel__party {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
  z-index: 5;
  offset-rotate: 0deg;
  will-change: offset-distance, transform;
}

.travel__figure {
  position: absolute;
  left: 0;
  top: 0;
  width: 16px;
  height: 24px;
  --fig-color: var(--accent);
  --fig-skin: #ffe4c4;
  /* Placement uses the individual translate/scale properties so `transform` is
     left free for the walk cycle. */
  translate: calc(-50% + var(--dx, 0px)) calc(-100% + var(--dy, 0px));
  scale: var(--fig-scale, 1);
  transform-origin: 50% 100%;
  animation: fig-bob 440ms ease-in-out infinite;
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .65));
}

/* Colour cycle — a crowd, not a chorus line. */
.travel__figure:nth-child(5n + 2) { --fig-color: var(--gold); }
.travel__figure:nth-child(5n + 3) { --fig-color: #ffffff; }
.travel__figure:nth-child(5n + 4) { --fig-color: #7cf9c1; }
.travel__figure:nth-child(5n + 5) { --fig-color: var(--danger); }

.travel__figure-body {
  position: absolute;
  left: 50%;
  bottom: 4px;
  width: 11px;
  height: 12px;
  margin-left: -5.5px;
  border-radius: 6px 6px 3px 3px;
  background: var(--fig-color);
  transform-origin: 50% 100%;
  /* The squash-stretch is what makes it a walk instead of a glide. */
  animation: fig-step 440ms ease-in-out infinite;
  animation-delay: inherit;
}

.travel__figure-head {
  position: absolute;
  left: 50%;
  bottom: 15px;
  width: 9px;
  height: 9px;
  margin-left: -4.5px;
  border-radius: 50%;
  background: var(--fig-skin);
  box-shadow: 0 0 0 1.5px rgba(0, 0, 0, .35);
}

.travel__figure::before,
.travel__figure::after {
  content: "";
  position: absolute;
  bottom: 0;
  width: 3px;
  height: 5px;
  border-radius: 2px;
  background: var(--fig-color);
  transform-origin: 50% 0;
  animation: fig-leg 440ms ease-in-out infinite alternate;
  animation-delay: inherit;
}
.travel__figure::before { left: calc(50% - 4px); }
.travel__figure::after  { left: calc(50% + 1px); animation-direction: alternate-reverse; }

@keyframes fig-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-2.5px); }
}

@keyframes fig-step {
  0%, 100% { transform: scaleY(1) scaleX(1); }
  50%      { transform: scaleY(.9) scaleX(1.07); }
}

@keyframes fig-leg {
  from { transform: rotate(-22deg); }
  to   { transform: rotate(22deg); }
}

/* Fades in at 2s. Kept in the layout the whole time so nothing jumps under a
   thumb, and never below the 56px floor. */
.travel__skip {
  flex: 0 0 auto;
  width: 100%;
  /* No auto margin here: .travel__map's `margin-block: auto` already eats all
     the free space, which centres the map and leaves this on the bottom edge
     under a thumb. */
  visibility: hidden;
  opacity: 0;
  transition: opacity var(--dur-screen) linear;
}
.travel__skip[data-ready="true"] { visibility: visible; opacity: 1; }

/* Spectators watch the walk; they do not get to end it. */
[data-screen="travel"][data-live="false"] .travel__skip { display: none; }

/* ==========================================================================
   T8 — EN ROUTE / AT THE BAR / OVERTIME
   ==========================================================================
   Three screens carrying the least glamorous stretch of the night: the walk,
   the arrival, and the half hour on site.

   The clock is the whole point of two of them, so it is deliberately absurd —
   --fs-timer is clamp(4rem, 22vw, 9rem), i.e. ~86px on a 390px phone, in the
   monospace face with tabular figures so the digits do not dance. It has to be
   readable from the other end of a bar.

   Colour is state, and the state comes from
   [data-screen="at-bar"][data-timer="idle|running|warn|critical|over"], set by
   screens/atbar.js on every tick from `remaining()`. It is never set by a
   class toggle on a schedule — there is no schedule, only the subtraction.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Shared — the role note under an MC-only control
   -------------------------------------------------------------------------- */

/* Rendered on every device; hidden for the MC in JS. The disabled attribute is
   the visual half of the story — the enforcing half is the role check in the
   click handler, because el.click() ignores both [disabled] and
   pointer-events:none. */
.rolenote {
  margin: 0;
  font-size: var(--fs-small);
  color: var(--ink-faint);
  text-align: center;
  text-wrap: pretty;
}

/* --------------------------------------------------------------------------
   Shared — the clock
   -------------------------------------------------------------------------- */

.clockwrap {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  /* Breathing room above the number, not `margin-block: auto` — flex-grow is
     resolved BEFORE auto margins, so the .spacer below has already eaten every
     pixel of free space by the time an auto margin would get a look in. The
     clock therefore sits just under the arrival card and the controls stay
     pinned to the bottom edge, under a thumb, which is where they want to be. */
  margin-top: clamp(8px, 4vh, 40px);
}

.clock {
  margin: 0;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-timer);
  font-weight: 800;
  line-height: .92;
  /* Mono at this size is wide; a touch of negative tracking keeps "+MM:SS"
     inside a 390px viewport without dropping the size. */
  letter-spacing: -.05em;
  text-align: center;
  color: var(--ink);
  transition: color var(--dur-fast) linear, text-shadow var(--dur-fast) linear;
}

.clockwrap__label { color: var(--ink-faint); }

/* --- the four states ------------------------------------------------------ */

[data-timer="running"] .clock { color: var(--ink); }

/* 5:00 — accent, and one flash so it is noticed without being nagged about. */
[data-timer="warn"] .clock {
  color: var(--accent);
  text-shadow: var(--glow-accent);
  animation: clock-flash 900ms var(--ease-out) 2;
}
[data-timer="warn"] .clockwrap__label { color: var(--accent); }

/* 1:00 — red, and a faster pulse that does not stop. */
[data-timer="critical"] .clock {
  color: var(--danger);
  text-shadow: var(--glow-danger);
  animation: clock-pulse 620ms var(--ease-out) infinite;
}
[data-timer="critical"] .clockwrap__label { color: var(--danger); }

/* Past zero. Same red, no pulse — the number itself is doing the work now. */
[data-timer="over"] .clock,
.clock--over {
  color: var(--danger);
  text-shadow: var(--glow-danger);
}

@keyframes clock-flash {
  0%, 100% { opacity: 1; }
  50%      { opacity: .35; }
}

@keyframes clock-pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(1.045); opacity: .82; }
}

/* --------------------------------------------------------------------------
   ENROUTE — readable while walking, in the dark, one-handed
   -------------------------------------------------------------------------- */

.enroute__head { flex: 0 0 auto; }

.enroute__name {
  margin: 4px 0 0;
  color: var(--accent);
  text-shadow: var(--glow-accent);
}

/* Sized up from .lead: this is read off a phone held at arm's length, at night,
   while walking, by someone who has had a drink. 20px was not enough. */
.enroute__address {
  margin: 10px 0 0;
  font-size: var(--fs-title);
  font-weight: 500;
  line-height: 1.1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.enroute__actions { flex: 0 0 auto; }

/* Extra separation from OPEN IN MAPS. A thumb reaching for the map link that
   lands on WE MADE IT instead starts the arrival beat fifteen minutes early. */
#enroute-arrived { margin-top: 10px; }

/* An <a> is not a <button>, so it does not inherit the flex centring from the
   control block above; .btn covers the rest (min-height: var(--tap)). */
#enroute-maps { display: flex; }

/* --------------------------------------------------------------------------
   AT_BAR — the arrival card, then the clock
   -------------------------------------------------------------------------- */

.atbar__head { flex: 0 0 auto; }

.atbar__name {
  margin: 4px 0 0;
  color: var(--accent);
  text-shadow: var(--glow-accent);
}

/* The trash talk. Deliberately larger than .lead — this line gets read out
   loud to a table in a noisy room, so it is title-sized but not shouted
   (weight 600, sentence case, normal tracking). */
.atbar__card {
  flex: 0 0 auto;
  margin: 6px 0 0;
  font-size: var(--fs-title);
  font-weight: 600;
  line-height: 1.14;
  color: var(--ink);
  text-wrap: pretty;
}

.atbar__actions { flex: 0 0 auto; }

/* "START THE CLOCK  30:00" — the duration sits inside the button so the MC can
   see what they are about to commit to (and, in demo mode, that it is 00:30). */
#atbar-start-duration {
  color: inherit;
  opacity: .72;
  font-weight: 700;
}

/* --------------------------------------------------------------------------
   TIME_UP — the overtime joke
   -------------------------------------------------------------------------- */

.timeup__head { flex: 0 0 auto; }

.timeup__slam {
  margin: 4px 0 0;
  color: var(--danger);
  text-shadow: var(--glow-danger);
}

.timeup__msg {
  flex: 0 0 auto;
  margin: 0;
  text-align: center;
  color: var(--ink-dim);
}

.timeup__actions { flex: 0 0 auto; }

/* --------------------------------------------------------------------------
   Short viewports — a 667px phone in a bar with the URL bar showing
   -------------------------------------------------------------------------- */

@media (max-height: 700px) {
  .clock { font-size: clamp(3rem, 17vw, 6rem); }
}

/* ==========================================================================
   T9 — ROLE VIEWS, JOIN FLOW, HECKLES
   Appended, nothing above reformatted. T10 and T11 append after this block.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Role picker — the first-run screen. Only a device with no ?role= and no
   remembered choice ever sees it, so it must be unmissable and then never
   again.
   -------------------------------------------------------------------------- */

.picker {
  position: fixed;
  inset: 0;
  z-index: 200;                 /* above the takeover card: it blocks boot */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(var(--pad), env(safe-area-inset-top)) var(--pad)
           max(var(--pad), env(safe-area-inset-bottom));
  background:
    radial-gradient(120% 70% at 50% 0%, rgba(0, 229, 255, .10) 0%, transparent 60%),
    var(--ground);
  overflow-y: auto;
}

.picker__card {
  width: 100%;
  max-width: 460px;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

.picker__title { margin: 2px 0 6px; color: var(--ink); }

.picker__fields { display: flex; flex-direction: column; gap: var(--gap); }

.field { display: flex; flex-direction: column; gap: 6px; }

.field__label {
  font-family: var(--font-display);
  font-size: var(--fs-micro);
  font-weight: 800;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.field__input {
  min-height: var(--tap);
  padding: 12px 16px;
  border: 2px solid var(--line-hot);
  border-radius: var(--radius);
  background: var(--ground-lift);
  color: var(--ink);
  font-size: var(--fs-body);
}
.field__input:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--glow-accent); }

.field__input--code {
  font-family: var(--font-mono);
  font-size: var(--fs-lead);
  font-weight: 700;
  letter-spacing: .35em;
  text-transform: uppercase;
  color: var(--gold);
}

.picker__roles { margin-top: 4px; }

.picker__role {
  flex-direction: column;
  gap: 3px;
  min-height: 76px;
  padding: 14px 18px;
  text-transform: none;
  text-align: left;
  align-items: flex-start;
}
.picker__role[data-pick-role="mc"] { border-color: var(--accent); }
.picker__role[data-pick-role="kent"] { border-color: var(--gold); }

.picker__rolelabel {
  font-family: var(--font-display);
  font-size: var(--fs-body);
  font-weight: 900;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.picker__rolehint {
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-dim);
}

/* --------------------------------------------------------------------------
   INTRO — the join flow. The MC's version is the only one with QR codes;
   followers never build them, so there is nothing here to hide.
   -------------------------------------------------------------------------- */

.intro__head { flex: 0 0 auto; }
.intro__hed { margin: 2px 0 0; color: var(--accent); text-shadow: var(--glow-accent); }

.roomcode {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: center;
  padding: 12px var(--pad) 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  text-align: center;
}

.roomcode__value {
  margin: 0;
  font-size: clamp(2.75rem, 16vw, 4.5rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: .18em;
  text-indent: .18em;              /* the tracking would otherwise sit it left */
  color: var(--gold);
  text-shadow: var(--glow-gold);
}

.roomcode__hint {
  margin: 0;
  font-size: var(--fs-small);
  color: var(--ink-dim);
}

.qrs {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
}

.qrfig {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

/* White ground and a quiet margin are not decoration — this is photographed
   off one phone screen by another, in a dim bar. */
.qrfig__code {
  display: block;
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 0 0 6px #fff, 0 6px 18px rgba(0, 0, 0, .5);
  image-rendering: pixelated;
}

.qrfig__cap {
  font-family: var(--font-display);
  font-size: var(--fs-micro);
  font-weight: 800;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-dim);
  text-align: center;
}

.intro__actions { flex: 0 0 auto; }
.intro__waiting { flex: 0 0 auto; text-align: center; }

.mcflag {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-micro);
  font-weight: 800;
  letter-spacing: .16em;
  text-transform: uppercase;
  text-align: center;
  color: var(--accent);
}

.poolnote { margin: 6px 0 0; }

/* --------------------------------------------------------------------------
   Roster — the presence list IS the party
   -------------------------------------------------------------------------- */

.roster { flex: 0 0 auto; display: flex; flex-direction: column; gap: 6px; }

.roster__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.roster__item {
  display: inline-flex;
  align-items: baseline;
  gap: 7px;
  padding: 7px 12px;
  border: 1px solid var(--line-hot);
  border-radius: 999px;
  background: var(--ground-lift);
  font-size: var(--fs-small);
}
.roster__item[data-role="mc"]   { border-color: var(--accent); }
.roster__item[data-role="kent"] { border-color: var(--gold); }

.roster__name { font-weight: 700; color: var(--ink); }

.roster__role {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.roster__me {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  color: var(--accent);
}

.roster__empty { color: var(--ink-faint); font-size: var(--fs-small); }

/* --------------------------------------------------------------------------
   Heckle bursts — MC and Kent only.
   The layer never takes a tap, and js/heckles.js spawns every burst ABOVE the
   protected controls and only ever moves it further up, so a burst can never
   sit over LOCK IT IN or the name pool.
   -------------------------------------------------------------------------- */

#heckle-layer {
  position: fixed;
  inset: 0;
  z-index: var(--z-toast);
  pointer-events: none;
  overflow: hidden;
}

.heckle {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  width: max-content;
  max-width: 40vw;
  pointer-events: none;
  will-change: transform, opacity;
}

.heckle__emoji {
  font-size: 44px;
  line-height: 1;
  filter: drop-shadow(0 3px 10px rgba(0, 0, 0, .65));
}

.heckle__who {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-display);
  font-size: var(--fs-micro);
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-dim);
  text-shadow: 0 1px 6px #000, 0 0 14px #000;
}

/* --------------------------------------------------------------------------
   Heckle bar — spectators only. Six emoji, one shared cooldown, because the
   rate limit is per member and not per button.
   -------------------------------------------------------------------------- */

.hecklebar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 90;                  /* over the screens, under the takeover card */
  display: flex;
  gap: 6px;
  padding: 8px max(8px, env(safe-area-inset-left))
           calc(8px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-right));
  background: linear-gradient(to top, #0a0a10 60%, rgba(10, 10, 16, .86));
  border-top: 1px solid var(--line);
}

.hecklebar__btn {
  flex: 1 1 0;
  min-width: 0;
  min-height: var(--tap);
  padding: 6px 0;
  border-radius: var(--radius);
  border-color: var(--line);
  background: var(--surface);
  font-size: 28px;
  line-height: 1;
}

.hecklebar[data-cooling="true"] .hecklebar__btn {
  opacity: .4;
  pointer-events: none;
}

/* The bar floats over the screens, so give every screen room to end above it. */
body[data-heckles="true"] .screen {
  padding-bottom: calc(var(--tap) + 26px + env(safe-area-inset-bottom));
}

@media (prefers-reduced-motion: reduce) {
  .heckle { transition: opacity var(--dur-screen) linear; }
}

/* --------------------------------------------------------------------------
   T10 — MC override panel, demo badge, paused clock, undo toast

   Nothing here is reachable without the MC role: `js/mcpanel.js` never BUILDS
   the panel on a follower, so these rules have nothing to match on Kent's or a
   spectator's device. The paused readout and the demo badge are the two
   exceptions and both are indicators, not controls.
   -------------------------------------------------------------------------- */

/* --- DEMO badge — permanent, on every screen, ?demo only ------------------ */

.statusbar__demo {
  flex: 0 0 auto;
  padding: 5px 9px;
  border: 1px solid var(--warn);
  border-radius: 999px;
  background: var(--warn);
  color: #1a0a00;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 800;
  letter-spacing: .18em;
  text-transform: uppercase;
  box-shadow: 0 0 14px rgba(255, 122, 26, .5);
}

/* --- Paused clock --------------------------------------------------------
   While parked, `timerEndsAt` sits on an absolute anchor decades out, so the
   real clock would read nonsense. Swap it for the banked remainder. Both
   elements live inside T8's `#atbar-clockwrap`; nothing is moved. */

#paused-clock,
#paused-clock-label { display: none; }

.clock--paused { text-shadow: 0 0 26px rgba(255, 122, 26, .45); }

/* Addressed by id on purpose: T8's `[data-screen][data-timer] .clock` zone
   colours out-specify a bare class, so a `.clock--paused { color }` would be
   silently overridden and the frozen clock would read as a running one. */
body[data-paused="true"] #paused-clock { display: block; color: var(--warn); }
body[data-paused="true"] #paused-clock-label { display: block; color: var(--warn); }
body[data-paused="true"] #atbar-clock,
body[data-paused="true"] #atbar-clocklabel { display: none; }

/* --- Undo toast ----------------------------------------------------------
   Above the takeover card on purpose: the WRONG slam holds for three seconds
   and the undo window is five, so a toast underneath it would be unusable for
   most of its life. */

.undotoast {
  position: fixed;
  z-index: calc(var(--z-takeover) + 10);
  left: max(12px, env(safe-area-inset-left));
  right: max(12px, env(safe-area-inset-right));
  bottom: calc(14px + env(safe-area-inset-bottom));
  display: flex;
  align-items: center;
  gap: var(--gap);
  padding: 10px 10px 10px 16px;
  border: 1px solid var(--line-hot);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: 0 18px 40px rgba(0, 0, 0, .65);
}

.undotoast[hidden] { display: none; }

.undotoast__label {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--ink-dim);
  font-size: var(--fs-small);
  font-weight: 700;
}

.undotoast__btn {
  flex: 0 0 auto;
  min-width: 120px;
  color: var(--accent);
}

/* The toast is pinned to the bottom edge and so is REVEAL MORE / LOCK IT IN.
   The puzzle screen is exactly one viewport tall and does not scroll, so
   without this the toast covers the round's primary control for its whole
   life — measured: #reveal-more's centre hit-tested to the toast. Same shape
   as T9's `body[data-heckles="true"] .screen` rule, and it stacks with it. */
body[data-undo="true"] .screen {
  padding-bottom: calc(92px + env(safe-area-inset-bottom));
}
body[data-undo="true"][data-heckles="true"] .screen {
  padding-bottom: calc(92px + var(--tap) + 26px + env(safe-area-inset-bottom));
}

/* --- The sheet -----------------------------------------------------------
   Above the takeover too: an MC who needs the override while a card is up
   should not have to wait the card out. */

.mcsheet {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-takeover) + 20);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.mcsheet[hidden] { display: none; }

.mcsheet__scrim {
  position: absolute;
  inset: 0;
  background: rgba(4, 4, 8, .72);
}

.mcsheet__sheet {
  position: relative;
  max-height: 92vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 10px var(--pad) calc(var(--pad) + env(safe-area-inset-bottom));
  border-top: 2px solid var(--accent);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: var(--ground-lift);
  box-shadow: 0 -20px 60px rgba(0, 0, 0, .7);
  animation: mcsheet-up var(--dur-screen) var(--ease-out) both;
}

@keyframes mcsheet-up {
  from { transform: translateY(14%); opacity: 0; }
  to   { transform: translateY(0);   opacity: 1; }
}

.mcsheet__grab {
  align-self: center;
  width: 44px;
  height: 4px;
  border-radius: 999px;
  background: var(--line-hot);
}

.mcsheet__title { margin: 0; color: var(--accent); }

.mcsheet__status {
  margin: 0 0 2px;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: .1em;
}
.mcsheet__status[data-flash="true"] { color: var(--warn); }

.mcsheet__head { margin: 8px 0 0; color: var(--ink-faint); }
.mcsheet__head--danger { color: var(--danger); }

/* Two controls per row, each still at the 56px floor. */
.mcsheet__row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
}

.mcsheet__wide { width: 100%; }

/* Three across, not six: six across a 390px phone gives 52px-wide buttons and
   the floor for a tap target is 56px in BOTH directions. */
.mcsheet__rounds {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.mcsheet__round {
  min-height: var(--tap);
  padding: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-body);
}
.mcsheet__round[data-current="true"] {
  border-color: var(--accent);
  color: var(--accent);
}

/* --- Reset: never one tap ------------------------------------------------ */

.mcsheet__confirm {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: var(--gap);
  border: 2px solid var(--danger);
  border-radius: var(--radius);
  background: rgba(255, 43, 69, .08);
}
.mcsheet__confirm[hidden] { display: none; }

.mcsheet__confirmnote {
  margin: 0;
  font-size: var(--fs-small);
  color: var(--ink-dim);
}

.mcsheet__input {
  min-height: var(--tap);
  padding: 0 14px;
  border: 2px solid var(--line-hot);
  border-radius: var(--radius);
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--fs-lead);
  letter-spacing: .2em;
  text-transform: uppercase;
}
.mcsheet__input:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
.mcsheet__input[data-bad="true"] { border-color: var(--danger); }

@media (prefers-reduced-motion: reduce) {
  .mcsheet__sheet { animation-duration: 1ms; }
}

/* --------------------------------------------------------------------------
   T11 — FINALE: count-up total, grade band, six-bar recap, completed map,
   and the only line the whole night was actually for.

   This is the one screen that gets left on a table for an hour, so it is the
   one screen built to be SCROLLED: `.screen` already scrolls, and every block
   below is `flex: 0 0 auto` so nothing collapses into the column. There are no
   fixed-position controls here — the bottom padding rules T9 and T10 added to
   `.screen` for the heckle bar and the undo toast therefore need no partner.
   -------------------------------------------------------------------------- */

[data-screen="finale"] {
  align-items: center;
  text-align: center;
  gap: 18px;
  /* Same easing as the travel route so the finished line is drawn the way the
     table watched each leg get drawn. Scoped, exactly as travel's is. */
  --ease-walk: cubic-bezier(.42, 0, .58, 1);
}

[data-screen="finale"] > * { flex: 0 0 auto; }

/* --- The number ---------------------------------------------------------- */

.finale__hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding-top: 4px;
}

.finale__total {
  font-size: var(--fs-timer);
  line-height: .88;
  letter-spacing: -.04em;
}

/* Land the number: it settles the instant the count-up hands over. */
[data-screen="finale"][data-counted="true"] .finale__total {
  animation: finale-land 460ms var(--ease-slam) both;
}

@keyframes finale-land {
  from { transform: scale(1.08); text-shadow: 0 0 60px rgba(255, 200, 68, .95); }
  to   { transform: scale(1);    text-shadow: var(--glow-gold); }
}

.finale__outof {
  font-size: var(--fs-small);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.finale__grade {
  margin-top: 8px;
  padding: 10px 18px;
  border: 2px solid currentColor;
  border-radius: 999px;
  font-family: var(--font-display);
  font-size: clamp(1.1rem, 5.4vw, 1.6rem);
  font-weight: 900;
  letter-spacing: .1em;
  text-transform: uppercase;
  line-height: 1;
  animation: finale-band 520ms var(--ease-slam) 1.9s both;
}

.finale__grade[data-band="legend"]      { color: var(--gold);   text-shadow: var(--glow-gold); }
.finale__grade[data-band="respectable"] { color: var(--accent); text-shadow: var(--glow-accent); }
.finale__grade[data-band="upright"]     { color: var(--danger); text-shadow: var(--glow-danger); }

@keyframes finale-band {
  from { opacity: 0; transform: scale(.7) rotate(-3deg); }
  to   { opacity: 1; transform: scale(1) rotate(-1.5deg); }
}

.finale__verdict {
  font-size: var(--fs-body);
  max-width: 30rem;
  animation: finale-rise 500ms var(--ease-out) 2.2s both;
}

/* --- Four numbers about the night ---------------------------------------- */

.finale__stats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
  width: 100%;
}

.finale__stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--ground-lift);
}

.finale__statval {
  font-size: var(--fs-lead);
  font-weight: 700;
  color: var(--ink);
}

.finale__statlabel {
  font-size: var(--fs-micro);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* Only ever shown on a device that missed part of the night. A blank is more
   honest than a number this phone had to guess at. */
.finale__timenote {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.finale__sectionhead {
  width: 100%;
  margin-top: 6px;
  text-align: left;
  color: var(--ink-faint);
}

/* --- The recap grid ------------------------------------------------------ */

.finale__recap {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
  width: 100%;
  text-align: left;
}

.finale__card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  animation: finale-rise 460ms var(--ease-out) both;
}

.finale__card[data-skipped="true"] { border-color: var(--line-hot); opacity: .72; }

.finale__shot {
  position: relative;
  aspect-ratio: 4 / 3;
  flex: 0 0 auto;              /* aspect-ratio collapses in a flex column without it */
  background: #0d1420;
  border-bottom: 1px solid var(--line);
}

/* A photo clamped by its container renders as a black box. */
.finale__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: cover;
  -webkit-user-drag: none;
}

.finale__num {
  position: absolute;
  top: 6px;
  left: 6px;
  min-width: 26px;
  padding: 3px 7px;
  border-radius: 8px;
  background: rgba(8, 8, 11, .82);
  color: var(--gold);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 700;
  text-align: center;
}

.finale__meta {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 10px;
}

.finale__barname {
  font-family: var(--font-display);
  font-size: clamp(.95rem, 3.9vw, 1.1rem);
  font-weight: 900;
  line-height: 1.05;
  letter-spacing: -.01em;
  text-transform: uppercase;
  text-wrap: balance;
}

.finale__barpts { font-size: var(--fs-small); }
.finale__barpts .score { font-size: clamp(1.05rem, 4.4vw, 1.3rem); }
.finale__ptslabel { color: var(--ink-faint); font-size: var(--fs-micro); }

.finale__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.finale__chip {
  padding: 3px 7px;
  border: 1px solid var(--line-hot);
  border-radius: 7px;
  font-size: var(--fs-micro);
  line-height: 1.2;
  color: var(--ink-dim);
}
.finale__chip[data-tone="good"] { border-color: rgba(53, 224, 122, .5); color: var(--good); }
.finale__chip[data-tone="bad"]  { border-color: rgba(255, 43, 69, .55);  color: var(--danger); }
.finale__chip[data-tone="gold"] { border-color: rgba(255, 200, 68, .5);  color: var(--gold); }

/* --- The completed map --------------------------------------------------- */

.finale__map {
  position: relative;
  width: 100%;
  aspect-ratio: 2000 / 1400;
  flex: 0 0 auto;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: #0d1420;
  box-shadow: 0 18px 44px rgba(0, 0, 0, .6);
}

.finale__mapimg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: contain;
  -webkit-user-drag: none;
}

.finale__route {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.finale__line {
  stroke: var(--accent);
  stroke-width: 18;
  stroke-dasharray: 52 36;
  stroke-linecap: round;
  filter: drop-shadow(0 0 10px rgba(0, 229, 255, .75));
}

/* Six for six. Same stamp the party watched land behind them all night. */
.finale__stamp {
  fill: rgba(255, 200, 68, .16);
  stroke: var(--gold);
  stroke-width: 7;
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation: travel-stamp 460ms var(--ease-slam) both;
}
.finale__stamp[data-solved="false"] {
  fill: none;
  stroke: var(--ink-faint);
  stroke-dasharray: 14 12;
}

/* --- Happy birthday ------------------------------------------------------ */

.finale__birthday {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: var(--tap);
  margin-top: 10px;
  padding: 22px 18px 20px;
  border: 2px solid var(--gold-deep);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(120% 130% at 50% -20%, rgba(255, 200, 68, .22) 0%, rgba(255, 200, 68, 0) 62%),
    var(--ground-lift);
  color: var(--ink);
  text-align: center;
}
.finale__birthday:active { background-color: var(--surface-hi); }

.finale__hbeyebrow { color: var(--ink-faint); }

.finale__hb {
  font-family: var(--font-display);
  font-size: var(--fs-display);
  font-weight: 900;
  line-height: .9;
  letter-spacing: -.03em;
  text-transform: uppercase;
  text-wrap: balance;
  color: var(--gold);
  text-shadow: var(--glow-gold);
  animation: finale-hb 2.6s ease-in-out 2.6s infinite;
}

@keyframes finale-hb {
  0%, 100% { transform: scale(1) rotate(-1deg);   text-shadow: var(--glow-gold); }
  50%      { transform: scale(1.035) rotate(1deg); text-shadow: 0 0 42px rgba(255, 200, 68, .8); }
}

.finale__hbnote {
  font-size: var(--fs-micro);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* --- Confetti ------------------------------------------------------------ */

/* Fixed, so it keeps raining over the recap while the screen is scrolled.
   Section 9's 34rem cap would otherwise pin it to the left of a wide screen. */
.finale__confetti {
  position: fixed;
  inset: 0;
  z-index: var(--z-toast);
  width: 100%;
  max-width: none;
  height: 100%;
  pointer-events: none;
}

@keyframes finale-rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

@media (min-width: 620px) {
  /* The map earns the extra width the 34rem column would take off it. */
  [data-screen="finale"] .finale__map { max-width: 44rem; }
  .finale__recap { grid-template-columns: repeat(3, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  .finale__card,
  .finale__verdict,
  .finale__grade { animation-duration: 1ms; animation-delay: 0ms; }
  .finale__hb { animation: none; }
  .finale__stamp { animation-duration: 1ms; }
  [data-screen="finale"][data-counted="true"] .finale__total { animation: none; }
}

/* --------------------------------------------------------------------------
   T12 — offline hardening: connection line, asset preload, wake-lock toggle,
   audio unlock prompt.

   Everything here is either IN FLOW (the connection line, the preload bar, the
   wake-lock toggle) or a single small button that only exists in a failure case
   (the audio prompt). Nothing in this block can ever cover the game: losing the
   wifi is not allowed to be an event on screen.
   -------------------------------------------------------------------------- */

/* Amber, not red. T3 painted `offline` with --danger, which is the same red as
   WRONG. DRINK. — the room reads that as a mistake somebody made. A dropped
   connection is amber and pulsing, exactly like `connecting`, because it IS
   connecting: pusher-js is retrying behind it. */
.statusbar__conn[data-state="offline"] {
  background: var(--warn);
  box-shadow: 0 0 12px var(--warn);
  animation: pulse-dot 1.8s ease-in-out infinite;
}

.connnote {
  flex: 0 0 auto;
  z-index: var(--z-status);
  padding: 5px max(12px, env(safe-area-inset-right)) 6px max(12px, env(safe-area-inset-left));
  background: #14100a;
  border-bottom: 1px solid var(--line);
  color: var(--warn);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: .04em;
  text-align: center;
}
.connnote[hidden] { display: none; }

/* --- INTRO asset preload -------------------------------------------------- */

.preload {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 6px 10px;
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--ground-lift);
}
.preload[hidden] { display: none; }

.preload__label {
  grid-column: 1;
  color: var(--ink-dim);
  font-size: var(--fs-small);
  font-weight: 700;
}

.preload__count {
  grid-column: 2;
  color: var(--ink-faint);
  font-size: var(--fs-micro);
}

.preload__track {
  grid-column: 1 / -1;
  height: 8px;
  overflow: hidden;
  border-radius: 999px;
  background: #000;
  box-shadow: inset 0 0 0 1px var(--line);
}

.preload__bar {
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--accent-deep), var(--accent));
  box-shadow: var(--glow-accent);
  transition: width var(--dur-fast) linear;
}

/* The blocking "assets not ready" state: START THE HUNT is disabled until the
   bar fills. It is the same disabled treatment the rest of the app uses, said
   out loud so a waiting MC knows it is not broken. */
[data-screen="intro"][data-preload="loading"] #intro-start {
  opacity: .45;
  filter: saturate(.4);
}
[data-screen="intro"][data-preload="loading"] #intro-start::after {
  content: " · loading art";
  font-size: var(--fs-micro);
  letter-spacing: .08em;
}

/* --- Wake-lock toggle (MC, AT_BAR, live clock only) ----------------------- */

.wakelock {
  min-height: var(--tap);
  padding: 0 18px;
  border: 1px solid var(--line-hot);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink-dim);
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: .04em;
}
.wakelock[hidden] { display: none; }
.wakelock[data-on="true"]  { color: var(--gold); border-color: var(--gold-deep); }
.wakelock[data-held="true"] { box-shadow: 0 0 18px rgba(255, 200, 68, .28); }

/* --- Audio unlock prompt (only ever visible if the unlock FAILED) ---------- */

.audioprompt {
  position: fixed;
  left: 50%;
  bottom: calc(14px + env(safe-area-inset-bottom));
  z-index: var(--z-toast);
  transform: translateX(-50%);
  min-height: var(--tap);
  padding: 0 22px;
  border: 1px solid var(--warn);
  border-radius: 999px;
  background: #1d1207;
  color: var(--warn);
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: 800;
  letter-spacing: .04em;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .6);
}

@media (prefers-reduced-motion: reduce) {
  .preload__bar { transition: none; }
  .statusbar__conn[data-state="offline"] { animation: none; }
}

/* T11 correction: the time note carries two very different messages — "a bar
   was skipped, that is why the clock has a gap" (routine) and "this device did
   not see part of the night" (a caveat on the numbers above it). Same element,
   different weight. */
.finale__timenote[data-tone="warn"] { color: var(--warn); }
