/* GeoParty — shared styles for landing, host controller, and TV screen.
   Dark (#111), high contrast, system fonts, readable from a couch at 4m. */

:root {
  --bg: #111;
  --panel: #1c1c22;
  --panel-2: #26262e;
  --fg: #f4f4f6;
  --muted: #9a9aa6;
  --accent: #ffcf3f;
  --good: #4ade80;
  --bad: #f87171;
  --team-1: #ffcf3f;
  --team-2: #4dd6ff;
  --team-3: #ff6ec7;
  --team-4: #7dff8a;

  /* Height reserved for the bottom action bar (guess button / controls),
     so the Mapillary arrow cluster and its sibling controls can be lifted
     clear of it instead of sitting underneath (issue #3). */
  --action-bar-h: 80px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--bg);
  color: var(--fg);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

button {
  font: inherit;
  border: none;
  border-radius: 12px;
  cursor: pointer;
  background: var(--panel-2);
  color: var(--fg);
  padding: 0.7em 1.2em;
}
button:disabled { opacity: 0.4; cursor: default; }

.btn-primary {
  background: var(--accent);
  color: #111;
  font-weight: 700;
}

.hidden { display: none !important; }

/* Phase sections: exactly one visible at a time. */
.phase-screen {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}

/* ---------- Connection pill & toast ---------- */

.conn-pill {
  position: fixed;
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--bad);
  color: #111;
  font-weight: 700;
  font-size: 14px;
  padding: 4px 14px;
  border-radius: 999px;
  z-index: 1000;
}

.toast {
  position: fixed;
  bottom: 110px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--fg);
  color: #111;
  font-weight: 700;
  padding: 10px 20px;
  border-radius: 12px;
  z-index: 1000;
  opacity: 0;
  transition: opacity 0.3s;
  pointer-events: none;
  /* Review §2.12 / §4.1: toasts wrap instead of clipping. `nowrap` used to
     truncate long status lines on a 360 px phone — and mechanic rules used
     to ride in them, which they no longer do (they live in sheets now). */
  white-space: normal;
  max-width: min(92vw, 420px);
  text-align: center;
  line-height: 1.35;
}
.toast.show { opacity: 1; }

/* ---------- Landing page (M4): hero over a drifting pano ---------- */

.hero-pano {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.4s ease-out;
}
.hero-pano.live { opacity: 1; }
.hero-scrim {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    rgba(17, 17, 17, 0.5),
    rgba(17, 17, 17, 0.78) 60%,
    var(--bg)
  );
}

.landing {
  position: relative;
  z-index: 2;
  max-width: 640px;
  margin: 0 auto;
  padding: 0 24px 24px;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}
.landing h1 {
  font-size: clamp(3rem, 10vw, 5rem);
  margin: 0 0 0.15em;
  letter-spacing: -0.02em;
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.7);
}
.landing h1 .geo { color: var(--accent); }

.ld-hero {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 76svh;
}
.ld-tagline {
  color: var(--fg);
  font-size: clamp(1.15rem, 4vw, 1.4rem);
  font-weight: 600;
  line-height: 1.45;
  margin: 0 0 28px;
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.8);
}

.ld-panel { display: flex; flex-direction: column; gap: 12px; max-width: 420px; }
.ld-cta-main { padding: 18px 0; font-size: 1.3rem; border-radius: 14px; }
.ld-cta-sub {
  background: rgba(28, 28, 34, 0.75);
  border: 1px solid var(--panel-2);
  padding: 14px 0;
  font-size: 1.05rem;
  font-weight: 600;
}
.ld-back {
  background: none;
  color: var(--muted);
  padding: 8px 0;
  align-self: flex-start;
}

/* The chooser: two experiences, never pages. First card is the default. */
.ld-choice {
  display: block;
  text-decoration: none;
  background: rgba(28, 28, 34, 0.88);
  color: var(--fg);
  border: 1px solid var(--panel-2);
  border-radius: 16px;
  padding: 16px 18px;
}
.ld-choice:first-child { border-color: var(--accent); }
.ld-choice .art { display: block; font-size: 1.6rem; margin-bottom: 4px; }
.ld-choice strong { display: block; font-size: 1.2rem; margin-bottom: 2px; color: var(--accent); }
.ld-choice span:last-child { color: var(--muted); font-size: 0.95rem; }

.ld-join-row .code-input { background: rgba(28, 28, 34, 0.88); }
.ld-join-note { color: var(--muted); font-size: 0.95rem; min-height: 1.2em; }

/* S2: the Daily Challenge affordance — always visible under the panels. */
.ld-daily {
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 420px;
  margin-top: 14px;
  padding: 13px 16px;
  background: rgba(28, 28, 34, 0.75);
  border: 1px solid var(--panel-2);
  border-radius: 14px;
  color: var(--muted);
  font-size: 0.98rem;
  line-height: 1.35;
  text-decoration: none;
}
.ld-daily:hover { border-color: var(--accent); }
.ld-daily .art { font-size: 1.5rem; }
.ld-daily strong { color: var(--accent); }

/* Three steps, pictures over paragraphs. */
.ld-steps {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 28px 0 20px;
}
.ld-step {
  flex: 0 1 150px;
  text-align: center;
  color: var(--fg);
  font-weight: 600;
  font-size: 0.95rem;
  line-height: 1.3;
}
.ld-step .art { font-size: 2.2rem; margin-bottom: 6px; }
.ld-step-arrow { color: var(--muted); font-size: 1.4rem; }
@media (max-width: 480px) {
  .ld-step-arrow { display: none; }
  .ld-steps { gap: 10px; }
  .ld-step { flex: 1 1 30%; }
}

.ld-footer {
  text-align: center;
  color: var(--muted);
  padding: 8px 0 16px;
  display: flex;
  justify-content: center;
  gap: 10px;
}
.ld-footer a { color: var(--muted); }
.ld-footer a:hover { color: var(--fg); }

/* ---------- Host controller ---------- */

.host-wrap {
  padding: 16px;
  padding-bottom: 96px; /* keep content clear of the bottom action bar */
  max-width: 560px;
  margin: 0 auto;
  width: 100%;
}

.host-wrap h1 { font-size: 1.6rem; margin: 4px 0 12px; }
.host-wrap h2 { font-size: 1.1rem; color: var(--muted); margin: 20px 0 8px; }

.settings-group { margin-bottom: 18px; }
.settings-group .label {
  font-size: 0.85rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 8px;
}

.seg {
  display: flex;
  gap: 6px;
}
.seg button {
  flex: 1;
  padding: 12px 0;
  background: var(--panel);
  font-weight: 600;
}
.seg button.sel {
  background: var(--accent);
  color: #111;
}

.team-names input {
  width: 100%;
  margin-bottom: 8px;
  background: var(--panel);
  border: 1px solid var(--panel-2);
  border-radius: 10px;
  color: var(--fg);
  padding: 12px;
  font: inherit;
}

/* Primary action pinned bottom-center for thumb reach (spec §6). */
.action-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
  display: flex;
  gap: 10px;
  justify-content: center;
  background: linear-gradient(transparent, rgba(17, 17, 17, 0.92) 40%);
  z-index: 500;
}
.action-bar button {
  flex: 1;
  max-width: 420px;
  padding: 16px 0;
  font-size: 1.15rem;
}

