/* ============================================================
   BRAINBUZZ — game-show design system
   Fonts: Outfit (display) + Inter (body) — chosen for legibility at TV distance
   All colors flow through theme tokens so holiday themes and
   AI-generated themes restyle every surface at once.
   ============================================================ */

:root {
  --bg-a: #0a0f2e;
  --bg-b: #050818;
  --board-bg: #030616;
  --cell-a: #1c2fd6;
  --cell-b: #101fa8;
  --cell-border: #04061c;
  --value: #e8b34b;
  --ink: #f4f6ff;
  --accent: #e8b34b;
  --accent-ink: #1a1300;
  --clue-a: #1c2fd6;
  --clue-b: #0a1470;
  --gold: #ffd977;

  /* Legibility first. The old display face was ultra-condensed, which is the
     worst case for reading across a living room. Outfit has open counters and
     wide apertures, so money values and category names stay crisp on a TV;
     Inter is the most readable option for small UI text on a phone. */
  --font-display: 'Outfit', 'Trebuchet MS', 'Helvetica Neue', sans-serif;
  --font-body: 'Inter', 'Helvetica Neue', Arial, sans-serif;
  --radius: 14px;
  --shadow: 0 10px 40px rgba(0, 0, 0, 0.55);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* Bulletproof dark canvas: a plain background-color on <html> that NO
   `background` shorthand can reset. Guarantees the page is never white,
   even on TV browsers that choke on gradients / var() / color-mix(). */
/* Tells the engine this is a dark UI, so native scrollbars, form controls and
   the <dialog> backdrop render dark instead of light. */
html { background-color: #050818; color-scheme: dark; }
html, body { height: 100%; }

/* Links: the browser default (#00EE) is unreadable on this dark background.
   Literal gold first so old TV browsers without var() still get contrast. */
a {
  color: #ffd977;
  color: var(--gold);
  text-decoration: none;
}
a:hover { text-decoration: underline; }
/* Anchors styled as buttons: .btn/.btn-accent already win on color; just keep
   them from underlining and make them size like real buttons. */
a.btn, a.btn-accent { display: inline-flex; align-items: center; justify-content: center; }
a.btn:hover, a.btn-accent:hover { text-decoration: none; }

body {
  font-family: var(--font-body);
  color: #f4f6ff; /* plain fallback — readable on any browser */
  color: var(--ink);
  /* Use background-COLOR + background-IMAGE (never the `background` shorthand),
     so the solid dark base can't be wiped by a later declaration. */
  background-color: #050818;
  background-image: linear-gradient(160deg, #0a0f2e, #050818); /* literal gradient */
  background-attachment: fixed;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}
/* Themed gradient only where custom properties actually work */
@supports (background: linear-gradient(#000, var(--x, #000))) {
  body { background-image: linear-gradient(160deg, var(--bg-a), var(--bg-b)); }
}
/* Fancy mesh only where color-mix() actually works */
@supports (background: color-mix(in srgb, #000, #fff)) {
  body {
    background-image:
      radial-gradient(1200px 700px at 20% -10%, color-mix(in srgb, var(--cell-a) 24%, transparent), transparent 60%),
      radial-gradient(1000px 600px at 110% 110%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 55%),
      linear-gradient(160deg, var(--bg-a), var(--bg-b));
  }
}

/* subtle scanline/grain atmosphere */
body::before {
  content: '';
  position: fixed;
  top: 0; right: 0; bottom: 0; left: 0; /* old-engine fallback for inset */
  pointer-events: none;
  background-image: repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.015) 0 1px, transparent 1px 3px);
  z-index: 1;
}

h1, h2, h3, .display {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

button {
  font-family: var(--font-body);
  font-weight: 700;
  border: none;
  border-radius: 10px;
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.12s ease;
}
button:active { transform: translateY(1px) scale(0.99); }
button:disabled { opacity: 0.45; cursor: not-allowed; }

.btn {
  padding: 0.7rem 1.3rem;
  font-size: 1rem;
  /* font-weight lives on the `button` ELEMENT above, not on this class, so
     the 22 places that put .btn on an <a> or a <label> rendered at 400 beside
     identical-looking buttons at 700. Declared here so the class carries its
     own appearance whatever element wears it. */
  font-weight: 700;
  background: rgba(255, 255, 255, 0.08);
  color: #f4f6ff; /* var-less fallback */
  color: var(--ink);
  border: 1px solid rgba(255, 255, 255, 0.15);
}
.btn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.14); }

.btn-accent {
  background: var(--accent); /* fallback for browsers without color-mix() */
  background: linear-gradient(180deg, #e8b34b, #e8b34b); /* var-less fallback */
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 100%, white 12%), var(--accent));
  color: #1a1300; /* var-less fallback */
  color: var(--accent-ink);
  border: none;
  box-shadow: 0 4px 18px rgba(232, 179, 75, 0.45); /* var-less fallback */
  box-shadow: 0 4px 18px color-mix(in srgb, var(--accent) 45%, transparent);
}
.btn-accent:hover:not(:disabled) { filter: brightness(1.08); }

.btn-ghost { background: transparent; border: 1px dashed rgba(255,255,255,0.3); color: var(--ink); }

.btn-danger { background: #a4243b; color: #fff; }
.btn-good { background: #2d8a4e; color: #fff; }

input, select, textarea {
  font-family: var(--font-body);
  font-size: 1rem;
  padding: 0.65rem 0.8rem;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(0, 0, 0, 0.35);
  color: #f4f6ff; /* var-less fallback */
  color: var(--ink);
  outline: none;
}
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
/* 0.4 measured 3.76:1 over the input's own rgba(0,0,0,0.35) background — below
   AA for body text, and on the join screen the placeholder is the ONLY label:
   the ROOM CODE and name fields carry no <label>, so a player who cannot read
   the placeholder cannot tell which box is which. 0.52 gives 5.66:1, which
   passes and still reads as a hint; 0.62 was tried first and measured 7.70:1,
   which is bright enough to be mistaken for text already typed. */
input::placeholder, textarea::placeholder { color: rgba(255, 255, 255, 0.52); }

.card {
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: var(--radius);
  padding: 1.2rem;
  box-shadow: var(--shadow);
  backdrop-filter: blur(6px);
  position: relative;
  z-index: 2;
}

/* Secondary text. The fallback is a literal rgba() because Tizen, webOS and
   Fire TV Silk support neither color-mix() nor var() — without it the whole
   declaration is dropped there and .muted inherits full-strength ink, so the
   de-emphasis silently disappears on exactly the screens the room is looking
   at. Every theme's --ink is a near-white (#f0..#ff), so one literal is a good
   stand-in for all eight. */
.muted { color: rgba(245, 246, 252, 0.55); }
.muted { color: color-mix(in srgb, var(--ink) 55%, transparent); }
.gold { color: var(--gold); }
.row { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; }

/* ---------- particles ---------- */

#particle-layer {
  position: fixed;
  top: 0; right: 0; bottom: 0; left: 0; /* old-engine fallback for inset */
  pointer-events: none;
  overflow: hidden;
  z-index: 0;
}
.particle {
  position: absolute;
  top: -50px;
  animation: fall linear forwards;
  opacity: 0.8;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.4));
}
@keyframes fall {
  0% { transform: translateY(-60px) rotate(0deg); opacity: 0; }
  8% { opacity: 0.85; }
  100% { transform: translateY(110vh) rotate(340deg); opacity: 0.5; }
}

.confetti-canvas { position: fixed; top: 0; right: 0; bottom: 0; left: 0; pointer-events: none; z-index: 50; }

/* ---------- toast ---------- */

#toast {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%) translateY(20px);
  background: rgba(10, 10, 14, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.2);
  color: #fff;
  padding: 0.8rem 1.4rem;
  border-radius: 12px;
  opacity: 0;
  transition: opacity 0.25s, transform 0.25s;
  z-index: 100;
  max-width: 90vw;
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
#toast.error { border-color: #ff5d73; color: #ffd9de; }

/* ---------- the board ---------- */

.board {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 0.6vw; /* old-engine fallback */
  gap: clamp(4px, 0.6vw, 10px);
  background: #030616; /* var-less fallback */
  background: var(--board-bg);
  padding: 0.8vw; /* old-engine fallback */
  padding: clamp(6px, 0.8vw, 14px);
  border-radius: var(--radius);
  box-shadow: var(--shadow), inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}

.board .cat, .board .cell {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  border-radius: 6px;
  background: linear-gradient(180deg, #1c2fd6, #101fa8); /* var-less fallback */
  background: linear-gradient(180deg, var(--cell-a), var(--cell-b));
  border: 1px solid #04061c; /* var-less fallback */
  border: 1px solid var(--cell-border);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), inset 0 -8px 18px rgba(0, 0, 0, 0.25);
}

.board .cat {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 1.5vw; /* old-engine fallback */
  font-size: clamp(0.7rem, 1.5vw, 1.5rem);
  line-height: 1.1;
  padding: 0.4em 0.3em;
  min-height: 3.2em;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
}

.board .cell {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 3.2vw; /* old-engine fallback */
  font-size: clamp(1.1rem, 3.2vw, 3rem);
  color: #e8b34b; /* var-less fallback */
  color: var(--value);
  text-shadow: 0 3px 8px rgba(0, 0, 0, 0.65);
  min-height: 2.1em;
  user-select: none;
}
.board .cell.done {
  background: rgba(0, 0, 0, 0.55); /* fallback for browsers without color-mix() */
  background: #030616; /* var-less fallback */
  background: color-mix(in srgb, var(--board-bg) 80%, black);
  color: transparent;
  text-shadow: none;
  box-shadow: none;
  border-color: transparent;
}
button.cell:hover:not(.done):not(:disabled) {
  filter: brightness(1.25);
  transform: scale(1.02);
}

/* ---------- fullscreen clue (TV) ---------- */