.resume-banner {
  background: var(--panel);
  border: 1px solid var(--accent);
  border-radius: 14px;
  padding: 14px;
  margin-bottom: 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.leaderboard-list { list-style: none; margin: 0; padding: 0; }
.leaderboard-list li {
  display: flex;
  justify-content: space-between;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--panel);
  margin-bottom: 4px;
  font-size: 0.95rem;
}
.leaderboard-list li .who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.leaderboard-list li .pts { color: var(--accent); font-weight: 700; margin-left: 12px; }
.leaderboard-list .empty { color: var(--muted); justify-content: center; }

/* Host lobby: giant room code + QR. */
.room-code-huge {
  font-size: clamp(3rem, 15vw, 8rem);
  font-weight: 800;
  letter-spacing: 0.12em;
  text-align: center;
  color: var(--accent);
  margin: 12px 0 4px;
}
.qr-box {
  display: flex;
  justify-content: center;
  margin: 8px 0 16px;
}
.qr-box canvas {
  background: #fff;
  padding: 12px;
  border-radius: 12px;
  image-rendering: pixelated;
  width: min(60vw, 260px);
  height: min(60vw, 260px);
}
.waiting-screen-note {
  text-align: center;
  color: var(--muted);
  margin-bottom: 8px;
}
.waiting-screen-note.ok { color: var(--good); font-weight: 700; }
/* S6: the phone-side auto-advance countdown line (steady digits, no
   layout jitter as the number ticks). Hidden entirely when blank. */
.advance-note { font-variant-numeric: tabular-nums; font-weight: 600; }
.advance-note:empty { display: none; }

/* Head-to-head lobby: invite link for remote rivals (no shared TV needed). */
.share-row {
  display: flex;
  justify-content: center;
  margin: 0 0 16px;
}

/* "Add a TV" affordance (both lobbies): a QR to scan-and-cast, one button
 * to send the link, one short typing fallback — never a raw URL for a human
 * to copy off the screen (tvlink.js owns that rule). Collapsed <details> on
 * the h2h phone, where the TV is a bonus; inline on the couch lobby, where
 * attaching a screen is the whole job. */
.tv-add {
  border: 1px solid var(--panel-2);
  background: var(--panel);
  border-radius: 14px;
  padding: 0 14px;
  margin: 0 0 16px;
}
.tv-add summary {
  cursor: pointer;
  padding: 12px 2px;
  color: var(--muted);
  font-weight: 600;
}
.tv-add[open] summary { color: var(--fg); }
.qr-small canvas { width: min(45vw, 180px); height: min(45vw, 180px); }
.tv-add-hint {
  text-align: center;
  color: var(--muted);
  margin: 0 0 12px;
}
.tv-add-alt {
  text-align: center;
  color: var(--muted);
  font-size: 0.85rem;
  margin: 4px 0 14px;
}

/* Phone-sized reveal map — h2h phones always, and the couch host phone
 * when it IS the screen (S7): the all-pins beat with no TV attached. */
.p-reveal-map {
  height: 36vh;
  min-height: 200px;
  border-radius: 16px;
  overflow: hidden;
  margin-bottom: 12px;
  background: #0b0b0f;
}

/* S7: the game-over crown on the host phone when no TV podium exists. */
.host-crown {
  text-align: center;
  font-size: 1.35rem;
  font-weight: 700;
  margin: 4px 0 14px;
  animation: rise-in 0.35s ease-out both;
}

/* Host round: full-bleed viewer + overlay HUD. */
.viewer-full {
  position: absolute;
  inset: 0;
}
.hud {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: space-between;
  padding: 12px 14px;
  pointer-events: none;
  z-index: 400;
  font-weight: 800;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.8);
}
.hud .hud-item {
  background: rgba(17, 17, 17, 0.65);
  border-radius: 10px;
  padding: 6px 12px;
  font-size: 1.05rem;
}

.map-full {
  position: absolute;
  inset: 0;
  background: #0b0b0f;
}
.guess-hint {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 600;
  background: rgba(17, 17, 17, 0.8);
  padding: 8px 16px;
  border-radius: 10px;
  font-weight: 600;
  pointer-events: none;
}

/* Host reveal / game over cards */
.stat-card {
  background: var(--panel);
  border-radius: 16px;
  padding: 18px;
  margin-bottom: 12px;
  text-align: center;
}
.stat-card .big {
  font-size: 2.4rem;
  font-weight: 800;
}
.stat-card .label { color: var(--muted); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.06em; }
.stat-card .big.place { font-size: 1.6rem; color: var(--accent); }
/* Speed-bonus line injected under the points card / tile at reveal. */
.time-note {
  margin-top: 6px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.time-note.zero { color: var(--muted); }
.time-note:empty { display: none; }
.reveal-num .time-note { margin-top: 0.6vh; font-size: 2.2vh; }

/* SUPER SURE (M6), de-cluttered (review §2.6 / §6.1). It used to be a
   full-width labelled pill on the floating context layer that restated the
   whole rule every round — and stayed on screen, disabled, once spent. Now:
   a 🔥 chip docked INTO the action bar, present only while the bet is
   unspent, whose only job is to open the one sheet that explains it.
   Absence communicates spent; the armed state shows on the primary button's
   own label. Still local-only — nothing about the bet reaches any rival
   surface before the reveal. */
.ss-chip {
  flex: 0 0 auto;
  width: 58px;
  max-width: 58px;
  min-height: 44px;
  padding: 0;
  font-size: 1.3rem;
  line-height: 1;
  background: rgba(17, 17, 17, 0.85);
  border: 1px solid var(--accent);
  color: var(--accent);
  border-radius: 14px;
}
.ss-chip.armed {
  background: var(--accent);
  color: #111;
  box-shadow: 0 0 18px rgba(255, 207, 63, 0.6);
}
/* Verdict line injected under points cards/tiles at reveal. */
.ss-note {
  margin-top: 6px;
  font-size: 0.85rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  color: var(--accent);
}
.ss-note.lost { color: var(--bad); }
.ss-note:empty { display: none; }
.reveal-num .ss-note { margin-top: 0.6vh; font-size: 2.2vh; }
/* Verdict tooltip on reveal-map halos (phone + TV). */
.ss-tooltip {
  background: var(--accent);
  color: #111;
  font-weight: 800;
  border: none;
}

/* ---------- First-time hints (M5): one-shot overlay cards ---------- */

/* Bottom sheet above the action bar; never covers the pano/map it teaches.
   Sits below the consent banner (3000) and above everything in-game. */
.hint-card {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: calc(96px + env(safe-area-inset-bottom));
  z-index: 2500;
  max-width: 440px;
  margin: 0 auto;
  background: var(--panel);
  border: 1px solid var(--accent);
  border-radius: 16px;
  padding: 14px 16px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
}
.hint-card p {
  margin: 0 0 8px;
  color: var(--fg);
  font-weight: 600;
  line-height: 1.4;
}
.hint-title {
  color: var(--accent);
  font-weight: 800;
  letter-spacing: 0.04em;
  margin-bottom: 6px;
}
.hint-dismiss { width: 100%; padding: 10px 0; }
/* Interstitial variant (showdown): centered card over a scrim. */
.hint-scrim {
  position: fixed;
  inset: 0;
  z-index: 2400;
  background: rgba(5, 5, 8, 0.7);
}
.hint-card.center {
  bottom: auto;
  top: 50%;
  transform: translateY(-50%);
  text-align: center;
}

/* ---------- The context-pill layer, now down to one (review §4.1) -------

   THREE things used to float here at z600 on the guess map: the "tap the
   map" banner, the M3 "if you locked in now" estimate, and the M6 SUPER
   SURE toggle. The estimate moved INTO the primary button
   (hints.lockButtonLabel) and the bet moved into the action-bar chip and
   its sheet, so the only occupant left is `.guess-hint` — and that one
   leaves as soon as a pin exists. The rule for this layer is now literal:
   at most ONE pill, and it is a slot, not a stack. */

/* The guess map's primary carries a live number, so its digits must not
   jitter as the speed bonus decays — and §6.1 calls that number a
   "sublabel", so the verb and the estimate stack. That matters at 360 px:
   the bar holds a 🔥 chip, a ghost secondary and this primary, and a
   two-line button only has to be as wide as its widest LINE.

   Budget at 360 px (308 px of bar after padding): chip 48 + gap 10 +
   ghost "Back to street" ≈ 119 + gap 10 + primary ≥ 102 = 289 ≤ 308. */
#btnLockIn, #btnConfirmGuess, #btnDLockIn {
  font-variant-numeric: tabular-nums;
  font-size: 1.1rem;
  padding: 9px 8px;
  line-height: 1.15;
}
.btn-main { display: block; }
.btn-sub {
  display: block;
  font-size: 0.8rem;
  font-weight: 700;
  opacity: 0.72;
  font-variant-numeric: tabular-nums;
}
.btn-sub:empty { display: none; }

/* The chip and the ghost yield space to the primary, never the reverse
   (§4.1: "primary always rightmost/widest"). */
#p-guess .ss-chip, #h-guess .ss-chip { width: 48px; max-width: 48px; }
#p-guess .btn-ghost, #d-guess .btn-ghost {
  flex: 0 1 auto;
  min-width: 0;
  font-size: 0.9rem;
  padding: 16px 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#p-guess .btn-primary, #d-guess .btn-primary, #h-guess .btn-primary {
  flex: 1 1 auto;
  flex-shrink: 0;
}
.totals-list { list-style: none; padding: 0; margin: 0; }
.totals-list li {
  display: flex;
  justify-content: space-between;
  background: var(--panel);
  border-radius: 10px;
  padding: 10px 14px;
  margin-bottom: 6px;
  font-weight: 600;
}
.totals-list li.active { outline: 2px solid var(--accent); }

/* ---------- TV screen ---------- */

.tv-center {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 4vh 4vw;
}

.tv-title {
  font-size: 9vh;
  font-weight: 800;
  letter-spacing: -0.02em;
  margin: 0;
}
.tv-title .geo { color: var(--accent); }
.tv-sub { font-size: 3.5vh; color: var(--muted); margin: 2vh 0 0; }

.room-entry input {
  margin-top: 4vh;
  font-size: 8vh;
  width: 7.5em;
  text-align: center;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  background: var(--panel);
  color: var(--fg);
  border: 2px solid var(--panel-2);
  border-radius: 16px;
  padding: 0.2em 0 0.2em 0.3em;
  font-family: inherit;
  font-weight: 800;
}
.room-entry input:focus { outline: 3px solid var(--accent); }
.room-entry .err { color: var(--bad); font-size: 2.5vh; margin-top: 2vh; min-height: 3vh; }

.tv-teams {
  display: flex;
  gap: 3vw;
  margin-top: 5vh;
  flex-wrap: wrap;
  justify-content: center;
}
.tv-teams .team-chip {
  font-size: 4.5vh;
  font-weight: 800;
  padding: 1vh 3vw;
  border-radius: 2vh;
  background: var(--panel);
}

/* Screen round: imagery is the show; HUD only in corners. */
.tv-hud-corner {
  position: absolute;
  z-index: 400;
  font-weight: 800;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
  pointer-events: none;
}
.tv-countdown {
  top: 2vh;
  right: 3vw;
  font-size: 12vh;
  font-variant-numeric: tabular-nums;
}
.tv-countdown.low { color: var(--bad); }
.tv-round-no {
  top: 2vh;
  left: 3vw;
  font-size: 5vh;
  color: var(--muted);
}
.tv-active-team {
  bottom: 2vh;
  left: 3vw;
  font-size: 5vh;
}
/* Final Showdown banner on the round screen: same pulse as "Guessing…". */
.tv-active-team.showdown {
  animation: guess-pulse 1.6s ease-in-out infinite;
}

/* Whose-turn label on the TV guess map (bottom-left corner, injected). */
.tv-guess-team {
  bottom: 2vh;
  left: 3vw;
  font-size: 5vh;
}

/* Screen guessing: full-bleed world map, live preview of the host's pin.
   Labels sit above Leaflet's panes (tiles 200 … tooltips 700). */
.guess-live-map {
  position: absolute;
  inset: 0;
}
#s-guess .tv-hud-corner { z-index: 800; }
.tv-guess-label {
  top: 2vh;
  right: 3vw;
  font-size: 6vh;
  color: var(--accent);
  animation: guess-pulse 1.6s ease-in-out infinite;
}
@keyframes guess-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}
.tv-guess-hint {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 4vh;
  z-index: 800;
  text-align: center;
  font-size: 3.5vh;
  color: var(--muted);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
  pointer-events: none;
}

/* Live pin on the TV guess map. The wrap is a zero-size divIcon anchored at
   the guess point; Leaflet positions it, CSS animates the children. The pin
   drops in with a bounce when first placed; the ring pulses when it moves. */
.tv-live-pin-wrap { pointer-events: none; }
/* Zero-size inner wrap so JS can set --pin-color (the active team's color)
   on one element; children keep their pin-point anchoring. */