.clue-screen {
  position: fixed;
  top: 0; right: 0; bottom: 0; left: 0; /* old-engine fallback for inset */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2.5vh;
  /* extra bottom padding reserves room for the fixed scoreboard + centered
     timer ring, so they never collide (and keeps content clear of TV overscan) */
  padding: 5vh 6vw 16vh;
  /* solid dark base + literal gradient (never the `background` shorthand) */
  background-color: #0a1470;
  background-image: radial-gradient(1000px 600px at 50% 15%, #1c2fd6, transparent 70%), linear-gradient(170deg, #1c2fd6, #0a1470);
  text-align: center;
  z-index: 10;
  animation: clue-in 0.45s cubic-bezier(0.2, 0.9, 0.3, 1.1);
}
/* themed clue background only where color-mix() works */
@supports (background: color-mix(in srgb, #000, #fff)) {
  .clue-screen {
    background-image: radial-gradient(1000px 600px at 50% 15%, color-mix(in srgb, var(--clue-a) 80%, white 6%), transparent 70%), linear-gradient(170deg, var(--clue-a), var(--clue-b));
  }
}
@keyframes clue-in {
  from { transform: scale(0.12); opacity: 0.2; border-radius: 20px; }
  to { transform: scale(1); opacity: 1; }
}
.clue-screen .kicker {
  font-family: var(--font-display);
  font-size: 2.4vw; /* old-engine fallback */
  font-size: clamp(1rem, 2.4vw, 2rem);
  letter-spacing: 0.2em;
  color: #ffd977; /* var-less fallback */
  color: var(--gold);
  text-transform: uppercase;
}
.clue-screen .clue-text {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 5vw; /* old-engine fallback */
  font-size: clamp(1.6rem, 5vw, 4.6rem);
  line-height: 1.18;
  text-transform: uppercase;
  text-shadow: 0 4px 14px rgba(0, 0, 0, 0.7);
  max-width: 22ch;
}
/* A long clue used to push the kicker off the top AND the buzz banner and the
   countdown off the bottom, at once — .clue-screen is a fixed, centred flex
   column under body{overflow:hidden}, so it overflows symmetrically and there
   is no scrollbar and nothing in any log. 15 of the 70 shipped clues are long
   enough to do it at 720p, where a 114-character clue already loses the
   kicker. The clue is what the room reads, but the banner and the clock are
   what it ACTS on.
   Two steps rather than a smooth shrink: a clue is either about one screenful
   or it is not, and two buckets are two numbers to verify instead of a curve.
   Set from the clue's own length in clueView(). */
.clue-screen .clue-text.long  { font-size: 3.6vw; font-size: clamp(1.4rem, 3.6vw, 3.4rem); max-width: 30ch; }
.clue-screen .clue-text.xlong { font-size: 2.8vw; font-size: clamp(1.2rem, 2.8vw, 2.6rem); max-width: 38ch; }
.clue-screen .answer-text {
  font-size: 3vw; /* old-engine fallback */
  font-size: clamp(1.2rem, 3vw, 2.6rem);
  color: #ffd977; /* var-less fallback */
  color: var(--gold);
  font-family: var(--font-display);
  animation: rise 0.4s ease;
}
@keyframes rise { from { transform: translateY(14px); opacity: 0; } to { transform: none; opacity: 1; } }

.clue-screen.buzzing { animation: none; }
.clue-screen.buzzing::after {
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0; /* old-engine fallback for inset */
  border: 10px solid #ffd977; /* var-less fallback */
  border: 10px solid var(--gold);
  pointer-events: none;
  animation: pulse-border 0.8s ease-in-out infinite;
}
@keyframes pulse-border { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }

.team-banner {
  font-family: var(--font-display);
  font-size: 3.4vw; /* old-engine fallback */
  font-size: clamp(1.4rem, 3.4vw, 3rem);
  padding: 0.3em 1em;
  border-radius: 14px;
  background: rgba(0, 0, 0, 0.4);
  border: 3px solid #ffd977; /* var-less fallback */
  border: 3px solid var(--team-color, var(--gold));
  color: #ffd977; /* var-less fallback */
  color: var(--team-color, var(--gold));
  animation: rise 0.3s ease;
  text-transform: uppercase;
}
/* Time's up: red and pulsing, so the room knows the clock died even from
   across a living room. Literal colors — the TV browser has no var(). */
.team-banner.timesup {
  color: #ff5d73;
  border-color: #ff5d73;
  animation: pulse-tile 0.5s ease infinite alternate;
}

/* Player photos. Literal sizes first — the TV browser has no clamp()/var(). */
.avatars { display: flex; gap: 4px; justify-content: center; flex-wrap: wrap; margin: 0.2rem 0; }
.avatars .avatar {
  width: 42px; height: 42px;
  width: clamp(28px, 3.4vw, 52px); height: clamp(28px, 3.4vw, 52px);
  border-radius: 50%;
  object-fit: cover;
  background: #0b1020;
  border: 2px solid #ffd977;
  border: 2px solid var(--team-color, var(--gold));
}
/* In-game scoreboard: keep the strip deliberately short so it never squeezes
   the board grid above it. */
.avatars.tiny { margin: 0; gap: 3px; }
.avatars.tiny .avatar {
  width: 22px; height: 22px;
  width: clamp(16px, 1.7vw, 28px); height: clamp(16px, 1.7vw, 28px);
  border-width: 1px;
}
/* Player phone: photo picker row */
.avatar-row { display: flex; align-items: center; gap: 0.6rem; justify-content: center; flex-wrap: wrap; margin-top: 1.2rem; }
.avatar-preview {
  width: 56px; height: 56px; border-radius: 50%; object-fit: cover;
  border: 2px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.06);
  display: flex; align-items: center; justify-content: center; font-size: 1.6rem;
}
.avatar-preview.empty { opacity: 0.6; }

.dd-splash {
  font-family: var(--font-display);
  font-size: 10vw; /* old-engine fallback */
  font-size: clamp(3rem, 10vw, 9rem);
  color: #ffd977; /* var-less fallback */
  color: var(--gold);
  text-shadow: 0 6px 0 rgba(0, 0, 0, 0.4); /* fallback for browsers without color-mix() */
  text-shadow: 0 0 40px color-mix(in srgb, var(--gold) 60%, transparent), 0 6px 0 rgba(0,0,0,0.4);
  animation: dd-zoom 0.8s cubic-bezier(0.2, 1.4, 0.4, 1);
  text-align: center;
  line-height: 1;
}
@keyframes dd-zoom { from { transform: scale(0.2) rotate(-8deg); opacity: 0; } to { transform: none; opacity: 1; } }

/* ---------- scoreboard ---------- */

.scoreboard {
  display: flex;
  gap: 1vw; /* old-engine fallback */
  gap: clamp(6px, 1vw, 16px);
  justify-content: center;
  flex-wrap: wrap;
}
.score-tile {
  min-width: 14vw; /* old-engine fallback */
  min-width: clamp(110px, 14vw, 210px);
  text-align: center;
  padding: 0.5rem 0.9rem;
  border-radius: 12px;
  background: rgba(0, 0, 0, 0.45);
  border-top: 4px solid #e8b34b; /* var-less fallback */
  border-top: 4px solid var(--team-color, var(--accent));
  box-shadow: var(--shadow);
}
.score-tile .tname {
  font-family: var(--font-display);
  font-size: 1.4vw; /* old-engine fallback */
  font-size: clamp(0.85rem, 1.4vw, 1.25rem);
  letter-spacing: 0.05em;
  color: #f4f6ff; /* var-less fallback */
  color: var(--team-color, var(--ink));
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 14vw; /* old-engine fallback */
  max-width: clamp(110px, 14vw, 210px);
}
.score-tile .tscore {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 2.6vw; /* old-engine fallback */
  font-size: clamp(1.3rem, 2.6vw, 2.4rem);
  color: #e8b34b; /* var-less fallback */
  color: var(--value);
}
.score-tile .tscore.negative { color: #ff8797; }
.score-tile .thints { margin-top: 0.15rem; font-size: clamp(0.7rem, 1vw, 1rem); letter-spacing: 0.05em; line-height: 1; opacity: 0.9; }
.score-tile.control { outline: 2px solid var(--gold); outline-offset: 2px; }
.score-tile.buzzed { animation: pulse-tile 0.7s ease infinite alternate; }
@keyframes pulse-tile { from { transform: scale(1); } to { transform: scale(1.06); } }

/* ---------- utility ---------- */

.code-chip {
  font-family: var(--font-display);
  font-size: 1.5rem;
  letter-spacing: 0.35em;
  padding: 0.2em 0.5em 0.2em 0.75em;
  background: rgba(0, 0, 0, 0.5);
  border: 1px solid #e8b34b; /* var-less fallback */
  border: 1px solid var(--accent);
  color: #ffd977; /* var-less fallback */
  color: var(--gold);
  border-radius: 10px;
}

.swatch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 0.6rem; }
.swatch {
  border-radius: 10px;
  padding: 0.6rem;
  text-align: center;
  cursor: pointer;
  border: 2px solid transparent;
  color: #fff;
  font-weight: 700;
  font-size: 0.85rem;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}
/* The selected-theme ring. Host console only, so the TV browsers never see it,
   but the fallback costs one line and keeps the rule readable next to the rest. */
.swatch.selected { border-color: #ffd977; box-shadow: 0 0 0 3px rgba(255, 217, 119, 0.4); }
.swatch.selected { border-color: var(--gold); box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 40%, transparent); }

.spinner {
  width: 20px; height: 20px;
  border: 3px solid rgba(255, 255, 255, 0.25);
  border-top-color: var(--accent);
  border-radius: 50%;
  display: inline-block;
  vertical-align: middle;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- TV connection helper (host Cast tab) ---------- */
/* Literal colour fallbacks precede every var()/color-mix: the host console is
   normally a phone, but hosts do open it on the same smart-TV browser they are
   casting to, and those engines drop any declaration they cannot parse. */
.tv-status {
  padding: 0.55rem 0.8rem;
  border-radius: 10px;
  font-weight: 700;
  font-size: 0.9rem;
  text-align: center;
}
.tv-status.wait { background: rgba(255, 217, 119, 0.14); color: #ffd977; }
.tv-status.ok { background: rgba(125, 219, 163, 0.16); color: #7ddba3; }
.tv-status.ok-soft { background: rgba(255, 255, 255, 0.08); color: #dfe4ff; }

.card.way { margin-top: 0.7rem; }
.card.way h4 {
  margin: 0 0 0.4rem;
  font-size: 1rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.card.way p { margin: 0 0 0.5rem; line-height: 1.45; }
.pill-best {
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 0.15rem 0.45rem;
  border-radius: 999px;
  background: rgba(125, 219, 163, 0.18);
  color: #7ddba3;
}
/* The two things a host reads out loud or types on a remote. Deliberately the
   largest text in the panel — everything else here is supporting detail. */
.big-url, .big-code {
  font-family: inherit;
  font-weight: 800;
  color: #ffd977;
  color: var(--gold);
  margin: 0 0 0.7rem;
  word-break: break-all;
}
.big-url { font-size: 1.35rem; }
.big-code { font-size: 2rem; letter-spacing: 0.22em; }
.tv-qr {
  width: 128px; height: 128px;
  background: #fff;
  padding: 6px;
  border-radius: 8px;
  display: block;
  margin: 0.3rem 0 0.5rem;
}
.no-tv-toggle input { width: 20px; height: 20px; flex: none; margin-top: 0.15rem; }

/* ---------------------------------------------------------------- quiz ---- */
/* The phone answer pad. Four big targets that fill the screen: this is used
   one-handed, in a noisy room, while looking up at a TV — so the buttons are
   sized to be hit without aiming, not to look tidy. Literal colour values
   throughout, not var(), because the same stylesheet is parsed by TV browsers
   that do not support custom properties. */
.quiz-pad {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.6rem;
  padding: 0.6rem 0;
  /* Fill the screen rather than sitting in a band at the top. A phone is held
     one-handed and the thumb reaches the BOTTOM most easily, so leaving dead
     space below the pad puts every target in the hardest place to hit. 60vh
     leaves room for the header and, in no-TV mode, the question above it. */
  min-height: 60vh;
}
.quiz-pad .quiz-opt { height: 100%; }
.quiz-opt {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  min-height: 7.5rem;
  border: none;
  border-radius: 16px;
  color: #ffffff;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  padding: 0.8rem 0.5rem;
  box-shadow: 0 4px 0 rgba(0, 0, 0, 0.28);
  transition: transform 0.06s ease;
}
.quiz-opt:active { transform: translateY(3px); box-shadow: 0 1px 0 rgba(0, 0, 0, 0.28); }
.quiz-shape { font-size: 2.6rem; line-height: 1; }
.quiz-opt-text { font-size: 1rem; line-height: 1.2; text-align: center; word-break: break-word; }

/* The phone's clock. A quiz is a race and the phone was the one screen that
   never said so — four coloured buttons and no indication anything was counting
   down, in the format whose whole premise is that the phone is what you hold.
   Tabular figures so the number does not jitter sideways as it ticks, and a
   literal red at the urgent threshold rather than a themed one: this has to
   read as alarm in all eight themes. */
.quiz-clock {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 0.15rem;
  margin: 0.5rem 0;
}
.quiz-clock-n {
  font-size: 2.6rem;
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  min-width: 2ch;
  text-align: right;
}
.quiz-clock-n.urgent { color: #ff6b6b; }
.quiz-clock-s { font-size: 1.1rem; opacity: 0.6; }

/* One column when the phone is narrow: two 7.5rem tiles side by side get
   cramped below about 360px, and a mis-tap costs a question. */
@media (max-width: 360px) {
  .quiz-pad { grid-template-columns: 1fr; }
  .quiz-opt { min-height: 4.5rem; flex-direction: row; gap: 0.8rem; }
}

/* -------------------------------------------------- mini games (phone) ---- */
/* Seven puzzles on one phone. Literal colours and no var()/color-mix()
   throughout, like everything else in this file — the TV browsers parse it too
   and silently drop rules they cannot understand.

   Everything here is sized by a THUMB, not by a cursor. The grids are square
   and use the full width, because a 4x4 Sudoku with 44px cells is a game about
   mis-tapping rather than a game about Sudoku. */
/* The board must be SQUARE and must FIT, on a phone that also has to show a
   header, a clock, directions and a Done button.

   Width alone cannot promise that: at 22rem the grid is 352px, and 352px of
   square board plus the chrome overflows a 667px phone. So the width is capped
   by the VIEWPORT HEIGHT as well — 46vh of width, on a 1:1 box, is 46vh of
   height, which leaves the rest for everything else whatever the screen.

   Literal width first and the min() behind @supports, per the house rule: the
   TV browsers parse this file and silently drop a declaration they cannot
   read. They never render these grids, but a rule that vanishes is exactly the
   failure that is invisible in every log. */
.mini-grid {
  display: grid;
  gap: 0.35rem;
  width: 20rem;
  max-width: 92vw;
  margin: 0.4rem auto;
  aspect-ratio: 1 / 1;
  /* Or the page scrolls under a finger drawing a Zip line. */
  touch-action: none;
  /* The containing block for .zip-trail's absolute overlay. Harmless on every
     other mini game — none of them position anything against it. */
  position: relative;
}
@supports (width: min(10rem, 10vh)) {
  .mini-grid { width: min(20rem, 92vw, 38vh); max-width: none; }
}
/* ROWS as well as columns. Columns alone leaves the rows auto-sized by their
   CONTENT, so a row holding a two-digit checkpoint was 57px tall while a row of
   bare dots was 26px — sixteen squares that were neither square nor the same
   size, on a board whose whole premise is a regular grid. The 1:1 box only
   fixes the OUTSIDE; the tracks inside have to be told to divide it evenly. */
.mini-grid-4 { grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr); }
.mini-grid-5 { grid-template-columns: repeat(5, 1fr); grid-template-rows: repeat(5, 1fr); }

/* The 2x2 box divider, drawn on the GRID rather than on the cells.
   It used to be a margin on the cells at the box edge, which made those cells
   narrower than the others — sixteen squares that were visibly not the same
   size, in a puzzle whose entire premise is a regular grid. A background line
   sits in the existing gap and costs the cells nothing: with four equal tracks
   and three equal gaps, the centre of the grid falls exactly in the middle
   gap. */
.mini-grid-sudoku {
  background-image:
    linear-gradient(to right, transparent calc(50% - 1.5px), rgba(255, 217, 119, 0.5) calc(50% - 1.5px), rgba(255, 217, 119, 0.5) calc(50% + 1.5px), transparent calc(50% + 1.5px)),
    linear-gradient(to bottom, transparent calc(50% - 1.5px), rgba(255, 217, 119, 0.5) calc(50% - 1.5px), rgba(255, 217, 119, 0.5) calc(50% + 1.5px), transparent calc(50% + 1.5px));
}

.mini-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid rgba(245, 246, 252, 0.18);
  border-radius: 10px;
  background: rgba(245, 246, 252, 0.06);
  color: #f5f6fc;
  font: inherit;
  font-size: 1.6rem;
  font-weight: 700;
  cursor: pointer;
  padding: 0;
  min-height: 0;
}
/* A "given" is part of the puzzle, not part of your answer — made obviously
   un-tappable rather than merely ignoring taps, since a control that swallows
   taps silently reads as a broken phone. */
.mini-cell.fixed { background: rgba(245, 246, 252, 0.16); color: #ffd977; cursor: default; }

/* The submit button must not grow. #app is a flex column, so a bare button
   stretches to eat every pixel the grid leaves behind — 270px of solid yellow
   under a 350px puzzle. */
.mini-send, .mini-grid + .btn { flex: 0 0 auto; min-height: 3rem; }
.mini-row .mini-send { flex: 2 0 auto; }
.mini-cell.on { background: #2f6fd0; border-color: #2f6fd0; }
/* Where the line currently ENDS. Every other visited square looks the same, so
   without this you are drawing a line whose end you cannot see — and Undo takes
   back a square you cannot identify. */
.mini-cell.head { background: #6ea8ff; border-color: #ffffff; }
.mini-cell.dot { background: #ffd977; color: #14161f; border-color: #ffd977; }
/* A numbered checkpoint you have already passed through. It has to stay yellow
   — it is still the number 3 — while also showing it is on the line, or there
   is no way to tell which checkpoints you have collected. */
.mini-cell.dot.on { box-shadow: inset 0 0 0 4px #2f6fd0; }
.mini-cell.dot.head { box-shadow: inset 0 0 0 4px #ffffff; }
.mini-cell:active { transform: scale(0.94); }

/* The Zip trail: a thick line through the CENTRES of covered squares, drawn
   on top of the cells (`.mini-cell.on`'s fill reads as a blob once the line
   turns a corner, not a path) rather than replacing that fill. Absolutely
   positioned over `.mini-grid` and `pointer-events: none` so it never steals
   a tap from the button underneath it, and painted by paintZip() — never by
   render() — so it survives a repaint mid-drag. `viewBox="0 0 n n"` lets the
   JS plot cell-centre coordinates directly with no pixel math; the stroke is
   non-scaling so it stays the same visual thickness whether the board is a
   5x5 or a smaller n. */
.zip-trail {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}
.zip-trail polyline {
  fill: none;
  stroke: rgba(255, 255, 255, 0.55);
  /* Thick enough to read as a drawn line rather than a hairline, and to stay
     visible under the finger that is drawing it. */
  stroke-width: 18px;
  vector-effect: non-scaling-stroke;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* 2048. Colours climb with the exponent so the board reads at a glance —
   the tile you are chasing has to be findable without reading it. */
.mini-2048 { gap: 0.4rem; }
/* 2048 is the one game with a THIRD control cluster below the grid — the
   d-pad, on top of the header/clock/directions every game carries and the
   Submit row every game ends with. At the general .mini-grid cap (38vh) that
   combination ran to 787px on a 667px-tall phone, 120px past the fold, with
   Submit and half the d-pad off the bottom. Nothing else on the screen can
   give back that much: the directions are load-bearing copy, the clock and
   header are shared chrome. So the one element unique to this game — its
   board — gives back the width other games' boards don't have to, and the
   d-pad below gives back its padding too (next rule). Literal first, then
   the min() version behind @supports, per the house TV-parses-this-file
   rule — 2048 never plays on a TV, but the cascade order still has to hold
   for the browsers that do read `min()`. */
.mini-2048 { width: 18rem; }
@supports (width: min(10rem, 10vh)) {
  .mini-2048 { width: min(18rem, 86vw, 34vh); }
}
.mini-tile { aspect-ratio: 1 / 1; }
.mini-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  background: rgba(245, 246, 252, 0.07);
  color: #14161f;
  font-weight: 800;
  font-size: 1.5rem;
}
.mini-tile.v2 { background: #efe5da; }
.mini-tile.v4 { background: #ece0c8; }
.mini-tile.v8 { background: #f2b179; }
.mini-tile.v16 { background: #f59563; color: #fff; }
.mini-tile.v32 { background: #f67c5f; color: #fff; }
.mini-tile.v64 { background: #f65e3b; color: #fff; }
.mini-tile.v128 { background: #edcf72; font-size: 1.25rem; }
.mini-tile.v256 { background: #edcc61; font-size: 1.25rem; }
.mini-tile.v512 { background: #edc850; font-size: 1.25rem; }
.mini-tile.v1024 { background: #edc53f; font-size: 1rem; }
.mini-tile.v2048 { background: #edc22e; font-size: 1rem; }

/* Arrow buttons as well as swipes: a swipe inside a scrolling page is
   unreliable on both mobile browsers, and a game that sometimes ignores you is
   worse than one with visible controls. */
.mini-dpad { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; margin: 0.3rem 0; }
.mini-dpad-mid { display: flex; gap: 0.3rem; }
/* Smaller than the quiz/pick-pad targets on purpose: a d-pad button is
   pressed dozens of times a round from a thumb already resting near it, not
   aimed at once like a quiz answer — precision, not size, is what matters
   here, and the previous size was borrowed from a control shaped for the
   opposite case. Still comfortably above the 44px touch minimum used
   elsewhere in this file. */
.mini-dpad .btn { min-width: 3.2rem; font-size: 1.1rem; padding: 0.45rem 0.8rem; }

/* Directions, on the screen the game is played from. Two lines and both small:
   the goal the round set, and the gesture that operates it. They have to earn
   their vertical space against a square board that must also fit. */
.mini-goal { text-align: center; font-size: 0.95rem; margin: 0.2rem 0 0; }
.mini-howto {
  text-align: center;
  font-size: 0.82rem;
  margin: 0.15rem 0 0;
  /* Literal first, then the themed version — the .muted lesson. */
  color: rgba(245, 246, 252, 0.6);
}
@supports (color: color-mix(in srgb, red 50%, transparent)) {
  .mini-howto { color: color-mix(in srgb, var(--ink) 60%, transparent); }
}

/* Short phones — an SE at 568px, or anything in landscape-ish aspect.
   Measured rather than guessed: at 320x568 the header, clock, two lines of
   directions, the board, the counter and the buttons came to 606px against
   568 of screen. Everything discretionary gives back a little so the board
   itself gives back least — the board is the game. */
@media (max-height: 620px) {
  .quiz-clock { margin: 0.15rem 0; }
  .quiz-clock-n { font-size: 1.9rem; }
  .mini-goal { font-size: 0.85rem; }
  .mini-howto { font-size: 0.74rem; }
  .mini-grid { margin: 0.25rem auto; }
  /* margin only: the font-size that used to be here was dead. This block is
     at line ~785 and the unconditional `.mini-hint { font-size: 0.9rem }` is
     117 lines LATER at the same specificity, so the later rule always won and
     the short-phone reduction never happened. Kept as margin-only rather than
     moved, because 0.9rem is what every phone has actually been rendering. */
  .mini-hint { margin: 0.15rem 0; }
  .mini-row .btn { padding: 0.55rem 1rem; }
  /* Submit onto its own line rather than squeezed to a third of 320px. */
  .mini-row .mini-send { flex: 1 0 100%; }
}
@supports (width: min(10rem, 10vh)) {
  @media (max-height: 620px) {
    .mini-grid { width: min(20rem, 92vw, 34vh); }
  .mini-clash { font-size: 2.6rem; margin: 0.7rem 0; }
  .mini-sum { font-size: 1.9rem; margin: 0.6rem 0; }
  }
}
/* .mini-2048's own cap (below) is declared BEFORE the generic short-phone
   `.mini-grid` rule just above, at equal specificity — so on a short phone
   the generic 34vh rule was winning and handing 2048 a BIGGER board than its
   unconditional cap gives it everywhere else, backwards from what a short
   phone needs. Repeated here, after, so it wins the cascade on the one
   screen size where the extra control cluster below it matters most. This
   is 320x568 territory — the "extremely small" case this format allows a
   host to scroll on as a last resort, but the board and d-pad give back
   more first because they are the two elements 2048 alone carries. */
@media (max-height: 620px) {
  .mini-2048 { width: 15rem; }
  .mini-dpad { margin: 0.15rem 0; gap: 0.2rem; }
  .mini-dpad .btn { padding: 0.25rem 0.5rem; font-size: 0.9rem; min-width: 2.4rem; }
  #mini-best { margin: 0.15rem 0; }
}
@supports (width: min(10rem, 10vh)) {
  @media (max-height: 620px) {
    .mini-2048 { width: min(15rem, 80vw, 26vh); }
  }
}

/* ---- the six added games ---- */
.mini-grid-3 { grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); }

/* A mini-game pick pad stays 2x2 always, and its tiles are compact always —
   NOT gated behind the 360px width the quiz pad itself collapses at.
   That gate made sense while the only two pad users (Odd One Out, Put It In
   Order) had nothing above the pad but two lines of directions; a quiz
   answer can be a whole sentence and needs the full 7.5rem tile, but these
   options are one word or one number and never needed it at any width.
   Colour Clash and Math Sprint stack a large stimulus (`.mini-clash`,
   `.mini-sum`) ABOVE the same pad, and at 7.5rem tiles that combination
   overflowed a plain 375x667 phone — not a narrow one — by 50px. Memory
   Flash (`.mini-slots`) always deals SIX slots, never four, and overflowed
   the same screen by 120px for the same reason: the pad's own size was
   sized for its two least demanding users, not its tallest. Unconditional
   compact tiles cost the two spacious games (Odd One Out, Put It In Order)
   nothing — they already had room to spare — and are what the other three
   need at every width, not just below 360px. */
.mini-pick-pad, .mini-slots { grid-template-columns: 1fr 1fr; }
.mini-pick-pad .quiz-opt, .mini-slots .quiz-opt { min-height: 3.4rem; flex-direction: column; }

/* Colour Clash. The word is enormous because the whole difficulty is that you
   read it before you can stop yourself — a small word is an easy round. */
.mini-clash {
  text-align: center;
  font-size: 3.4rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin: 1.2rem 0;
  line-height: 1;
}
/* Math Sprint. Tabular figures so the sum does not shuffle sideways. */
.mini-sum {
  text-align: center;
  font-size: 2.4rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  margin: 1rem 0;
}
.mini-num .quiz-opt-text { font-size: 1.5rem; font-variant-numeric: tabular-nums; }
.mini-step { text-align: center; font-size: 0.85rem; opacity: 0.6; margin: 0.2rem 0; }
/* Same bare-.quiz-pad 60vh problem as Odd One Out and Put It In Order, on the
   one game that reuses .quiz-pad under a different name (.mini-slots, for its
   own background tint) — the min-height override has to follow it there too,
   or Memory Flash's six answer slots push Submit off the same screens. */
.mini-pick-pad, .mini-slots { min-height: 0; }

/* Tap in Order: a square that HELD a number stays visible after the digits go,
   or the round is a guess rather than a memory test. */
.mini-cell.spot { border-color: rgba(255, 217, 119, 0.75); background: rgba(255, 217, 119, 0.12); }
/* #0f1117, not white: white on this green is 3.39:1 and the tick is small. */
.mini-cell.spot.on { background: #3a9e54; border-color: #3a9e54; color: #0f1117; }
.mini-cell:disabled { cursor: default; }

/* Lights Out. Lit is the state you are trying to remove, so it is the loud one. */
.mini-light { background: rgba(245, 246, 252, 0.05); }
.mini-light.lit {
  background: #ffd977;
  border-color: #ffd977;
  box-shadow: 0 0 14px rgba(255, 217, 119, 0.5);
}

/* Repeat the Pattern. The lit step has to be unmistakable from across a table
   — this is the one game where missing a single frame loses the round. */
.mini-simon { transition: filter 0.09s ease, transform 0.09s ease, box-shadow 0.09s ease; }
/* Every pad that is NOT currently lit goes dark while the pattern plays. The
   old version brightened one pad against three already-bright ones, which on a
   phone in a lit room is a difference you have to hunt for. Dimming the rest
   turns the flash into the only thing on screen. */
.mini-simon.dim { filter: brightness(0.28); transform: scale(0.97); }
/* Declared AFTER .dim: both are two-class selectors, so source order is what
   decides, and a lit pad that loses to the dim rule is an invisible pattern. */
.mini-simon.dim.lit,
.mini-simon.lit {
  filter: brightness(2.1);
  transform: scale(1.08);
  box-shadow: 0 0 0 5px #ffffff;
}
.mini-simon:disabled { opacity: 1; }

/* Sliding puzzle. Only the tiles that can actually move look movable. */
.mini-tile-n { font-size: 1.8rem; font-weight: 800; }
.mini-tile-n.movable { border-color: rgba(255, 217, 119, 0.7); }
.mini-gap { background: transparent; border: 2px dashed rgba(245, 246, 252, 0.14); }

.mini-row { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; }
/* Undo and Reset share a line and Submit takes the rest. They must be able to
   WRAP: three buttons on one row is 320px of "Undo | Reset | Submit" with the
   labels clipped, and a control whose label you cannot read is not a control.
   The basis keeps the two small ones together and pushes Submit down first. */
.mini-row .btn { flex: 1 1 5.5rem; }
.mini-row .mini-undo { flex: 1 1 5.5rem; }

/* The nudge shown when the working-out looks finished. It is a PROMPT and not
   an auto-submit: rounds used to send themselves the moment the last tile
   landed, which is exactly what made a mis-tap unfixable. */
.mini-ready {
  text-align: center;
  margin: 0.3rem 0 0.1rem;
  font-size: 0.85rem;
  color: #ffd977;
}

/* The pick you are about to send, on the two single-choice games. Without it a
   chosen answer looks identical to an unchosen one, so a screen that now waits
   for Submit gives no sign it registered the tap at all.
   Literal box-shadow first: the TV parses this stylesheet and supports neither
   var() nor color-mix(), so an unguarded modern value drops the whole rule. */
.quiz-opt.chosen {
  box-shadow: inset 0 0 0 4px #ffffff;
  transform: scale(0.97);
}
.mini-send { flex: 2; }
.mini-hint { text-align: center; font-size: 0.9rem; margin: 0.3rem 0; }

/* Order: the badge is the whole feedback loop — it says where each tap landed,
   which is the only way to tell a half-built order from a wrong one. */
.mini-order-opt { position: relative; }
/* A picked tile used to fade to opacity 0.55, which took the NUMBER with it.
   White loses more luminance than a mid-tone tile does, so the badge's contrast
   fell as it faded — to about 2.0:1, and since the number is only drawn on a
   picked tile, 2.0:1 was the only state that ever rendered. CLAUDE.md calls
   this badge "the whole feedback loop — the only way to tell a half-built order
   from a wrong one", so it has to stay legible.
   A ring and an inset shadow mark the tile as picked instead: the state is just
   as obvious and neither the tile nor its ink moves. */
.mini-order-opt.picked {
  box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0.85), 0 0 0 2px rgba(0, 0, 0, 0.35);
}
.mini-order-n {
  position: absolute; top: 0.4rem; left: 0.5rem;
  font-size: 1.3rem; font-weight: 800;
  /* Inherits the tile's paired ink, set inline beside its background. */
  color: inherit;
}

/* Memory: big enough to take in at a glance, because a five-second look is the
   entire game. */
.mini-symbols {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 1rem;
  font-size: 3rem; margin: 1.5rem 0;
}
.mini-ask { font-size: 1.4rem; }
.mini-slots .quiz-opt { background: rgba(245, 246, 252, 0.1) !important; font-size: 1.6rem; }

/* Scramble. The assembled word is the feedback, so it gets the emphasis. */
.mini-word {
  text-align: center; font-size: 2rem; font-weight: 800; letter-spacing: 0.15em;
  min-height: 2.6rem; margin: 0.6rem 0;
}
.mini-letters { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem; margin-bottom: 0.8rem; }
.mini-letter {
  width: 3.2rem; height: 3.6rem;
  border: none; border-radius: 10px;
  background: #2f6fd0; color: #ffffff;
  font: inherit; font-size: 1.6rem; font-weight: 800;
  cursor: pointer;
}
.mini-letter.used { opacity: 0.25; }

/* -------------------------------------------------- mini games (the TV) ---- */
.mini-icon { font-size: 5rem; line-height: 1; }
/* Narrower than it looks like it should be: at 70% a two-player room renders a
   name on the far left and a time on the far right with a metre of nothing
   between them, and the eye stops connecting the two. */
.mini-results { display: flex; flex-direction: column; gap: 0.5rem; width: 52%; margin-top: 1rem; }
.mini-result {
  display: flex; align-items: baseline; gap: 1rem;
  font-size: 1.8rem;
  /* Literal first, then the themed version — .muted taught this lesson once
     already: an unguarded color-mix() is simply dropped on a TV and the rule
     loses that property with nothing in any log. */
  color: rgba(245, 246, 252, 0.6);
  opacity: 0.75;
}
.mini-result.solved { color: #f5f6fc; opacity: 1; }
.mini-result-name { flex: 1; text-align: left; }
.mini-result-time { font-variant-numeric: tabular-nums; }
.mini-result-pts { font-weight: 800; min-width: 5rem; text-align: right; }

@media (max-height: 800px) {
  .mini-icon { font-size: 3.5rem; }
  /* 1.8rem is 28.8px, which is LARGE text and needs only 3:1; 1.4rem is
     22.4px, which is not, and needs 4.5. The unsolved rows measure 4.23:1, so
     this step-down silently moved them from passing to failing. Raising the
     element opacity to 0.85 takes the effective alpha to 0.51 and the ratio
     to ~5.2:1, while staying clearly dimmer than a solved row. */
  .mini-result { font-size: 1.4rem; opacity: 0.85; }
}

/* ---- everyone below the podium, on the final screen ----------------------
   Wrapped and small on purpose: it must not compete with the top three, but a
   player who came eleventh should still find their own name on the television
   at the end of a game they played all of. Literal colours — this is drawn on
   a TV, which has no var(). Sized so ~37 names (the 40-player cap minus the
   podium) still fit a 1080p screen without pushing the podium off. */
/* "+14 more" under a capped list. Literal rgba, no var(): this is read by TV
   browsers, and a dropped declaration here would leave it at full-strength ink
   competing with the names above it. */
.quiz-more {
  margin-top: 0.8rem; font-size: 1.25rem; letter-spacing: 0.06em;
  color: rgba(244, 246, 255, 0.5);
}
.quiz-rest { width: 86%; max-width: 1100px; margin: 1.6rem auto 0; text-align: center; }
.quiz-rest-title {
  font-size: 1.2rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: rgba(244, 246, 255, 0.55);
  margin-bottom: 0.7rem;
}
.quiz-rest-names {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 0.5rem 1.1rem;
}
.quiz-rest-name {
  font-size: 1.35rem;
  color: rgba(244, 246, 255, 0.82);
  white-space: nowrap;
}
/* The place number leads, so the list reads as a continuation of the podium
   rather than as an unordered pile of names. */
.quiz-rest-name b { color: #ffd977; font-weight: 700; margin-right: 0.15em; }
.quiz-rest-name i { font-style: normal; color: rgba(244, 246, 255, 0.5); margin-left: 0.2em; }
@media (max-height: 800px) {
  .quiz-rest { margin-top: 1rem; }
  .quiz-rest-name { font-size: 1.1rem; }
  .quiz-rest-title { font-size: 1rem; }
}

/* ---- the flash, on the TV --------------------------------------------------
   Four games open by showing the room something for a few seconds; these are
   the big-screen copies. Sized in vmin so they fill a television without
   overflowing a laptop being used as one, and written in LITERAL colours with
   no var(), color-mix() or clamp() — this is drawn on Tizen, webOS and Fire TV
   Silk, which drop what they cannot parse and would leave the stimulus
   invisible with nothing in any log. Above the quiz heading only because that
   block has to stay last; everything here obeys the TV's rules regardless. */
.tv-flash { margin: 1.4rem auto 0; }
.tv-dots {
  position: relative;
  width: 46vmin; height: 46vmin;
  margin: 1.2rem auto 0;
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.05);
  border: 2px solid rgba(255, 255, 255, 0.16);
}
.tv-dots i {
  position: absolute;
  width: 1.6vmin; height: 1.6vmin;
  margin: -0.8vmin 0 0 -0.8vmin;
  border-radius: 999px;
  background: #ffd977;
}
.tv-syms {
  display: flex; justify-content: center; flex-wrap: wrap;
  gap: 3vmin; margin: 2vmin auto 0; font-size: 9vmin; line-height: 1.1;
}
.tv-grid {
  display: grid; gap: 1vmin;
  width: 44vmin; height: 44vmin;
  margin: 1.5vmin auto 0;
}
/* Rows AND columns, or a row of empty cells collapses to nothing and the grid
   stops being square — the same trap the phone boards hit. */
.tv-grid i {
  display: flex; align-items: center; justify-content: center;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.05);
  font-style: normal; font-size: 4vmin; font-weight: 700;
}
.tv-grid i.spot { background: #ffd977; color: #14142a; }
.tv-simon {
  display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr;
  gap: 2vmin; width: 44vmin; height: 44vmin; margin: 1.5vmin auto 0;
}
.tv-simon i {
  display: flex; align-items: center; justify-content: center;
  border-radius: 14px; font-style: normal; font-size: 7vmin; color: #fff;
  opacity: 0.32;
}
/* The lit step. Opacity plus a ring rather than a colour change, so which pad
   is lit is legible across a room even on a set with the contrast wound down. */
.tv-simon i.lit {
  opacity: 1;
  box-shadow: 0 0 0 0.8vmin #ffd977, 0 0 6vmin rgba(255, 217, 119, 0.55);
}

/* ---- How Many? -------------------------------------------------------------
   The dot field is square and capped by viewport HEIGHT, the same rule the
   other mini boards follow: a phone carrying a clock, two lines of directions
   and two buttons has far less room than its width suggests. Literal width
   first, because min() with vh is exactly what the TV browsers drop. */
.mini-dots {
  position: relative;
  width: 300px; width: min(88vw, 38vh);
  height: 300px; height: min(88vw, 38vh);
  margin: 0.6rem auto;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.12);
  overflow: hidden;
}
/* Positioned by percentage from the server's unit square, so the field scales
   with the box and the pattern a player sees does not depend on their phone. */
.mini-dots i {
  position: absolute;
  width: 10px; height: 10px;
  margin: -5px 0 0 -5px;                     /* centre the dot on its point */
  border-radius: 999px;
  background: #ffd977;
  background: var(--gold, #ffd977);
}
/* The reveal, on the TV. Read from a sofa, so the marks are fat and the truth
   is a full-height bar rather than a dot. Literal colours throughout — this
   rule is above the TV block but it is drawn ON a TV, which is exactly the
   combination that used to lose its styling silently. */
.mini-spread { width: 90%; max-width: 900px; margin: 1.2rem auto 0.4rem; }
.mini-spread-line {
  position: relative; height: 46px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.14);
}
/* Every guess, unattributed. Semi-transparent so a cluster reads as darker —
   ten people on one number should look different from one person there. */
.mini-spread-line i {
  position: absolute; top: 8px;
  width: 6px; height: 30px; margin-left: -3px;
  border-radius: 3px;
  background: rgba(244, 246, 255, 0.55);
}
.mini-spread-truth {
  position: absolute; top: -6px;
  width: 6px; height: 58px; margin-left: -3px;
  border-radius: 3px;
  background: #ffd977;
}
.mini-spread-legend {
  display: flex; justify-content: center; gap: 0.6rem;
  margin-top: 0.5rem; font-size: 1.4rem;
  color: rgba(244, 246, 255, 0.75);
}
.mini-spread-sep { opacity: 0.4; }
.mini-spread-answer { color: #ffd977; font-weight: 700; }

.mini-guess { margin: 0.8rem auto 0; max-width: 22rem; }
.mini-guess-n {
  font-family: var(--font-display);
  font-size: 3rem; line-height: 1; text-align: center;
  color: #ffd977; color: var(--gold, #ffd977);
}
/* The dash means "you have not answered", so it must not read as a number. */
.mini-guess-n.none { color: #8b93b8; opacity: 0.85; }
.mini-guess-row { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.6rem; }
.mini-guess-row input[type="range"] { flex: 1; min-width: 0; height: 44px; }
.mini-nudge {
  flex: 0 0 auto; min-width: 3rem; min-height: 44px;
  font-size: 1.3rem; line-height: 1;
}
@media (max-height: 620px) {
  .mini-dots { width: 34vh; height: 34vh; }
  .mini-guess-n { font-size: 2.2rem; }
}

/* ---- account controls, top right of every public page ----
   Shared because this appears on nine pages. Defined per page it would drift,
   which is exactly how the gallery card ended up drawn two different ways with
   only one of them carrying the format fix.

   It sits OUTSIDE the page content: being signed in is true of the whole site,
   not of any one card, and Sign out in particular must never end up adjacent to
   a page's primary action — on the home page it used to sit ten pixels under
   "Create game room".

   Literal colours first: app.css is the stylesheet the TV browsers parse and
   they support neither var() nor color-mix(). */
#acctbar {
  position: absolute; top: 0; right: 0;
  display: flex; align-items: center; gap: 0.35rem;
  padding: 0.7rem 0.9rem; font-size: 0.85rem;
  z-index: 30;
}
#acctbar a {
  /* inline-flex + min-height rather than padding alone: padding that reaches a
     44px target makes the hover pill enormous on a desktop, and these measured
     30px, which is under the guideline on the device where the bar sits closest
     to the screen edge. */
  display: inline-flex; align-items: center; min-height: 44px;
  padding: 0 0.6rem; border-radius: 10px;
  color: #f4f6ff; color: var(--ink, #f4f6ff); opacity: 0.75;
  text-decoration: none; white-space: nowrap;
}
#acctbar a:hover { opacity: 1; background: rgba(255, 255, 255, 0.08); }
#acctbar a:focus-visible { outline: 2px solid #ffd977; outline-offset: 2px; }
#acctbar .who { color: #ffd977; color: var(--gold, #ffd977); opacity: 0.95; }
#acctbar .sep { opacity: 0.3; }
/* On a phone the name would push Sign out toward the edge, and the name is the
   less useful half — you know who you are, you came here to act. */
@media (max-width: 430px) {
  #acctbar { padding: 0.3rem 0.35rem; font-size: 0.8rem; }
  #acctbar .who .nm, #acctbar .sep { display: none; }
}

/* The HOST's own answer clock. The two .countdown rules in the TV block below
   are both scoped to .quiz-meta, a TV container, so the span the console mounts
   (host.js:500 for a quiz, :684 for a mini round) matched nothing and drew at
   the inherited 16px — and its .urgent class, added by the countdown in the
   last seconds, styled nothing at all. The host runs the room off this number.
   Smaller than the TV's: the console is read at desk distance and this shares
   its row with the answered-count. Kept ABOVE the TV heading because it is not
   TV CSS, and everything below that heading is held to the TV's no-var() rule. */
.countdown { font-family: var(--font-display); font-size: 1.6rem; font-weight: 800; line-height: 1; }
.countdown.urgent { color: #ff6b6b; }

/* The player's end-of-round card — resultCard() in play.js, shared by all four
   final screens. Kept ABOVE the TV heading: it is phone CSS, and everything
   below that heading is held to the TV's no-var() rule.

   The stats are a labelled ROW, not three stacked centred lines. Stacked, the
   placing sat directly under a header already reading "1 / 5" — two
   number-of-number figures on a 375px screen meaning completely different
   things. A label under each number is what tells them apart. */
.result-icon { font-size: 3.4rem; line-height: 1; }
.result-gained {
  font-family: var(--font-display); font-size: 1.6rem; font-weight: 800;
  color: #ffd977; color: var(--gold, #ffd977);
  margin: 0.2rem 0 0;
}
.result-stats {
  display: flex; justify-content: center; flex-wrap: wrap;
  gap: 0.4rem 1.6rem; margin: 0.9rem 0 0.2rem;
}
.result-stat { display: flex; flex-direction: column; align-items: center; gap: 0.1rem; }
.result-stat b {
  font-family: var(--font-display); font-size: 1.5rem; font-weight: 800; line-height: 1.1;
}
/* The "/ 8" of "3 / 8" is context, not the number that matters. Same size would
   read as a fraction; this reads as a placing out of a field. */
.result-stat b i { font-style: normal; font-size: 0.95rem; opacity: 0.6; }
.result-stat span {
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase; opacity: 0.55;
}
/* Its own class, not the TV's .quiz-move: that rule lives in the TV block and
   is sized for a scoreboard read from a sofa (1.3rem, a fixed 2.6rem column),
   which is wrong inline beside a placing on a phone. */
.result-move { font-size: 0.85rem; font-weight: 700; margin-left: 0.2rem; }
.result-move.up { color: #5fd08a; }
.result-move.down { color: #e2848a; }

/* ---------- who the room is waiting on (mini games, m-play) ---------- */
/* Literal colours first on every property that also uses var(): a TV browser
   supports neither var() nor color-mix() and silently DROPS the declaration. */
.mini-field {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 0.6rem; margin-top: 1.4rem;
}
.mini-chip {
  padding: 0.45rem 1rem;
  border-radius: 999px;
  font-size: 1.5rem;
  background: rgba(255, 255, 255, 0.07);
  color: #e9ecff;
  border: 2px solid rgba(255, 255, 255, 0.14);
}
.mini-chip.done {
  background: rgba(95, 208, 138, 0.22);
  border-color: rgba(95, 208, 138, 0.55);
  color: #ffffff;
}
/* Two density steps, because the room cap is 40 and the TV cannot scroll.
   Literal values only — no var(), no color-mix(). */
.mini-field.many { gap: 0.45rem; margin-top: 1rem; }
.mini-field.many .mini-chip { font-size: 1.15rem; padding: 0.32rem 0.75rem; border-width: 2px; }
.mini-field.crowd { gap: 0.3rem; margin-top: 0.7rem; }
.mini-field.crowd .mini-chip { font-size: 0.95rem; padding: 0.22rem 0.6rem; border-width: 1px; }
/* A 720p TV is the common case and the strip is the tallest thing on this
   screen at 40 players. */
@media (max-height: 800px) {
  .mini-field.many .mini-chip { font-size: 1rem; }
  .mini-field.crowd .mini-chip { font-size: 0.85rem; padding: 0.18rem 0.5rem; }
}

@keyframes mini-pop {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.18); }
  100% { transform: scale(1); }
}
.mini-chip.pop { animation: mini-pop 0.6s ease-out; }

/* The reveal cascade. The DOM order does NOT change — the list stays sorted
   best-first, exactly as it is drawn today. Only the DELAYS are inverted, so
   the bottom row arrives first and the winner's row lands last. Reordering the
   rows would change what the screen SAYS; this changes only when each part of
   it arrives. */
@keyframes mini-casc {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
.mini-result.casc { animation: mini-casc 0.42s ease-out both; }
/* nth-LAST-child, counting from the bottom of the table. nth-child(6)..(1)
   assumes the list is always six long: at three players the rows are children
   1-3, so they draw the 0.48-0.80s delays and the screen sits EMPTY for half a
   second before anything arrives — and a single row waits 0.8s, which reads as
   a bug rather than a flourish. Counting from the end means the bottom row
   always leaves at 0s and the winner's row is always last, at any length. */
.mini-result.casc:nth-last-child(1) { animation-delay: 0s; }
.mini-result.casc:nth-last-child(2) { animation-delay: 0.16s; }
.mini-result.casc:nth-last-child(3) { animation-delay: 0.32s; }
.mini-result.casc:nth-last-child(4) { animation-delay: 0.48s; }
.mini-result.casc:nth-last-child(5) { animation-delay: 0.64s; }
.mini-result.casc:nth-last-child(6) { animation-delay: 0.80s; }

/* The clock under ten seconds. PERIODIC rather than one-shot, which is the only
   reason it is allowed to live in rendered markup: a restart mid-cycle is
   invisible, so a re-render cannot spoil it. */
@keyframes mini-warn {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}
.countdown.warn {
  color: #ffb4a2;
  animation: mini-warn 1s ease-in-out infinite;
}

/* ---------- iOS double-tap ---------- */
/*
 * Without this, Safari on iOS waits ~300ms after a tap to see whether a second
 * one is coming (double-tap to zoom). Two symptoms, both reported from a real
 * game: the first tap on Submit seems not to register, so players tap again —
 * and the second tap zooms the page instead of pressing the button.
 *
 * `manipulation` keeps panning and PINCH zoom and drops only double-tap zoom,
 * which is why it is the fix rather than user-scalable=no on the viewport:
 * that would take pinch zoom away from everyone who needs it, and modern iOS
 * ignores it anyway.
 *
 * .buzzer already carried this — it is the one button in the product that was
 * ever hammered — and every mini-game control was left without it.
 */
button,
.btn,
.mini-cell, .mini-tile, .mini-letter, .mini-step, .mini-clash, .mini-sum,
.mini-dpad button, .mini-order-n, .mini-guess-n, .mini-symbols button {
  touch-action: manipulation;
}

/* ------------------------------------------------ accessibility & polish ---- */

/* Visible focus, and why this is a SECOND rule rather than an edit to the
   `input:focus` one above.
   A selector containing :focus-visible is DROPPED WHOLESALE by an engine that
   does not know it — which is every TV browser this stylesheet is parsed by. So
   the older `:focus { border-color }` rule stays exactly where it is and remains
   the TV's indicator, and this adds a real ring for everything else. Splitting
   them is what keeps the TV from losing focus styling altogether.
   Literal colour first, then the token, per the rule for this file. */
:focus-visible {
  outline: 2px solid #ffd977;
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 6px;
}
/* Inputs already carry `outline: none`; give them the ring back on keyboard
   focus without reintroducing it on click. */
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid #ffd977;
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Announced to screen readers, invisible on screen. Not `display:none`, which
   would take it out of the accessibility tree too. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* A button that reads as a link. Used where the action is an action — the
   thing was an <a> with no href and a click handler, which is a button
   wearing the wrong element. */
.linklike {
  background: none; border: 0; padding: 0;
  font: inherit; cursor: pointer;
  color: #ffd977;
  color: var(--accent);
  text-decoration: underline;
}

/* Scrolling inside the clue editor must not chain to the page behind it. */
dialog { overscroll-behavior: contain; }

/* Stops single-word widows on headings. Unknown to older engines, which simply
   ignore it — no fallback needed because the failure mode is today's wrapping. */
h1, h2, h3 { text-wrap: balance; }

/* The games a host still has open — js/activegames.js, on the home page and on
   /account. Two screens, one description, so one set of rules.

   Every colour is declared literally first: these rules sit ABOVE the TV block
   but app.css is one file and a TV browser parses all of it, and a dropped
   var() here would leave the room code invisible on its own background. */
.active-games { margin: 0 auto 1.4rem; max-width: 760px; text-align: left; }

.game-list { list-style: none; margin: 0.6rem 0 0; padding: 0; display: grid; gap: 0.6rem; }

/* Wraps on a phone, where the button drops under the text rather than
   squeezing the title into two characters. */
.game-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.9rem;
  padding: 0.7rem 0.8rem;
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.035);
}

/* The four letters are how a host recognises the room — the same string that is
   on the TV across the room — so they are set like a code, not like text. */
.game-code {
  font-weight: 800;
  letter-spacing: 0.14em;
  font-size: 1.1rem;
  padding: 0.25rem 0.55rem;
  border-radius: 8px;
  color: #ffd977;
  color: var(--accent);
  background: rgba(255, 217, 119, 0.12);
}

/* Takes the slack so the buttons stay hard right on a wide screen and the
   title truncates rather than pushing them off. */
.game-what { display: grid; gap: 0.15rem; flex: 1 1 12rem; min-width: 0; }
/* Rejoin and End travel together and wrap together: split across two lines they
   read as belonging to different rows. */
.game-actions { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; }
.game-what b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.game-what .muted { font-size: 0.82rem; }

/* ------------------------------------------------------- quiz on the TV ---- */
/* Read from a sofa, not a desk. The phone carries no answer text, so anything
   unreadable here is unreadable full stop. Literal px/rem and no var(),
   color-mix() or clamp(): Tizen, webOS and Fire TV Silk parse this file too and
   silently drop rules they cannot understand — a var() here is a blank screen
   in a living room, with nothing in any log to say so.

   THIS BLOCK MUST STAY LAST. test/quizgame.mjs enforces the rule above by
   slicing this file from this heading to the end, so anything appended below is
   held to the TV's rules whether or not it ever reaches a TV — which is exactly
   how the account bar, appended here, failed a check about the quiz. New rules
   go ABOVE this heading. */
/* The quiz gathering screen: the title and who has joined on the left, how to
   join on the right. Literal px/vh and no var(), clamp() or color-mix() — the
   TV browsers parse this block too. */
.quiz-lobby {
  display: flex; align-items: center; justify-content: center;
  gap: 5vw; height: 100%; padding: 2vh 4vw; text-align: left;
}
/* Grouped in the middle rather than pinned to the two edges: on a 1920px TV,
   `flex: 1` on the left column pushed the QR into the far corner with a dead
   third of the screen between them. */
.quiz-lobby-head { flex: 0 1 auto; min-width: 0; max-width: 46vw; }
.quiz-lobby-title {
  font-size: 3.2rem; font-weight: 800; line-height: 1.1; margin-bottom: 1.2vh;
  color: #ffd977;
}
.quiz-joined {
  display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 2vh;
}
.quiz-joined span {
  font-size: 1.5rem; font-weight: 700;
  padding: 0.5rem 1.1rem; border-radius: 999px;
  background: rgba(255, 255, 255, 0.09);
  border: 1px solid rgba(255, 255, 255, 0.16);
}
/* The corner strip, shown DURING a question. Absolute rather than in flow so
   the question and the tiles keep their position and size — pushing them down
   to make room is exactly what makes a question hard to read across a room.
   Top-LEFT: the question, progress line and countdown all run down the centre,
   and the fullscreen/sound chrome sits bottom-right, so this corner is the one
   thing on these screens that collides with nothing. Dimmed to 0.72 because it
   is reference material, not something anyone should read instead of the
   question. */
.join-corner {
  position: absolute; top: 2.5vh; left: 2.5vw;
  display: flex; align-items: center; gap: 0.9rem;
  opacity: 0.85;
  z-index: 5;
}
.join-corner-text { text-align: right; line-height: 1.3; }
.join-corner-label {
  font-size: 0.85rem; letter-spacing: 0.12em; text-transform: uppercase; opacity: 0.7;
}
.join-corner-url { font-size: 1.15rem; }
.join-corner-code {
  display: inline-block; margin-top: 0.25rem;
  font-size: 1.4rem; letter-spacing: 0.24em;
}
/* A QR has to be SCANNED from a sofa, so it is sized for the room rather than
   shrunk to be polite: at 9vh it was ~97px on a 1080p screen, which a phone
   camera will not resolve from three metres. */
.join-corner-qr {
  width: 13vh; height: 13vh;
  background: #fff; padding: 6px; border-radius: 10px; display: block;
}
@media (max-height: 800px) {
  .join-corner-qr { width: 12vh; height: 12vh; }
  .join-corner-label { font-size: 0.72rem; }
  .join-corner-url { font-size: 0.95rem; }
  .join-corner-code { font-size: 1.1rem; }
}
.join-big { flex: 0 0 auto; text-align: center; }
.join-big-label {
  font-size: 1.1rem; letter-spacing: 0.14em; text-transform: uppercase;
  opacity: 0.7; margin-bottom: 1.2vh;
}
.join-big-qr {
  width: 30vh; height: 30vh;
  background: #fff; padding: 10px; border-radius: 14px; display: block;
}
.join-big-url { font-size: 1.4rem; margin-top: 1.4vh; }
.join-big-code {
  display: inline-block; margin-top: 0.8vh;
  font-size: 2.4rem; letter-spacing: 0.3em;
}
/* A 720p TV is short: shrink the QR rather than let the row overflow. */
@media (max-height: 800px) {
  .quiz-lobby-title { font-size: 2.4rem; }
  .join-big-qr { width: 26vh; height: 26vh; }
  .join-big-code { font-size: 1.9rem; }
  .quiz-joined span { font-size: 1.2rem; }
}
.quiz-screen {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.2rem;
  height: 100%;
  padding: 2vh 3vw;
  text-align: center;
  /* Anchors the corner join strip. Without it that strip would position
     against the viewport instead and drift as the page scrolls. */
  position: relative;
}
.quiz-progress { font-size: 1.4rem; opacity: 0.6; letter-spacing: 0.08em; text-transform: uppercase; }
.quiz-question { font-size: 3.4rem; line-height: 1.15; font-weight: 800; max-width: 90%; }
.quiz-sub { font-size: 1.8rem; opacity: 0.75; }
.quiz-meta { display: flex; align-items: center; gap: 2.4rem; }
/* The clock is the tension in this format, so it must not be the smallest thing
   on the screen. The shared .countdown style is sized for a clue card; on a TV
   quiz it needs to read from the back of the room. */
.quiz-meta .countdown { font-size: 3.2rem; font-weight: 800; line-height: 1; min-width: 3rem; }
.quiz-meta .countdown.urgent { color: #ff6b6b; }
.quiz-count { font-size: 1.5rem; opacity: 0.75; }

.quiz-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; width: 92%; }
.quiz-tile {
  display: flex; align-items: center; gap: 1rem;
  padding: 1.4rem 1.6rem; border-radius: 16px; color: #ffffff;
  font-size: 2.1rem; font-weight: 700; text-align: left;
}
.quiz-tile-shape { font-size: 2.4rem; flex: none; }
.quiz-tile-text { line-height: 1.15; }

/* Reveal: how the room split. Counts only — never who picked what. */
.quiz-answer-line { font-size: 2.6rem; }
.quiz-answer-line strong { font-size: 3rem; }
.quiz-note { font-size: 1.6rem; opacity: 0.8; max-width: 80%; }
.quiz-bars { display: flex; align-items: flex-end; justify-content: center; gap: 2rem; height: 34vh; width: 90%; }
.quiz-bar-col { display: flex; flex-direction: column; align-items: center; justify-content: flex-end; flex: 1; height: 100%; gap: 0.4rem; }
.quiz-bar { width: 100%; border-radius: 10px 10px 0 0; min-height: 6px; }
.quiz-bar-n { font-size: 1.6rem; font-weight: 700; }
.quiz-bar-label { font-size: 1.3rem; line-height: 1.2; }

/* Scoreboard / podium */
.quiz-scores-title { font-size: 2.4rem; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.7; }
.quiz-winner { font-size: 3.4rem; font-weight: 800; }
.quiz-ranks { display: flex; flex-direction: column; gap: 0.7rem; width: 70%; }
.quiz-rank {
  display: flex; align-items: center; gap: 1.2rem;
  background: rgba(255, 255, 255, 0.07); border-radius: 14px; padding: 0.9rem 1.4rem;
  font-size: 2rem;
}
.quiz-place { font-size: 1.6rem; opacity: 0.6; width: 2rem; text-align: right; }
.quiz-rank-name { flex: 1; text-align: left; font-weight: 700; }
.quiz-streak { font-size: 1.4rem; opacity: 0.85; }
.quiz-rank-score { font-weight: 800; }
/* Movement since the question opened, and what it was worth. Literal colours:
   the TV browser has no var(), so a custom property here renders as nothing. */
.quiz-move { font-size: 1.3rem; font-weight: 700; width: 2.6rem; text-align: left; }
.quiz-move.up { color: #5fd08a; }
.quiz-move.down { color: #e2848a; }
.quiz-move.flat { color: #8f97b5; }
.quiz-gain { font-size: 1.4rem; font-weight: 700; color: #5fd08a; }
/* A climb is worth a beat of attention on the big screen — one pulse, not a
   loop: a scoreboard that keeps moving is a scoreboard nobody can read. */
.quiz-rank.climbed { animation: quiz-climb 620ms ease-out 1; }
@keyframes quiz-climb {
  0%   { transform: translateY(10px); background: rgba(95, 208, 138, 0.07); }
  35%  { background: rgba(95, 208, 138, 0.24); }
  100% { transform: translateY(0);    background: rgba(255, 255, 255, 0.07); }
}
/* Respect a viewer who has asked for less motion.
 *
 * Every @keyframes in the product is named here, and test/minigames.mjs
 * DISCOVERS them rather than trusting this list — one rule honoured the
 * preference for months while eight others ignored it, and the count was
 * misremembered as four when it was nine.
 *
 * Turning motion off must leave the RESTING state, never a blank screen. That
 * is free here: render() already paints the finished appearance and animation
 * only decorates it, so `animation: none` lands on a correct screen.
 */
@media (prefers-reduced-motion: reduce) {
  /* quiz-climb */    .quiz-rank.climbed { animation: none; }
  /* clue-in */       .clue-screen { animation: none; }
  /* dd-zoom */       .dd-splash { animation: none; }
  /* pulse-border */  .clue-screen.buzzing::after { animation: none; }
  /* pulse-tile */    .team-banner.timesup { animation: none; }
  /* rise */          .clue-screen .answer-text { animation: none; }
  /* fall */          .particle { animation: none; display: none; }
  /* mini-pop */      .mini-chip.pop { animation: none; }
  /* mini-casc */     .mini-result.casc { animation: none; opacity: 1; transform: none; }
  /* mini-warn */     .countdown.warn { animation: none; }

  /* spin is the exception: it is the loading spinner, and a frozen one reads as
     a hung app rather than as respect. Slowed, not stopped. */
  .spinner { animation-duration: 2.4s; }
}

/* A 720p TV is the common case and everything above is sized for 1080p. */
@media (max-height: 800px) {
  .quiz-question { font-size: 2.6rem; }
  .quiz-tile { font-size: 1.6rem; padding: 1rem 1.2rem; }
  .quiz-rank { font-size: 1.5rem; padding: 0.6rem 1rem; }
  .quiz-bars { height: 28vh; }
}

/* ------------------------------------------------- quiz: host console ---- */
/* The host is standing up, reading questions aloud, glancing down between
   them — so the question and the correct answer have to be findable at a
   glance, not scanned for. */
/* The TV's paused banner. Literal colours and no var() — the TV browser has
   neither. Fixed so it is readable over whatever phase is frozen underneath. */
.quiz-paused-banner {
  position: fixed; left: 50%; bottom: 4vh; transform: translateX(-50%);
  font-size: 2.2rem; font-weight: 800; letter-spacing: 0.08em;
  padding: 0.6rem 1.6rem; border-radius: 999px;
  background: rgba(10, 12, 26, 0.88);
  border: 2px solid #e2848a;
  color: #ffc9cd;
  z-index: 40;
}
/* Pacing controls, shown only before the first question. */
.quiz-pacing {
  display: flex; flex-direction: column; gap: 0.5rem;
  padding: 0.7rem 0.8rem; border-radius: 12px;
  background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.09);
}
.quiz-pace-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; }
.quiz-pace-row input[type="number"] { width: 4.5rem; }
.quiz-pace-row > span:first-of-type { line-height: 1.35; }
/* The handbrake. Sits ABOVE the phase card so it is in the same place all game
   — a control you reach for in an emergency must not move between phases. */
.quiz-transport {
  display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
  margin-bottom: 0.6rem;
}
.quiz-paused-flag {
  font-size: 0.8rem; font-weight: 700; letter-spacing: 0.04em;
  padding: 0.2rem 0.5rem; border-radius: 999px;
  background: rgba(226, 132, 138, 0.18); border: 1px solid rgba(226, 132, 138, 0.45);
  color: #ffc9cd;
}
.host-q { font-size: 1.25rem; font-weight: 700; line-height: 1.25; }
.host-opts { display: flex; flex-direction: column; gap: 0.3rem; }
.host-opt {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.4rem 0.6rem; border-radius: 10px;
  background: rgba(255, 255, 255, 0.05); font-size: 0.95rem;
}
/* The correct one is the single thing the host must not misread. */
.host-opt.is-right { background: rgba(74, 222, 128, 0.16); font-weight: 700; }
.host-answer {
  font-size: 1.05rem; padding: 0.5rem 0.7rem; border-radius: 10px;
  background: rgba(74, 222, 128, 0.14);
}

/* The AI format chooser: two mutually exclusive buttons rather than a select,
   because this is the decision that changes what the whole evening looks like
   (one screen and no phones, or a phone per player) and it should be visible
   without opening anything. */
/* Three buttons now, not two — wraps rather than squeezing them onto one line
   in a panel that is narrow on a laptop and narrower on a phone. */
.ai-format { display: flex; gap: 0.4rem; flex-wrap: wrap; }
/* The game picker. Two columns so seven checkboxes are one glance rather than
   a scroll, and the "(uses AI)" note is de-emphasised because it is a caveat,
   not a label. */
.mini-picker { display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem 0.8rem; }
.mini-pick { display: flex; align-items: center; gap: 0.4rem; font-size: 0.88rem; cursor: pointer; }
.mini-pick em { font-style: normal; opacity: 0.55; font-size: 0.78rem; }
@media (max-width: 520px) { .mini-picker { grid-template-columns: 1fr; } }
.fmt-btn {
  flex: 1; padding: 0.55rem 0.6rem; border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.05); color: inherit;
  font: inherit; font-size: 0.9rem; cursor: pointer;
}
.fmt-btn.is-on { background: rgba(255, 217, 119, 0.18); border-color: rgba(255, 217, 119, 0.55); font-weight: 700; }

/* ---- the format badge on a public-board card ----
   Which format a board is decides whether the evening needs a TV and teams or
   a phone each, so it is the first thing a browsing host has to see — hence a
   colour-coded badge rather than another grey tag.

   Here rather than in a page's own <style> because TWO screens draw these
   cards: the /gallery page and the host console's board picker. Styled on one
   of them only, the badge renders as bare text on the other — which is the
   same class of split that let the console describe every quiz as an empty
   Jeopardy board. Literal colours throughout: app.css is the stylesheet the TV
   browsers parse, and they support neither var() nor color-mix(). */
.fmt {
  font-size: 0.72rem; font-weight: 700; white-space: nowrap;
  letter-spacing: 0.04em; padding: 0.16rem 0.5rem; border-radius: 999px;
}
.fmt-jeopardy {
  background: rgba(93, 142, 255, 0.18); border: 1px solid rgba(93, 142, 255, 0.45);
  color: #b9ccff;
}
.fmt-quiz {
  background: rgba(217, 155, 26, 0.18); border: 1px solid rgba(217, 155, 26, 0.5);
  color: #ffd977;
}
/* A third colour, distinct from both — a format that borrowed another's would
   be indistinguishable at a glance, which is the one job a badge has. */
.fmt-minigames {
  background: rgba(58, 158, 84, 0.18); border: 1px solid rgba(58, 158, 84, 0.5);
  color: #86e0a0;
}