.pin-parts {
  position: absolute;
  left: 0;
  top: 0;
}
.tv-live-pin {
  position: absolute;
  left: -14px;
  top: -14px;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--pin-color, #ffcf3f);
  border: 3px solid #fff;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.55);
  animation: pin-drop 0.6s ease-out both;
}
@keyframes pin-drop {
  0%   { transform: translateY(-110px) scale(1.3); opacity: 0; }
  45%  { transform: translateY(0) scale(0.92, 1.08); opacity: 1; }
  55%  { transform: translateY(2px) scale(1.15, 0.85); } /* squash on impact */
  72%  { transform: translateY(-12px) scale(1); }
  86%  { transform: translateY(0) scale(1.05, 0.95); }
  94%  { transform: translateY(-4px) scale(1); }
  100% { transform: translateY(0) scale(1); }
}
.pin-ripple {
  position: absolute;
  left: -14px;
  top: -14px;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 3px solid var(--pin-color, #ffcf3f);
  opacity: 0;
}
.pin-ripple.go {
  animation: pin-ripple 0.75s ease-out both;
}
@keyframes pin-ripple {
  0%   { transform: scale(0.5); opacity: 0.95; }
  100% { transform: scale(3.2); opacity: 0; }
}

/* Screen reveal: split view — map on top, numbers below. */
.reveal-split {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
}
.reveal-map { flex: 1 1 55%; min-height: 0; }
/* Place name banner — the shout-it-out moment. Hidden until the truth
   marker lands, then pops in. */
.reveal-place {
  flex: 0 0 auto;
  text-align: center;
  font-size: 7.5vh;
  font-weight: 800;
  line-height: 1.1;
  padding: 1.2vh 3vw 0;
  color: var(--accent);
  opacity: 0;
  transform: translateY(2vh) scale(0.92);
  transition: opacity 0.45s ease-out, transform 0.45s cubic-bezier(0.2, 1.4, 0.4, 1);
}
.reveal-place.show { opacity: 1; transform: none; }
/* S6: the TV's between-round soft auto-advance countdown line. */
.tv-advance-note {
  flex: 0 0 auto;
  text-align: center;
  padding: 0.6vh 0 1.2vh;
  font-size: 2.6vh;
  font-weight: 700;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.tv-advance-note:empty { display: none; }
.reveal-stats {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-evenly;
  gap: 2vw;
  padding: 2vh 3vw;
}
.reveal-num {
  text-align: center;
}
.reveal-num .val {
  font-size: 11vh;
  font-weight: 800;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
}
.reveal-num .val.points { color: var(--accent); }
.reveal-num .label { font-size: 2.6vh; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }

.reveal-board {
  display: flex;
  flex-direction: column;
  gap: 1vh;
  min-width: 26vw;
}
.reveal-board .row {
  display: flex;
  justify-content: space-between;
  gap: 2vw;
  font-size: 3.6vh;
  font-weight: 700;
  background: var(--panel);
  border-radius: 1.2vh;
  padding: 0.8vh 1.5vw;
}
.reveal-board .row .pts { font-variant-numeric: tabular-nums; }

/* Showdown reveal: two boards side by side (this round + totals), each with
   a small caption pulled from data-caption. The closest team gets crowned. */
.tv-showdown::before,
.reveal-board.captioned::before {
  content: attr(data-caption);
  font-size: 2.2vh;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  text-align: center;
}
.tv-showdown .row.closest {
  outline: 0.4vh solid var(--accent);
}

/* Host showdown reveal: closest-first results list. */
.showdown-results li.closest { outline: 2px solid var(--accent); }

/* Podium */
.podium {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 3vw;
  margin-top: 6vh;
}
.podium .slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.5vh;
}
.podium .slot .name { font-size: 4.5vh; font-weight: 800; }
.podium .slot .score { font-size: 3vh; color: var(--muted); font-variant-numeric: tabular-nums; }
.podium .slot .bar {
  width: 18vw;
  border-radius: 2vh 2vh 0 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 1.5vh;
  font-size: 5vh;
  font-weight: 800;
  color: #111;
}
/* §2.11: a TV is not a touch surface. The game-over "New game" control was
   a full accent button plus an explanatory line; the real path is "the
   screen follows the host", so both collapse into one quiet line with the
   code-entry escape hatch demoted to a text button inside it. */
.tv-newgame {
  margin-top: 4vh;
  font-size: 1.9vh;
  padding: 0.6vh 1.2vw;
  border-radius: 1vh;
  background: none;
  border: 0;
  color: var(--muted);
  font-weight: 600;
  text-decoration: underline;
}
.tv-newgame:hover, .tv-newgame:focus-visible { color: var(--fg); }
.tv-newgame-line {
  margin: 4vh 0 0;
  font-size: 1.9vh;
  color: var(--muted);
}
.tv-newgame-line .tv-newgame { margin: 0 0 0 0.6vw; }

.podium .slot:nth-child(1) .bar { height: 28vh; background: var(--accent); }
.podium .slot:nth-child(2) .bar { height: 19vh; background: var(--team-2); }
.podium .slot:nth-child(3) .bar { height: 13vh; background: var(--team-3); }
.podium .slot:nth-child(4) .bar { height: 9vh; background: var(--team-4); }

/* Cheap CSS-only confetti (spec §6): strips falling on loop. #8: js/fx.js seeds
   per-strip fall duration/delay and two custom properties — --drift (a signed
   horizontal sway) and --spin (a randomized total rotation) — set inline by
   fx-ui.js#spawnConfetti, so the loop varies instead of marching in lockstep.
   A gentle ease keeps the sway from reading mechanical. */
.confetti { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.confetti i {
  position: absolute;
  top: -4vh;
  width: 1.4vh;
  height: 2.8vh;
  opacity: 0.9;
  animation: confetti-fall cubic-bezier(0.37, 0.02, 0.63, 1) infinite;
}
@keyframes confetti-fall {
  0% { transform: translate(0, -6vh) rotate(0deg); opacity: 0; }
  8% { opacity: 0.9; }
  100% {
    transform: translate(var(--drift, 0), 112vh) rotate(var(--spin, 720deg));
    opacity: 0.9;
  }
}

/* Reveal line-draw + count-up hooks (used from JS) */
.count-up { transition: none; }

/* Leaflet dark-friendly tweaks */
.leaflet-container { background: #0b0b0f; }

/* Mapillary viewer fills its container */
.mapillary-viewer, .viewer-full > div { width: 100%; height: 100%; }

/* ================================================================
   Issue #3 — arrow cluster reposition. Lifts the direction (arrow)
   component clear of the bottom action bar and enlarges it; nudges the
   zoom/bearing/sequence controls to stay clear of it in turn. Verified
   against the loaded MapillaryJS 4.1.2 build's actual class names
   (mapillary-bearing's outer element is "-indicator-container", not
   "-container" — the other three matched the inferred names). Standalone
   translate/scale only, no !important, no viewer/canvas transform.
   ================================================================ */
.viewer-full .mapillary-direction-perspective {
  translate: 0 calc(-1 * (var(--action-bar-h) + env(safe-area-inset-bottom) + 8px));
  scale: 1.25;
}
.viewer-full .mapillary-sequence-container { top: 64px; }
.viewer-full .mapillary-bearing-indicator-container {
  bottom: calc(var(--action-bar-h) + env(safe-area-inset-bottom) + 16px);
}
.viewer-full .mapillary-zoom-container {
  bottom: calc(var(--action-bar-h) + env(safe-area-inset-bottom) + 72px);
}

/* ================================================================
   Head-to-head mode
   ================================================================ */

.h2h-tag {
  font-size: 0.55em;
  color: var(--team-2);
  font-weight: 800;
  letter-spacing: 0.04em;
}

/* ---------- Player phone: home / join ---------- */

.join-row { display: flex; gap: 8px; }
.join-row .code-input {
  width: 7em;
  background: var(--panel);
  border: 1px solid var(--panel-2);
  border-radius: 10px;
  color: var(--fg);
  padding: 12px;
  font: inherit;
  font-weight: 800;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  text-align: center;
}
.join-row button { flex: 1; }
.join-err { color: var(--bad); font-size: 0.9rem; margin-top: 8px; min-height: 1.2em; }

/* ---------- Player phone: locked-in screen ---------- */

.locked-wrap { text-align: center; padding-top: 10vh; }
.locked-stamp {
  font-size: clamp(2.6rem, 13vw, 4rem);
  font-weight: 800;
  letter-spacing: 0.06em;
  color: var(--accent);
  transform: rotate(-4deg);
  animation: stamp-in 0.45s cubic-bezier(0.2, 1.6, 0.4, 1) both;
}
@keyframes stamp-in {
  0% { transform: rotate(-4deg) scale(2.2); opacity: 0; }
  100% { transform: rotate(-4deg) scale(1); opacity: 1; }
}
.locked-rank { font-size: 1.3rem; font-weight: 700; color: var(--good); margin-top: 6px; }
.locked-sub { color: var(--muted); font-size: 1.05rem; }
.locked-wrap h2, .locked-wrap .totals-list { text-align: left; }

.handoff-note {
  margin-top: 18px;
  background: var(--panel);
  border-radius: 14px;
  padding: 14px;
  color: var(--muted);
  line-height: 1.45;
}

/* ---------- TV: head-to-head lobby ---------- */

.h2h-lobby-code {
  font-size: 16vh;
  font-weight: 800;
  letter-spacing: 0.12em;
  color: var(--accent);
  margin: 2vh 0 0;
}

/* ---------- TV: the split-panel race (1 -> 4 teams) ----------
   1 full-screen · 2 side-by-side · 3 two-up-top + one centered below
   (equal size, nobody privileged, nothing stretched) · 4 quad. */

.h2h-grid {
  position: absolute;
  inset: 0;
  display: grid;
  gap: 0.6vh;
  padding: 0.6vh;
  background: #000;
}
.h2h-grid[data-n="1"] { grid-template-columns: 1fr; grid-template-rows: 1fr; }
.h2h-grid[data-n="2"] { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr; }
.h2h-grid[data-n="3"],
.h2h-grid[data-n="4"] { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.h2h-grid[data-n="3"] .h2h-panel:nth-child(3) {
  grid-column: 1 / 3;
  width: 50%;
  justify-self: center;
}

.h2h-panel {
  position: relative;
  overflow: hidden;
  border: 0.5vh solid var(--team-color, var(--accent));
  border-radius: 1.2vh;
  background: #0b0b0f;
}
.h2h-viewer, .h2h-map { position: absolute; inset: 0; }

.h2h-label {
  position: absolute;
  left: 1vw;
  bottom: 1.2vh;
  z-index: 850;
  display: flex;
  align-items: center;
  gap: 0.8vw;
  background: rgba(17, 17, 17, 0.78);
  border-radius: 1vh;
  padding: 0.7vh 1.2vw;
  pointer-events: none;
}
.h2h-label .dot {
  width: 1.8vh;
  height: 1.8vh;
  border-radius: 50%;
  background: var(--team-color, var(--accent));
}
.h2h-label .name { font-size: 3vh; font-weight: 800; }
.h2h-label .status { font-size: 2.2vh; color: var(--muted); }

/* Locked panel: freeze the feed under a stamped overlay — the final pin
   stays off the TV until the reveal. */
.h2h-lock {
  position: absolute;
  inset: 0;
  z-index: 860;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1vh;
  background: rgba(10, 10, 14, 0.72);
  pointer-events: none;
}
.h2h-lock .stamp {
  font-size: 6.5vh;
  font-weight: 800;
  letter-spacing: 0.08em;
  color: var(--team-color, var(--accent));
  border: 0.6vh solid var(--team-color, var(--accent));
  border-radius: 1.5vh;
  padding: 0.6vh 2vw;
  transform: rotate(-5deg);
  animation: stamp-in 0.45s cubic-bezier(0.2, 1.6, 0.4, 1) both;
}
.h2h-lock .rank { font-size: 3vh; font-weight: 700; color: var(--fg); }
.h2h-panel.locked { border-color: rgba(255, 255, 255, 0.25); }

/* HUD over the grid */
.h2h-hud { z-index: 900; }
.h2h-countdown {
  top: 1.4vh;
  right: 2vw;
  z-index: 900;
  font-size: 8vh;
  font-variant-numeric: tabular-nums;
  position: absolute;
  font-weight: 800;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
  pointer-events: none;
}
.h2h-countdown.low { color: var(--bad); }
.h2h-progress {
  position: absolute;
  top: 1.8vh;
  left: 50%;
  transform: translateX(-50%);
  z-index: 900;
  font-size: 3.4vh;
  font-weight: 800;
  color: var(--accent);
  background: rgba(17, 17, 17, 0.78);
  border-radius: 1vh;
  padding: 0.6vh 1.6vw;
  pointer-events: none;
}
.h2h-progress:empty { display: none; }

/* ---------- TV: the reveal countdown beat ---------- */

.h2h-count-overlay {
  position: absolute;
  inset: 0;
  z-index: 950;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2vh;
  background: rgba(5, 5, 8, 0.88);
}
.h2h-count-title {
  font-size: 5.5vh;
  font-weight: 800;
  letter-spacing: 0.1em;
  color: var(--fg);
  animation: guess-pulse 1s ease-in-out infinite;
}
.h2h-count-num {
  font-size: 34vh;
  font-weight: 800;
  line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  animation: count-beat 1s ease-out infinite;
}
@keyframes count-beat {
  0% { transform: scale(1.18); }
  35% { transform: scale(1); }
  100% { transform: scale(1); }
}

/* ---------- TV: game-over handoff note ---------- */

.tv-next-host {
  margin-top: 4vh;
  font-size: 3.6vh;
  font-weight: 800;
}

/* ---------- Analytics consent (GDPR opt-in — see PRIVACY.md) ---------- */

.consent-banner {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom));
  z-index: 3000;
  max-width: 560px;
  margin: 0 auto;
  background: var(--panel);
  border: 1px solid var(--panel-2);
  border-radius: 16px;
  padding: 16px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
}
.consent-banner p {
  margin: 0 0 12px;
  color: var(--muted);
  font-size: 0.95rem;
  line-height: 1.45;
}
.consent-banner .consent-status { color: var(--fg); font-weight: 700; }
.consent-actions { display: flex; gap: 10px; }
.consent-actions button { flex: 1; padding: 12px 0; font-size: 1rem; }

/* Quiet corner control to reopen the banner once a choice has been made. */
.consent-settings {
  position: fixed;
  left: 10px;
  bottom: calc(10px + env(safe-area-inset-bottom));
  z-index: 2900;
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: 50%;
  background: rgba(28, 28, 34, 0.7);
  font-size: 16px;
  line-height: 1;
  opacity: 0.75;
}
.consent-settings:hover, .consent-settings:focus-visible { opacity: 1; }
/* Keep the control clear of the bottom action bar on the phone pages. */
body:has(.action-bar) .consent-settings {
  bottom: calc(84px + env(safe-area-inset-bottom));
}

/* ---------- Daily Challenge (S2) ---------- */

.daily-date { color: var(--muted); font-size: 0.95rem; margin-top: 4px; }

/* ---------- G1/G4/G5/G6/G8 daily expansion ---------- */

/* G1/G8: muted streak + personal-best lines on the calm intro/done screens. */
.daily-records {
  color: var(--muted);
  font-size: 0.95rem;
  margin-top: 8px;
  min-height: 1em;
}
.daily-pb {
  color: var(--accent);
  font-weight: 700;
  margin-top: 6px;
}

/* G5: the challenge eyebrow above the daily number. */
.challenge-eyebrow {
  color: var(--accent);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: 0.85rem;
  margin-bottom: 6px;
}

/* G6: the hard-mode entry row — a quiet, earned invitation. */
.hard-entry {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
  margin: 14px 0;
  padding: 12px;
  border: 1px solid var(--border, #333);
  border-radius: 12px;
  color: var(--muted);
}
.hard-entry .btn-ghost { min-height: 44px; }

/* G5: the per-round duel verdict line + the done-screen verdict block. */
.reveal-duel {
  color: var(--fg);
  font-size: 0.95rem;
  margin-top: 6px;
}
.done-duel {
  margin: 14px 0;
  padding: 12px;
  border-radius: 12px;
  background: rgba(201, 162, 255, 0.08);
}
.done-duel-head { font-size: 1.3rem; font-weight: 800; }
.done-duel-margin { color: var(--muted); margin-top: 4px; }
.done-duel-strip {
  margin-top: 10px;
  font-size: 1.4rem;
  letter-spacing: 4px;
  line-height: 1.5;
}

/* G2 Twists: the ritual-interstitial card flip (center overlay + scrim). */
.twist-card-overlay {
  position: fixed;
  inset: 0;
  z-index: 2500;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: rgba(0, 0, 0, 0.72);
  animation: twist-card-in 0.4s cubic-bezier(0.2, 1.4, 0.4, 1) both;
}
.twist-card-title {
  font-size: clamp(2rem, 9vw, 4rem);
  font-weight: 900;
  letter-spacing: 0.02em;
  color: #fff;
  text-align: center;
}
.twist-card-rule {
  font-size: clamp(1rem, 4vw, 1.4rem);
  color: var(--muted);
  text-align: center;
  max-width: 80%;
}
@keyframes twist-card-in {
  from { transform: rotateX(90deg) scale(0.9); opacity: 0; }
  to { transform: rotateX(0) scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .twist-card-overlay { animation: none; }   /* fade only — no flip */
}

/* G3 Crown Night: the muted tally line + the full-screen champion. */
.night-tally {
  color: var(--muted);
  font-size: 0.95rem;
  margin-top: 8px;
}
.night-champion {
  font-size: clamp(1.4rem, 4vw, 2.6rem);
  font-weight: 900;
  color: #ffcf3f;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
  margin: 10px 0;
}
.btn-subline {
  display: block;
  font-size: 0.8rem;
  font-weight: 500;
  opacity: 0.85;
  margin-top: 2px;
}

/* "Your Color Takes the Room" win celebration (party h2h + Daily): a radial
   bloom and confetti behind the game-over content, tinted by --win (the
   winner's own team color, or gold for a Crown Night champion) — set by
   player-ui.js/daily-ui.js from fx.js#celebrationSpec. */
#p-gameover, #d-done {
  position: relative;
  overflow: hidden;
}
#p-gameover .host-wrap, #d-done .host-wrap {
  position: relative;
  z-index: 1;
}
/* Confetti is a celebratory burst that should fall OVER the screen content
   (leaderboard, score, buttons), not behind it. z-index 2 > host-wrap's 1. */
#p-gameover .confetti, #d-done .confetti {
  z-index: 2;
}
.win-bloom {
  position: absolute;
  inset: 0;
  z-index: 0;   /* the color glow stays BEHIND the content */
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(
    circle at 50% 30%, var(--win, var(--accent)) 0%, transparent 62%);
}
.is-win .win-bloom { animation: win-bloom-in 0.55s ease-out both; }
@keyframes win-bloom-in {
  from { opacity: 0; transform: scale(0.9); }
  to { opacity: 0.22; transform: scale(1); }
}
.is-champion::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  box-shadow: inset 0 0 12vh 2vh rgba(255, 215, 0, 0.35);
}
.win-headline {
  color: var(--fg);
  text-shadow: 0 0 18px var(--win, var(--accent));
}
.is-win .win-headline {
  animation: win-punch 0.5s cubic-bezier(0.2, 1.5, 0.35, 1) both;
}
@keyframes win-punch {
  0% { transform: scale(0.82); opacity: 0; }
  60% { transform: scale(1.06); }
  100% { transform: scale(1); opacity: 1; }
}
.win-stat {
  color: var(--muted);
  font-size: 0.95rem;
  margin: 2px 0 10px;
}

/* G7: the decoy marker (planter's own map + reveal exposure). */
.decoy-marker {
  font-size: 1.4rem;
  line-height: 1;
  text-shadow: 0 1px 3px #000;
  filter: saturate(0.7);
}
.decoy-marker.reveal { opacity: 0.85; }

/* G5: the ghost pin's distance chip on the reveal map (map is replay-blocked). */
.ghost-chip {
  color: #e9dcff;
  font-weight: 700;
  font-size: 0.8rem;
  white-space: nowrap;
  text-shadow: 0 1px 3px #000, 0 0 2px #000;
}

.daily-rules {
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.5;
  margin: 14px 0;
}
.daily-rules strong { color: var(--fg); }
.daily-emoji {
  font-size: 1.9rem;
  letter-spacing: 5px;
  margin-top: 8px;
}
.daily-back-link {
  align-self: center;
  color: var(--muted);
  text-decoration: none;
  padding: 12px 10px;
}
.daily-back-link:hover { color: var(--fg); }

/* ---------- S4: sound + motion pass (design review §2.4) ---------- */

/* Phones no longer just toggle `hidden`: every phase screen rises in.
   Quick and composited (opacity + transform only); the class only changes
   on a real phase change, so steady-state re-renders never replay it. */
.phase-screen { animation: screen-in 0.28s ease-out; }
@keyframes screen-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}
/* Viewer-bearing screens: an ancestor transform over the Mapillary WebGL
   canvas — even a transient animated one — changes compositing / creates a
   containing block and can desync the viewer. These screens keep the S4
   entrance as a pure fade (same duration/easing), never a translate. */
#p-round, #h-round, #d-round, #s-round, #s-h2h-live {
  animation-name: screen-in-fade;
}
@keyframes screen-in-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Countdown urgency: the phone HUD timers pulse (and go red) over the
   final seconds — same count-beat the TV's 3-2-1 uses. JS adds .low. */
.hud-item.low {
  color: var(--bad);
  animation: count-beat 1s ease-out infinite;
}
/* The TV clocks already turn red on .low; give them the pulse too. */
.tv-countdown.low,
.h2h-countdown.low { animation: count-beat 1s ease-out infinite; }

/* Phone-side reveal: the map, the place headline, the result line and the
   board rows land in a quick cascade — the phone's version of the TV's
   reveal drama. Review §4.5 caps the TOTAL stagger at 0.35 s; with the
   three stat cards and second list gone there are fewer things to stagger,
   so the last row now starts at 0.24 s (was 0.34 s) and the whole cascade
   is over sooner than it used to begin its tail. */
#p-reveal .p-reveal-map, #h-reveal .p-reveal-map, #d-reveal .p-reveal-map,
#p-reveal .reveal-headline, #h-reveal .reveal-headline,
#d-reveal .reveal-headline,
#p-reveal .reveal-result, #h-reveal .reveal-result, #d-reveal .reveal-result,
#p-reveal .totals-list li, #h-reveal .totals-list li,
#d-reveal .total-row, #d-reveal .totals-list li,
#h-reveal .stat-card, #h-reveal .showdown-results li {
  animation: rise-in 0.3s ease-out both;
}
@keyframes rise-in {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: none; }
}
#p-reveal .reveal-headline, #h-reveal .reveal-headline,
#d-reveal .reveal-headline { animation-delay: 0.05s; }
#p-reveal .reveal-result, #h-reveal .reveal-result,
#d-reveal .reveal-result { animation-delay: 0.1s; }
#p-reveal .totals-list li:nth-child(1), #h-reveal .totals-list li:nth-child(1),
#d-reveal .total-row { animation-delay: 0.15s; }
#p-reveal .totals-list li:nth-child(2), #h-reveal .totals-list li:nth-child(2) { animation-delay: 0.18s; }
#p-reveal .totals-list li:nth-child(3), #h-reveal .totals-list li:nth-child(3) { animation-delay: 0.21s; }
#p-reveal .totals-list li:nth-child(4), #h-reveal .totals-list li:nth-child(4) { animation-delay: 0.24s; }

/* Transient lock-in stamp for surfaces with no locked screen of their own
   (couch host, daily): slams in, holds a beat, fades. Injected by fx-ui. */
.stamp-flash {
  position: fixed;
  top: 40%;
  left: 50%;
  z-index: 1500;
  transform: translate(-50%, -50%) rotate(-5deg);
  font-size: clamp(2.4rem, 12vw, 4rem);
  font-weight: 800;
  letter-spacing: 0.06em;
  color: var(--accent);
  border: 4px solid var(--accent);
  border-radius: 16px;
  padding: 6px 22px;
  background: rgba(17, 17, 17, 0.72);
  white-space: nowrap;
  pointer-events: none;
  animation: stamp-flash 0.9s cubic-bezier(0.2, 1.6, 0.4, 1) both;
}
@keyframes stamp-flash {
  0% { opacity: 0; transform: translate(-50%, -50%) rotate(-5deg) scale(2.1); }
  18% { opacity: 1; transform: translate(-50%, -50%) rotate(-5deg) scale(1); }
  70% { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -50%) rotate(-5deg) scale(1); }
}

/* The 🔊/🔇 toggle: quiet corner control, mirroring the consent 🍪 on the
   opposite side. Always present in-game — sound must be one tap away. */
.sound-toggle {
  position: fixed;
  right: 10px;
  bottom: calc(10px + env(safe-area-inset-bottom));
  z-index: 2900;
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: 50%;
  background: rgba(28, 28, 34, 0.7);
  font-size: 16px;
  line-height: 1;
  opacity: 0.5;
}
.sound-toggle:hover, .sound-toggle:focus-visible { opacity: 1; }
/* Keep it clear of the bottom action bar on the phone pages. */
body:has(.action-bar) .sound-toggle {
  bottom: calc(84px + env(safe-area-inset-bottom));
}

/* ---------- §4.1 utility corners: calm states only ----------

   The 🍪 and 🔊 controls are 34 px of visual weight that used to sit over
   the panorama and the guess map through entire rounds. chrome-ui.js stamps
   body[data-play="1"] while a play screen is up (chrome.js owns the
   predicate, under test); here they simply leave. Nothing about the consent
   GATE changes — the control still reopens the banner in every calm state,
   and revoking is never more than one tap away from a moment that matters.

   Review §4.3 also wants a 44×44 minimum target without visually enlarging
   the button: a transparent pseudo-element does exactly that (34 + 2×5 =
   44), so the touch box grows and the ink does not. */
body[data-play="1"] .consent-settings,
body[data-play="1"] .sound-toggle { display: none !important; }

/* Overnight bundle #6 safety net: the undecided consent banner is retracted by
   consent.js when play begins, but pin the rule in CSS too — no GDPR dialog may
   float over a panorama or guess map at z3000 even if that JS ever fails to
   run. It reappears on the next calm screen (data-play="0"), exactly as the
   consent.js reoffer does. */
body[data-play="1"] .consent-banner { display: none !important; }

/* The joiner's home panel (§6.2) is the one phone screen with no action
   bar, so the corners sit low rather than dodging a bar that isn't there. */
body[data-screen="p-home"] .consent-settings,
body[data-screen="p-home"] .sound-toggle {
  bottom: calc(10px + env(safe-area-inset-bottom));
}

.consent-settings, .sound-toggle { position: fixed; }
.consent-settings::before, .sound-toggle::before {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: 50%;
}

/* The sound toggle sits at 0.5 on the TV, where it is 4 m away and purely
   decorative; on a phone it is a control and needs to be legible. */
.sound-toggle { opacity: 0.75; }

/* Reduced motion: collapse every CSS animation/transition to a single
   instant frame (states still land — nothing depends on motion), and drop
   the looping confetti outright. JS-driven motion (reveal line draws, the
   score count-up, map glides) respects the same preference via
   fx.js motionDuration / fx-ui.js prefersReducedMotion. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .confetti { display: none; }
  /* The win celebration's audio (fanfare/championFanfare, buzz) still
     plays under reduced motion — only the confetti and these three visual
     flourishes are motion-gated; the states themselves still land. */
  .win-bloom { animation: none; opacity: 0.22; }
  .win-headline { animation: none; }
  .is-champion::after { animation: none; }
}

/* ---------- Field observability: report flow (plan §10) ---------- */

/* The toast is normally pointer-transparent; only the reactive variant that
   carries a Report action becomes interactive, and only while it is up. */
.toast.has-action {
  pointer-events: auto;
  white-space: normal;
  max-width: min(92vw, 420px);
  display: flex;
  align-items: center;
  gap: 10px;
  text-align: left;
}
.toast-action {
  flex: 0 0 auto;
  background: #111;
  color: var(--fg);
  border: 0;
  border-radius: 8px;
  padding: 6px 12px;
  font-weight: 700;
  font-size: 0.85rem;
  cursor: pointer;
}

/* Calm-state entry point: a quiet link on the diagnostics settings surface,
   never on the pano itself (docs/ui-ux-design-review.md). */
.consent-report {
  display: block;
  width: 100%;
  margin-top: 10px;
  padding: 6px 0;
  background: none;
  border: 0;
  color: var(--muted);
  font-size: 0.85rem;
  text-align: center;
  text-decoration: underline;
  cursor: pointer;
}
.consent-report:hover, .consent-report:focus-visible { color: var(--fg); }

.report-sheet {
  position: fixed;
  inset: 0;
  z-index: 3100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(0, 0, 0, 0.6);
}
.report-card {
  width: 100%;
  max-width: 420px;
  background: var(--panel);
  border: 1px solid var(--panel-2);
  border-radius: 16px;
  padding: 18px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
}
.report-title { margin: 0 0 8px; font-size: 1.05rem; }
.report-body {
  margin: 0 0 14px;
  color: var(--muted);
  font-size: 0.92rem;
  line-height: 1.5;
}
.report-actions { display: flex; gap: 10px; }
.report-actions button { flex: 1; padding: 12px 0; font-size: 1rem; }


/* ================================================================
   The de-clutter pass (docs/ui-ux-design-review.md)
   ================================================================ */

/* ---------- §4.3 controls: ghost actions, 44 px targets ---------- */

/* Destructive/exit actions (Abandon, Leave, Back) are ghosts — never
   filled peers of the primary. §4.1 forbids two filled buttons in one bar
   and forbids three buttons outright. */
.btn-ghost {
  background: none;
  color: var(--muted);
  font-weight: 600;
  border: 1px solid var(--panel-2);
}
.btn-ghost:hover, .btn-ghost:focus-visible { color: var(--fg); }
/* In the action bar a ghost never claims the width of the primary. */
.action-bar .btn-ghost { flex: 0 1 34%; font-size: 1rem; }

/* A quiet in-content exit — the "third action" §4.1 sends to overflow, so
   the bar itself stays at one primary + one secondary. */
.exit-link {
  display: block;
  width: 100%;
  min-height: 44px;
  margin-top: 14px;
  background: none;
  border: 0;
  color: var(--muted);
  font-size: 0.95rem;
  text-decoration: underline;
}
.exit-link:hover, .exit-link:focus-visible { color: var(--fg); }

/* Minimum touch target, applied to the controls that were under 44 px. */
.seg button { min-height: 44px; }
.daily-back-link { min-height: 44px; display: flex; align-items: center; }
.tv-add summary, .more-options summary, .past-games summary { min-height: 44px; }
.toast-action { min-height: 44px; }

/* ---------- §4.3 progressive disclosure: "More options" ---------- */

/* Segmented controls are default-visible only where the choice defines the
   experience (Difficulty, Rounds, Teams). Convenience settings — seconds
   per round, movement — live behind one disclosure, and the all-time
   leaderboard behind another: nobody needs game history while configuring
   a party (§2.2). Same visual language as the proven `.tv-add` details. */
.more-options, .past-games {
  border: 1px solid var(--panel-2);
  background: var(--panel);
  border-radius: 14px;
  padding: 0 14px;
  margin: 0 0 18px;
}
.more-options summary, .past-games summary {
  cursor: pointer;
  padding: 12px 2px;
  color: var(--muted);
  font-weight: 600;
  display: flex;
  align-items: center;
}
.more-options[open] summary, .past-games[open] summary { color: var(--fg); }
.more-options .settings-group:last-child,
.past-games > :last-child { margin-bottom: 14px; }

/* ---------- §6.3 lobby: the room code gets an eyebrow, not an <h1> ------ */

.eyebrow {
  text-align: center;
  color: var(--muted);
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin: 8px 0 0;
}

/* ---------- §6.4 the phone reveal ----------

   The payoff is the map; everything else is commentary. Map grows from
   36 vh to 52 vh, the place name becomes the headline (the TV's
   `reveal-place` treatment, phone-sized), and the personal outcome
   collapses from three stat cards + two injected sub-lines to one line. */

#p-reveal .p-reveal-map,
#h-reveal .p-reveal-map,
#d-reveal .p-reveal-map {
  height: 52vh;
  min-height: 240px;
  margin-bottom: 10px;
}

/* A small heading, not a title: "Round 3 of 5" is context, not the story. */
.reveal-round {
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 4px 0 10px;
  text-align: center;
}

.reveal-headline {
  text-align: center;
  font-size: 1.6rem;
  font-weight: 800;
  line-height: 1.2;
  color: var(--accent);
  margin: 0 0 6px;
}

.reveal-result {
  text-align: center;
  font-size: 1.05rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  margin: 0 0 16px;
  line-height: 1.4;
}
.reveal-result.lost { color: var(--muted); }

/* One board replaces "This round" + "Totals": the delta sits next to the
   running total, which is more legible than two lists the eye must join. */
.reveal-board-list li { font-variant-numeric: tabular-nums; }
.reveal-board-list li .delta { color: var(--accent); font-weight: 700; }
.reveal-board-list li .delta.zero { color: var(--muted); }

/* Daily's "Total so far" is a row, never a fourth card (§2.10). */
.total-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  background: var(--panel);
  border-radius: 10px;
  padding: 10px 14px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.total-row .label { margin-bottom: 0; }
.total-row .val { color: var(--accent); font-size: 1.2rem; }

/* ---------- §4.2 tabular numbers wherever a number ticks ---------- */

.room-code-huge, .locked-rank, .stat-card .big,
.totals-list li span:last-child, .leaderboard-list li .pts,
.hud-item, .h2h-lobby-code {
  font-variant-numeric: tabular-nums;
}

/* ---------- §4.4 sheets: the one-at-a-time teaching layer ---------- */

.hint-actions { display: flex; gap: 10px; }
.hint-actions .hint-dismiss { flex: 1; min-height: 44px; }
.hint-dismiss:not(.btn-primary) {
  background: none;
  border: 1px solid var(--panel-2);
  color: var(--muted);
  font-weight: 600;
}

/* ---------- §P2.1 pinch-zoom restored, gestures preserved ----------

   `user-scalable=no` is gone from every page, so the browser's own zoom
   (and iOS/Android accessibility zoom) works everywhere again. The two
   surfaces that own their gestures declare it: the panorama and the guess
   map consume touch themselves, so the browser must not also pan/zoom the
   page under them. The reveal maps are non-interactive (dragging, zoom and
   touchZoom are all disabled in JS), so they explicitly ALLOW vertical
   scrolling and pinch — otherwise a finger landing on the map would trap
   the page's scroll. */
.viewer-full, .map-full, .guess-live-map, .h2h-grid { touch-action: none; }
.p-reveal-map, .reveal-map { touch-action: pan-y pinch-zoom; }


/* ================================================================
   Retryable imagery-degraded overlay (stabilization: review P1-3/P2-1)
   ================================================================ */

/* Overnight bundle #4: the round-transition cover. Sits INSIDE the pano
   container (below the HUD/action-bar siblings), opaque, so the previous
   round's street is neutralized while the next image loads (a moveTo can take
   up to 20s). viewer-ui.js lifts it only when the new image actually arrives;
   on a failed load it stays up and the .imagery-degraded overlay / map fallback
   takes over. Static — no motion to reduce. */
.pano-cover {
  position: absolute;
  inset: 0;
  z-index: 500;
  background: radial-gradient(circle at 50% 42%, #1b2647, #0b1020 72%);
}

/* Issue #3 follow-up: a non-blocking "Finding your way…" pill shown while a
   move-enabled round's street is interactive but Mapillary's spatial-edge
   arrows haven't rendered yet. pointer-events:none — it never blocks input.
   Fades the instant an arrow glyph appears, or after a bounded timeout. */
.pano-nav-hint {
  position: fixed;          /* mounted on <body>; fixed to viewport, immune to
                               the Mapillary viewer container's CSS */
  left: 50%;
  bottom: calc(var(--action-bar-h) + env(safe-area-inset-bottom) + 20px);
  transform: translateX(-50%);
  z-index: 600;
  display: inline-flex;   /* inline-flex shrink-wraps to its text; block-level
                             flex would stretch to full width */
  width: max-content;     /* never stretch to full width */
  height: max-content;    /* never stretch to full height */
  align-items: center;
  gap: 8px;
  max-width: 80%;
  padding: 7px 14px;
  border-radius: 999px;
  background: rgba(17, 17, 20, 0.82);
  border: 1px solid rgba(244, 244, 246, 0.14);
  color: var(--fg);
  font-size: 0.85rem;
  font-weight: 600;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.35s ease;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
}
.pano-nav-hint.show { opacity: 1; }
.pano-nav-hint-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  animation: nav-hint-pulse 1.4s ease-in-out infinite;
}
@keyframes nav-hint-pulse {
  0%, 100% { opacity: 0.35; transform: scale(0.85); }
  50% { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .pano-nav-hint-dot { animation: none; opacity: 1; }
}

/* Shown when a stub viewer (SDK blocked / no WebGL) or a transient timeout
   left the round with no playable panorama. It is deliberately NOT pool
   exhaustion: nothing is scored, saved or pushed — the player retries. */
.imagery-degraded {
  position: fixed;
  inset: 0;
  z-index: 3100;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding: 24px;
  text-align: center;
  background: rgba(0, 0, 0, 0.82);
}
.imagery-degraded p {
  margin: 0;
  max-width: 420px;
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.5;
}
.imagery-degraded button { min-width: 160px; padding: 12px 0; font-size: 1rem; }
