/* Projector-first. Every size here is picked to be legible from the back row of
   a classroom, not from a laptop on a desk — hence clamp() everywhere and a
   dark ground, because a projected white page washes out a room. The palette is
   the logo's own (src/brand.js) inverted: same magenta, teal and gold, dark
   ground instead of cream paper, so the board reads as the same product.

   The ground is a near-neutral plum rather than a plain black: the logo's
   magenta goes muddy on a warm brown ground and harsh on a pure black one. */

:root {
  --ground: #17121B;
  --panel: #221A28;
  --cell: #2E2436;
  --cell-hover: #3D3047;
  --spent: #1C1620;
  --ink: #FDF8F1;
  --accent: #E71F69;
  --accent-hover: #F3467F;
  --accent-deep: #BE0087;
  --teal: #00A89F;
  --gold: #F8B31A;
  --muted: #A798AE;
  --edge: #46394F;
  --on-bright: #1C1420;

  /* Semantic, and deliberately NOT brand colours: these three have to mean the
     same thing at a glance from the back of a room, so they are picked to stay
     apart from the accent and from each other. --ok leans green rather than
     teal, and --bad leans orange rather than pink, because the brand supplies a
     teal and a pink that would otherwise be mistaken for them. */
  --ok: #5FBE6B;
  --bad: #FF7A5E;
  --hot: #FF9E4D;

  /* Podium metal. Gold doubles as the brand's gold; the other two are the
     medals themselves, which no amount of rebranding should recolour. */
  --silver: #B9C2CC;
  --bronze: #C0794A;
}

* { box-sizing: border-box; }

/* The board is a fixed one-screen layout, never a scrolling page: a projector
   has no scrollbar anyone can reach mid-game, so anything that overflows is
   simply lost. Every panel below shrinks instead of pushing. */
/* All three need the height for the board's own `height: 100%` to resolve —
   stop at body and the percentage falls back to content height, which lets a
   wrapped six-team score strip push itself off the bottom of the screen. */
html, body, #app { height: 100%; overflow: hidden; }

/* Phones re-size text they think is too small to read, and do it unevenly —
   a phone turned sideways inflates some lines and not others. Every size here
   is already chosen; see the Phones section at the end for how they change. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: 'Trebuchet MS', 'Segoe UI', system-ui, sans-serif;
  min-height: 100vh;
}

/* Setup screens size themselves to the window rather than scrolling.
 *
 * A projected screen is often 1280x720, and scrolling one is genuinely awkward
 * — the teacher is at a laptop driving a display across the room. So everything
 * on these screens is measured in vh and shrinks to fit instead.
 *
 * `safe center` is the safety net for the case where it still doesn't fit:
 * plain `center` in a scrollable flex column pushes the overflow off BOTH ends,
 * and no scrolling can bring the first line back. `safe` falls back to
 * flex-start the moment content would overflow, so the top stays reachable.
 */
.centered {
  min-height: 100%;
  max-height: 100%;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: safe center;
  gap: clamp(0.4rem, 1.7vh, 1.2rem);
  padding: clamp(0.6rem, 2.4vh, 2rem);
  text-align: center;
}

/* Default paragraph margins are ~16px top and bottom each; across a subtitle,
   two hints and a caption that is over 100px of a 720px screen spent on nothing. */
.centered p { margin: 0; }

.centered button {
  padding: clamp(0.38em, 1.15vh, 0.7em) clamp(0.85em, 2.2vh, 1.4em);
}

.muted { color: var(--muted); font-size: clamp(0.72rem, 1.8vh, 1rem); }
.error { color: var(--bad); }

h1 { font-size: clamp(1.3rem, 4.4vh, 3rem); margin: 0; }
h2 { font-size: clamp(0.95rem, 2.5vh, 2rem); margin: 0; }

button {
  font: inherit;
  font-weight: 700;
  color: var(--ink);
  background: var(--cell);
  border: 2px solid var(--edge);
  border-radius: 10px;
  padding: 0.7em 1.4em;
  cursor: pointer;
  transition: background 0.12s, transform 0.08s;
}
button:hover { background: var(--cell-hover); }
button:active { transform: translateY(1px); }
button.primary { background: var(--accent); border-color: var(--accent); }
button.primary:hover { background: var(--accent-hover); }
button.ghost { background: transparent; }
button.small { font-size: 0.85rem; padding: 0.45em 0.9em; }

/* A disabled control has to LOOK spent, not merely refuse the click — a primary
   button keeps the accent as its background whatever its state, and in a colour
   this bright that reads as the next thing to press. The spent board squares
   are excluded: they do their own fading, and dimming them twice washes the
   board out. */
button:disabled:not(.clue-cell) { opacity: 0.4; cursor: default; }
button:disabled:not(.clue-cell):hover { background: var(--cell); }
button.primary:disabled:hover { background: var(--accent); }

input[type="text"], input[type="number"] {
  font: inherit;
  color: var(--ink);
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 10px;
  padding: 0.6em 0.9em;
}
input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---------- Brand ---------- */

/* Sized in vh for the same reason everything else on these screens is: the
   setup screens shrink to fit rather than scroll, and a logo fixed in px is the
   one thing that would refuse to give ground when the window is short. */
.brand-mark { display: block; width: auto; flex: none; }
.brand-full { height: clamp(2.6rem, 11vh, 6.5rem); max-width: min(90%, 34rem); }
.brand-compact { height: clamp(1.5rem, 5.4vh, 3rem); max-width: min(74%, 20rem); opacity: 0.92; }

/* Aimed at the teacher, over the heads of the class, so it is deliberately the
   quietest thing on the screen until it is hovered. */
.more-games {
  color: var(--muted);
  font-size: clamp(0.7rem, 1.7vh, 0.95rem);
  text-decoration: none;
  border-bottom: 1px solid var(--edge);
  padding-bottom: 1px;
  transition: color 0.12s, border-color 0.12s;
}
.more-games:hover { color: var(--accent); border-color: var(--accent); }

/* Under the standings rather than among them, with enough air that nobody
   reaches for it while looking for their own name. */
.podium-more { margin-top: clamp(0.5rem, 2vh, 1.4rem); text-align: center; flex: none; }
.caller-more { margin-top: 0.4rem; text-align: center; }

/* The two asks, stacked and centred. The gap is small on purpose: they are one
   block addressed to one person, not two competing offers. */
.after-game {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.3rem, 1.1vh, 0.7rem);
}

/* Louder than the store link below it, and the only thing on this screen that
   is — it is the ask that expires with the lesson. Still nowhere near the
   standings: a card, not a button, because a button on the podium is something
   a child walks up and presses. */
.review-cta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  max-width: min(92%, 34rem);
  padding: clamp(0.4rem, 1.4vh, 0.7rem) clamp(0.7rem, 2vw, 1.2rem);
  border: 1px solid var(--edge);
  border-radius: 12px;
  text-decoration: none;
  color: var(--ink);
  transition: border-color 0.12s, background 0.12s;
}
.review-cta:hover { border-color: var(--accent); background: var(--cell); }
.review-ask { font-weight: 700; font-size: clamp(0.78rem, 1.9vh, 1.05rem); }
/* The half that is a fact rather than a request, so it reads as the small print
   it is — and wraps to two lines on a phone without shoving the podium up. */
.review-why {
  color: var(--muted);
  font-size: clamp(0.66rem, 1.55vh, 0.88rem);
  text-align: center;
  text-wrap: balance;
}

/* ---------- Setup ---------- */

.setup { max-width: 40rem; width: 100%; margin-inline: auto; }
.team-rows { display: flex; flex-direction: column; gap: clamp(0.3rem, 1.1vh, 0.6rem); width: 100%; }
.team-row { display: flex; gap: 0.6rem; align-items: center; }
.team-row input { flex: 1; }
.team-dot {
  width: 1.5rem; height: 1.5rem; border-radius: 50%; flex: none;
}

/* ---------- Board ---------- */

.board-screen {
  height: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  padding: clamp(0.5rem, 1.5vw, 1.2rem);
  gap: 0.8rem;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.topbar { flex: none; }

/* Whose turn it is has to survive being glanced at from the back of a room by
   a teacher who is not looking for it. Three signals carry it at once — size,
   the team's own colour as a filled block, and a one-off pulse on handover —
   because any single one of them gets missed. */
/* Three things fought each other here and all three showed on screen.
   `baseline` lined the mascot up with the text's baseline rather than its
   middle, so the badge sat low; the label had nothing stopping it wrapping, so
   "LE TOCA A" broke after "TOCA"; and the name could not shrink inside a flex
   row without `min-width: 0`, so it was clipped mid-word while the label kept
   space it was not using. The label never wraps and never shrinks, and the name
   takes whatever is left before it ellipses. */
.turn-pill {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  max-width: 100%;
  border: 3px solid;
  border-radius: 999px;
  padding: 0.35em 1.2em;
  animation: turn-in 0.9s ease-out;
}
.turn-pill .label {
  font-size: clamp(0.7rem, 1.1vw, 1rem);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
  white-space: nowrap;
  flex: 0 0 auto;
}
.turn-pill .name {
  font-size: clamp(1.2rem, 2.6vw, 2.4rem);
  font-weight: 800;
  line-height: 1.1;
  flex: 1 1 auto;
  min-width: 0;
}

@keyframes turn-in {
  0%   { transform: scale(0.94); }
  45%  { transform: scale(1.05); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .turn-pill { animation: none; }
}

.board {
  flex: 1;
  /* Without this the grid refuses to shrink below its content height and
     shoves the score strip off the bottom of the screen. */
  min-height: 0;
  display: grid;
  gap: clamp(4px, 0.6vw, 10px);
  grid-auto-rows: 1fr;
}

.cat {
  background: var(--accent);
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0.4rem;
  font-size: clamp(0.7rem, 1.3vw, 1.3rem);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  line-height: 1.15;
}

.clue-cell {
  background: var(--cell);
  border: none;
  border-radius: 10px;
  color: var(--gold);
  font-size: clamp(1.2rem, 3.2vw, 3.2rem);
  font-weight: 800;
  display: flex;
  align-items: center;
  justify-content: center;
}
.clue-cell:hover { background: var(--cell-hover); }
.clue-cell[disabled] {
  background: var(--spent);
  color: transparent;
  cursor: default;
}

/* ---------- Scores ---------- */

/* Centred, like the award buttons: with seven or eight teams the strip wraps
   on a smaller projector, and a second row hugging the left edge reads as
   something that ran out of room rather than a scoreboard. */
.scores { display: flex; gap: 0.6rem; flex-wrap: wrap; justify-content: center; flex: none; }
.score {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 10px;
  padding: 0.4rem 0.9rem;
  font-size: clamp(0.85rem, 1.5vw, 1.3rem);
}
.score.active { border-width: 3px; font-weight: 800; }
.score .pts { font-weight: 800; color: var(--gold); }
/* The strip is the shortcut into the score panel (openScores), so it has to say
   it is touchable — quietly. It sits under the board all game and must not read
   as another row of buttons to press. */
.score { cursor: pointer; }
.score:hover { border-color: var(--muted); }

/* ---------- Clue overlay ---------- */

/* One padding value, named, because the Daily Double has to add the banner's
   height to the same number — and because the corner clock is inset by it. It
   answers to the height as well as the width: 48px of margin top and bottom is
   nothing on a 1080p screen and a tenth of a 720p one. */
.clue-screen {
  --pad: clamp(0.6rem, min(3vw, 2.6vh), 3rem);
  position: fixed;
  inset: 0;
  overflow: hidden;
  background: var(--ground);
  display: flex;
  flex-direction: column;
  padding: var(--pad);
  gap: 1.5rem;
  z-index: 10;
}

 /* Doubles as the "write it here" instruction: the printed team sheet is this
    same grid, so category + value already names one square and no second
    numbering scheme is needed. Centred and large because students read it off
    the wall while hunting for the right box on their paper. */
.clue-head {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2rem;
  text-align: center;
}
.clue-head .where {
  font-size: clamp(1rem, 2.4vw, 2rem);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.clue-head .where .value { color: var(--gold); }
.clue-head .note {
  font-size: clamp(0.7rem, 1.2vw, 1rem);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

/* `safe center` for the same reason the setup screens use it: a plain centred
   column spills its overflow out of BOTH ends, and here the bottom end is
   where the answer and the award live. */
.clue-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: safe center;
  overflow-y: auto;
  /* Height as well as width: on a 1280x720 projector the width-only gap was
     spending 40px between every element of a screen that had no 40px to
     spare, which is what pushed the tail of the clue out of view. */
  gap: clamp(0.6rem, min(3vw, 3.5vh), 2.5rem);
  text-align: center;
}
/* Same reasoning, and the one element big enough to crowd out everything under
   it. Scoped to the clue: bingo's calling screen is the glyph and little else,
   so there it can have the room. */
.clue-body .call-audio { font-size: clamp(2.5rem, min(14vw, 17vh), 12rem); }
/* Reading, not listening: the text is on screen, so the two things that stood
   in for it go. They come back if it is hidden again. */
.clue-body.reading .call-audio,
.clue-body.reading .call-note { display: none; }
.clue-body .audio-text { font-size: clamp(1.2rem, min(3vw, 5vh), 2.6rem); }
.clue-body .call-en { font-size: clamp(1rem, min(2.6vw, 4vh), 2.2rem); }
/* Capped by line length, not by the window: a sentence spanning the full width
   of a projector screen is read by turning your head. Two short lines beat one
   very long one at the back of a classroom. */
/* The height cap does nothing at 1080p, where the 5rem ceiling is already the
   binding one. It bites below about an 800px-tall window — a 720p projector, a
   laptop with a browser chrome on it — which is exactly where four lines of
   clue at 80px had no room to be four lines. */
.prompt {
  font-size: clamp(1.4rem, min(6vw, 10vh), 5rem);
  font-weight: 700;
  line-height: 1.25;
  margin: 0;
  max-width: 26ch;
}
/* The answer owns a row of the clue screen, rather than being the last line
   inside the clue body — a long clue, a big 🔊, the English and the timer
   would push it down past the body's edge and the "+$400" popping up
   underneath was then written straight over it.

   Its own row cannot be written over: the row is `flex: none` against a
   `flex: 1` clue body, so it takes the height it needs and the body gives way,
   however little is left. It takes that height only when it arrives, rather
   than standing empty through the clue — holding the space open would have
   spent it on nothing all game to save a shift that happens anyway, since
   .answered restyles the clue in the same moment. */
.answer {
  flex: none;
  align-self: center;
  max-width: 100%;
  font-size: clamp(1.5rem, min(4.5vw, 7.5vh), 3.5rem);
  font-weight: 800;
  color: var(--ok);
  margin: 0;
  padding: 0.12em 0.55em;
  border: 3px solid var(--ok);
  border-radius: 0.35em;
  background: var(--panel);
}
.answer.hidden { display: none; }
.answer:not(.hidden) { animation: pop-in 0.4s cubic-bezier(.2, 1.5, .4, 1) both; }

/* The corner clock.
 *
 * Out of the column entirely, pinned to the screen's top right: a countdown
 * that a long clue can push off the bottom of the screen is worse than no
 * countdown, since the room stops believing in it. The corner is empty on
 * every clue — the heading above is centred — and on a Daily Double the
 * screen's own top padding has already stepped down past the banner. */
.timer {
  position: absolute;
  top: var(--pad);
  right: var(--pad);
  font-size: clamp(1.6rem, min(6vw, 8vh), 4.5rem);
  font-weight: 800;
  color: var(--gold);
  font-variant-numeric: tabular-nums;
}
.clue-screen.daily .timer { top: calc(var(--pad) + clamp(1.7rem, 4.2vw, 3.4rem)); }
.timer.done { color: var(--bad); }

/* `flex: none`: the teacher's row is the one thing that may never be squeezed
   out, so it takes its height first and the clue above gives way. Which makes
   its own gaps worth measuring in vh — they are spent before the clue's. */
.clue-controls { display: flex; flex-direction: column; gap: clamp(0.35rem, 1.2vh, 0.8rem); align-items: center; flex: none; }
.award-row { display: flex; gap: 0.6rem; flex-wrap: wrap; justify-content: center; }
.award-row button { display: flex; align-items: center; gap: 0.5rem; }

/* The two writing times, tied together by a tighter gap than the row they sit
   in, so they read as one control with two settings rather than as two more
   buttons competing with Ver vocabulario beside them. */
.timer-choice { display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; justify-content: center; }
.timer-choice .rate-label { margin-right: 0.15rem; }

/* The length that suits this clue, outlined rather than filled: it is a
   suggestion the teacher may ignore, and filling it would make it the loudest
   thing on a row that already has an answer button on it. */
button.suggested { border-color: var(--accent); }
.hint { color: var(--muted); font-size: clamp(0.7rem, 1.7vh, 0.95rem); }

/* Answered: the clue has done its job, so it stands down and hands the screen
   to the answer. Nothing here is hidden that is still being read — the loud
   parts are the ones that were competing with the answer for the same room. */
/* Height-capped like everything else here, and for one extra reason: this rule
   also lands on a listening clue's text, which is shown at a size of its own
   while the class reads it. Without the cap, answering GREW that text — the
   stood-down clue coming out bigger than the clue itself. */
.clue-screen.answered .prompt { font-size: clamp(1.2rem, min(3.4vw, 4.5vh), 2.6rem); color: var(--muted); }
.clue-screen.answered .call-en { font-size: clamp(0.9rem, min(2.2vw, 3vh), 1.8rem); }
.clue-screen.answered .call-note,
.clue-screen.answered .clue-head .note { display: none; }
/* Answering a listening clue shows its text, so the glyph is normally already
   gone by now — this is for the teacher who hides the text again afterwards,
   and brings a 12rem speaker back onto a screen that now has an answer on it
   too. */
.clue-screen.answered .call-audio { font-size: clamp(1.4rem, min(4vw, 6vh), 3rem); }
/* Tighter than the clue it replaces: answering a listening clue puts its text
   on screen, so the body is holding more lines than it was a moment ago and
   the old spacing tipped it into a scrollbar. */
.clue-screen.answered .clue-body { gap: clamp(0.4rem, min(2vw, 2vh), 1.2rem); }
.clue-screen.answered .answer { font-size: clamp(1.8rem, min(5.5vw, 7.5vh), 4.5rem); }

/* ---------- Shared top bar ---------- */

/* Equal side columns so the topic sits on the true centre of the screen rather
   than in whatever gap its neighbours leave. minmax(0, …) everywhere because a
   long team name on the left must shrink rather than shove the title off-centre. */
.topbar {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr) minmax(0, 1fr);
  align-items: center;
  gap: 0.8rem;
}
.topbar > :first-child { justify-self: start; }
.topbar > :last-child { justify-self: end; }

.topic-wrap { min-width: 0; }
.topic {
  margin: 0;
  text-align: center;
  font-weight: 800;
  letter-spacing: 0.01em;
  white-space: nowrap;
  overflow: hidden;
  /* Overwritten by the fit-to-width pass in ui.js; this is only what the first
     paint uses before that measurement runs. */
  font-size: 40px;
}

.turn-pill .name, .score span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------- Choice buttons (bingo setup, and both games' speed row) ---------- */

.choice-list { display: flex; flex-wrap: wrap; gap: clamp(0.3rem, 1.1vh, 0.6rem); justify-content: center; }
.choice { border-color: var(--edge); }
.choice.chosen {
  background: var(--accent);
  border-color: var(--accent);
}

/* ---------- Bingo: the call ---------- */

.call-stage {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}
.call-view {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  text-align: center;
}
.call-label {
  margin: 0;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: clamp(0.8rem, 1.4vw, 1.2rem);
}
.call-text {
  margin: 0;
  font-size: clamp(2rem, 7vw, 6rem);
  font-weight: 800;
  line-height: 1.2;
  max-width: 22ch;
}
.call-idle { color: var(--muted); font-size: clamp(1rem, 2vw, 1.6rem); }
.call-audio { font-size: clamp(4rem, 14vw, 12rem); line-height: 1; }
.call-note { margin: 0; color: var(--muted); font-size: clamp(1rem, 2vw, 1.6rem); }

button.big { font-size: clamp(1rem, 2vw, 1.6rem); padding: 0.6em 1.6em; }

.revealed {
  font-size: clamp(1.2rem, 2.4vw, 2rem);
  font-weight: 800;
  color: var(--gold);
  padding: 0 0.6em;
}

/* ---------- Bingo: card check ---------- */

.card-number { width: 6rem; font-size: clamp(1rem, 2vw, 1.6rem); text-align: center; }

.check-result { display: flex; flex-direction: column; align-items: center; gap: 1rem; }

.verdict {
  font-size: clamp(1.6rem, 4vw, 3.5rem);
  font-weight: 800;
  letter-spacing: 0.04em;
}
.verdict.win { color: var(--ok); }
.verdict.lose { color: var(--bad); }

.clue-body.check {
  justify-content: flex-start;
  gap: 1rem;
  overflow-y: auto;
  min-height: 0;
}

/* Square, and bounded by the space actually left over rather than by a fixed
   slice of the viewport. A 3x3 fills the same box as a 4x4, so a width-based
   size makes the smaller grid the bigger drawing and pushes the panel into a
   scrollbar. Height first, width follows the aspect ratio. */
.card-grid {
  display: grid;
  gap: 6px;
  aspect-ratio: 1;
  height: min(46vh, 100%);
  width: auto;
  max-width: min(80vw, 46vh);
  flex: 0 1 auto;
  min-height: 0;
}
.card-cell {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0.3rem;
  border: 2px solid var(--edge);
  border-radius: 8px;
  background: var(--cell);
  font-size: clamp(0.6rem, 1.4vw, 1.2rem);
  line-height: 1.15;
  overflow: hidden;
}
/* Called squares are filled, not ticked: from the back of the room a teacher
   needs to read the pattern, not hunt for marks. */
.card-cell.marked {
  background: var(--accent);
  border-color: var(--accent);
  font-weight: 700;
}


/* ---------- Bingo: pattern choice ---------- */

.pattern-choice {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.2rem, 0.7vh, 0.4rem);
}
.pattern-name { font-weight: 800; }
.pattern-note { font-size: 0.8em; color: var(--muted); font-weight: 400; }
.pattern-choice.chosen .pattern-note { color: var(--ink); }

.mini-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 2px;
  width: clamp(1.9rem, 5.2vh, 3.4rem);
}
.mini-cell {
  aspect-ratio: 1;
  border-radius: 2px;
  background: var(--edge);
}
.mini-cell.on { background: var(--gold); }

/* ---------- In-game controls (the speed row is shared with the quiz) ---------- */

.rate-row, .mode-switch {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  justify-content: center;
}
.rate-label {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}
.rate-label.soft { opacity: 0.65; text-transform: none; letter-spacing: 0; }

/* Revealed only when a class is stuck, so it should read as a helper line and
   not compete with the speaker glyph above it. */
.audio-text {
  font-size: clamp(1.2rem, 3vw, 2.6rem);
  color: var(--gold);
  max-width: 26ch;
}

/* ---------- Bingo: called words ---------- */

.clue-body.check.called { justify-content: center; }

.called-list {
  list-style: decimal;
  text-align: left;
  width: min(48rem, 88vw);
  columns: 3;
  column-gap: 2.5rem;
  margin: 0;
  padding-left: 2rem;
  font-size: clamp(0.9rem, 1.8vw, 1.5rem);
}
.called-item { break-inside: avoid; padding: 0.15em 0; }
.called-item.latest { color: var(--gold); font-weight: 800; }


/* display:flex on the inner list would otherwise beat the hidden attribute. */
[hidden] { display: none !important; }

.mix-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.25rem, 1vh, 0.5rem);
  padding: clamp(0.45rem, 1.6vh, 0.9rem) clamp(0.7rem, 2vh, 1.2rem);
  border: 2px dashed var(--edge);
  border-radius: 12px;
}
.mix-box .hint { margin: 0; }

/* The English of a sentence clue: a scaffold shown on request, so it reads as
   support rather than as the answer. */
.call-en-wrap { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
.call-en {
  margin: 0; font-size: clamp(1rem, 2.6vw, 2.2rem); color: var(--muted);
  max-width: 26ch; font-style: italic;
}

/* The one square worth double. Loud on purpose — it is the surprise. */
.daily-banner {
  position: absolute; top: 0; left: 0; right: 0;
  background: var(--gold); color: var(--on-bright); text-align: center;
  font-weight: 800; letter-spacing: 0.18em;
  font-size: clamp(0.9rem, 2.2vw, 1.8rem); padding: 0.35em;
}

/* ---------- Vocabulary flash ---------- */

/* Wide columns on purpose.
   auto-fit with a narrow minimum packed thirty words into ten thin columns and
   three rows, leaving most of the screen empty and the type too small to read
   from the back of a room. A wide minimum forces fewer, fuller columns, and the
   cap stops five words stretching across the whole wall. */
/* `safe center` and a scroll for the list that still doesn't fit: with the
   English showing, thirty words on a 720px projector can run past the bottom,
   and a clipped last row is a word the class never reviews. */
.vocab-body { justify-content: safe center; overflow-y: auto; }
.vocab-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 45vw), 1fr));
  gap: clamp(0.4rem, 2vh, 1.2rem) clamp(1rem, 3vw, 3rem);
  width: min(100%, 68rem);
  margin: 0 auto;
}
.vocab-item {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  border-bottom: 1px solid var(--edge); padding-bottom: 0.3em;
}
/* A word that says itself when tapped (ui.js openVocab). It must still look like
   a list entry, not a grid of thirty buttons — the list is what the class reads. */
button.vocab-item {
  font-weight: inherit;
  background: transparent;
  border: none; border-bottom: 1px solid var(--edge);
  border-radius: 6px 6px 0 0;
  padding: 0.2em 0.3em 0.3em;
}
button.vocab-item:hover { background: var(--cell); }
/* Said once already: the teacher going round the list can see where they got to. */
button.vocab-item.heard { border-bottom: 2px solid var(--accent); }
button.vocab-item.heard .vocab-es { color: var(--gold); }
.vocab-es { font-size: clamp(1rem, 3.2vh, 2.4rem); font-weight: 700; }
/* Muted and italic: support, not a second answer to read. */
.vocab-en { font-size: clamp(0.75rem, 2.1vh, 1.5rem); color: var(--muted); font-style: italic; }

/* ---------- How to play (ui.js openHowTo) ---------- */

/* Two wide columns, filled down and then across, so eight steps with their
   English fit a 720px projector without scrolling and still read in order.
   Wide, because it is height that runs out: every extra wrapped line is a
   line of height. A narrow window drops to one column, and the body scrolls
   rather than clip the last rule. */
.howto-body { justify-content: safe center; overflow-y: auto; }
.howto-steps {
  list-style: none;
  counter-reset: howto;
  margin: 0 auto;
  padding: 0;
  width: min(100%, 90rem);
  columns: 2 24rem;
  column-gap: clamp(1.5rem, 4vw, 4rem);
  text-align: left;
}
.howto-step {
  counter-increment: howto;
  display: flex;
  gap: 0.6em;
  align-items: flex-start;
  break-inside: avoid;
  padding: clamp(0.3rem, 1.2vh, 0.9rem) 0;
  border-bottom: 1px solid var(--edge);
}
/* Drawn by CSS rather than the list's own marker, which can't be given a
   badge, and which a flex row would lose anyway. Same size as the Spanish. */
.howto-step::before {
  content: counter(howto);
  flex: none;
  display: grid;
  place-items: center;
  width: 1.5em;
  height: 1.5em;
  border-radius: 50%;
  background: var(--accent);
  font-size: clamp(0.95rem, 2.8vh, 2rem);
  font-weight: 800;
  line-height: 1;
}
/* Sized to the largest that still fits the longest list (eight steps, the
   quiz) on a 1024x768 projector — still common in classrooms, and the
   tightest of them, since its 4:3 wraps the most lines. */
.howto-icon { flex: none; width: 1.4em; text-align: center; font-size: clamp(1.2rem, 3.7vh, 2.6rem); line-height: 1.1; }
.howto-text p { margin: 0; }
.howto-es { font-size: clamp(0.95rem, 2.8vh, 2rem); font-weight: 700; line-height: 1.25; }
/* The same support styling as the word list's English. */
.howto-text .howto-en { margin-top: 0.15em; font-size: clamp(0.72rem, 1.95vh, 1.35rem); color: var(--muted); font-style: italic; line-height: 1.3; }

/* ---------- Flashcards (ui.js flashDeck) ---------- */

.flash {
  display: flex; flex-direction: column; align-items: center;
  gap: clamp(0.5rem, 2vh, 1.4rem);
  width: min(100%, 60rem);
}
.flash-count { color: var(--muted); font-size: clamp(0.8rem, 2vh, 1.2rem); letter-spacing: 0.1em; }
/* The card is the one thing on screen: tall, and the word as big as a
   projector allows. Sized by height, since that is what runs out first on a
   720px screen with the buttons underneath. */
button.flash-card {
  width: 100%;
  min-height: clamp(9rem, 42vh, 26rem);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.4em;
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 22px;
  padding: 1rem 2rem;
  font-weight: inherit;
}
button.flash-card:hover { background: var(--panel); border-color: var(--accent); }
.flash-main {
  font-size: clamp(2.2rem, 12vh, 8rem); font-weight: 800; line-height: 1.05;
  overflow-wrap: anywhere;
}
/* A card showing its English side reads as the question, not as the word. */
.flash-main.en { font-style: italic; font-weight: 700; color: var(--gold); }
.flash-sub { font-size: clamp(1rem, 4.4vh, 2.8rem); color: var(--muted); font-style: italic; }
.flash-sub.ask { font-size: clamp(0.8rem, 2.2vh, 1.3rem); font-style: normal; letter-spacing: 0.06em; }
.flash-options {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: clamp(0.3rem, 1.1vh, 0.6rem);
}
.flash-options .rate-label { margin-right: 0.2rem; }
/* ← on the first card, 🔊 before an English card has turned: plainly off, or
   the teacher presses it twice and wonders what broke. */
.flash button:disabled { opacity: 0.4; cursor: default; }
.flash button:disabled:hover { background: var(--cell); }

.flash-card.arrive { animation: card-arrive 0.3s ease-out; }
.flash-card.turned { animation: card-turn 0.35s ease-out; }
@keyframes card-arrive {
  from { opacity: 0; transform: translateX(2.5rem); }
  to   { opacity: 1; transform: none; }
}
@keyframes card-turn {
  0%   { transform: perspective(60rem) rotateX(0); }
  50%  { transform: perspective(60rem) rotateX(80deg); }
  100% { transform: perspective(60rem) rotateX(0); }
}

/* ---------- Fun: sound, mascots and motion (fx.js) ----------

   Everything that moves below is switched off together at the end of this
   section under prefers-reduced-motion. That block is also what the builder's
   screenshot capture relies on: it asks for reduced motion, so a TpT thumbnail
   is never a frame caught halfway through a zoom. */

/* The ❓ beside it (ui.js howToButton) is the same size, so the two read as a pair. */
.fx-mute, .howto-btn { font-size: 1.05rem; line-height: 1; padding: 0.35em 0.55em; }

.fx-confetti {
  position: fixed; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 100;
}

/* The team's colour with its animal inside it. Sized in em so it grows with
   whatever it sits beside — the score strip, the turn pill, an award button. */
.team-badge {
  width: 1.9em; height: 1.9em; border-radius: 50%; flex: none;
  display: inline-grid; place-items: center;
  line-height: 1; font-size: 1em;
}
.turn-pill .team-badge { font-size: clamp(1rem, 2vw, 1.8rem); align-self: center; }
.award-row button .team-badge { font-size: 0.95em; }

/* On the setup screen the badge is the reroll button. */
button.team-badge.reroll {
  font-size: clamp(1.1rem, 3vh, 1.6rem);
  padding: 0; border: none;
  width: 1.9em; height: 1.9em;
}
button.team-badge.reroll:hover { filter: brightness(1.15); }
.team-badge.spin { animation: badge-spin 0.45s ease-out; }
@keyframes badge-spin {
  0%   { transform: rotate(0) scale(0.6); }
  70%  { transform: rotate(380deg) scale(1.12); }
  100% { transform: rotate(360deg) scale(1); }
}

/* The board deals itself in, square by square, when a game starts. */
/* `backwards`, not `both`: a fill that outlives the animation keeps holding
   `transform`, and the hover lift below would never work on that board. */
.board.intro .cat { animation: deal 0.4s ease-out backwards; }
.board.intro .clue-cell {
  animation: deal 0.45s cubic-bezier(.2, .9, .3, 1.35) backwards;
  animation-delay: calc(0.25s + var(--i, 0) * 22ms);
}
@keyframes deal {
  from { opacity: 0; transform: translateY(-18px) scale(0.85); }
  to   { opacity: 1; transform: none; }
}
.clue-cell:not([disabled]):hover { transform: scale(1.03); }

/* The double square's clue screen, once the splash has gone: the banner stays
   as the reminder, so the heading moves down clear of it — it used to sit
   jammed against the banner's bottom edge. The height is the banner's own,
   its font size times line height plus padding. */
.clue-screen.daily { padding-top: calc(var(--pad) + clamp(1.7rem, 4.2vw, 3.4rem)); }
.clue-screen.daily .where .value { display: inline-block; }
.clue-screen.dd-revealed .where .value { animation: throb 1s 0.35s ease-in-out 3; }

/* The Daily Double splash (jeopardy.js dailySplash). Acted out in beats — the
   delays here are matched by the sound timings in that function:
   0s title · 0.5s the rule · 0.8s $400 · 1.3s ×2 stamped on · 1.9s = $800 · 2.3s ¡Vamos! */
.dd-splash {
  position: absolute; inset: 0; z-index: 5;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(0.5rem, 2.4vh, 1.6rem);
  padding: 1rem; text-align: center; overflow: hidden;
  background: radial-gradient(circle at 50% 45%, #43113C 0%, var(--ground) 72%);
  animation: dd-in 0.45s cubic-bezier(.2, 1.3, .4, 1) both, shake 0.45s 1.3s ease-in-out;
}
.dd-splash.leaving { animation: dd-out 0.3s ease-in forwards; }
/* The game-show sunburst: alternating gold and magenta rays, turning. */
.dd-burst {
  position: absolute; left: 50%; top: 45%;
  width: 250vmax; height: 250vmax; margin: -125vmax 0 0 -125vmax;
  z-index: -1;
  background: repeating-conic-gradient(
    rgba(248, 179, 26, 0.2) 0deg 9deg, transparent 9deg 18deg,
    rgba(190, 0, 135, 0.30) 18deg 27deg, transparent 27deg 36deg);
  animation: dd-spin 30s linear infinite;
}
.dd-title {
  margin: 0;
  font-size: clamp(2.4rem, min(11vw, 17vh), 9rem);
  font-weight: 900; letter-spacing: 0.02em; line-height: 1;
  color: var(--gold);
  text-shadow: 0 0.06em 0 #7A5400, 0 0 0.5em rgba(248, 179, 26, 0.45);
  animation: dd-title-in 0.6s cubic-bezier(.2, 1.4, .4, 1) backwards;
}
.dd-line {
  margin: 0; font-weight: 800;
  font-size: clamp(1.1rem, min(3.4vw, 5vh), 2.6rem);
  animation: rise-in 0.4s 0.5s ease-out backwards;
}
.dd-math {
  display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
  gap: 0.35em; font-weight: 900;
  font-size: clamp(2rem, min(7vw, 11vh), 6rem);
}
.dd-base { animation: pop-in 0.4s 0.8s cubic-bezier(.2, 1.5, .4, 1) backwards; }
.dd-times {
  color: var(--hot); display: inline-block;
  animation: dd-stamp 0.35s 1.3s cubic-bezier(.2, 1.6, .4, 1) backwards;
}
.dd-eq { color: var(--muted); animation: rise-in 0.3s 1.75s ease-out backwards; }
.dd-total {
  color: var(--gold); font-size: 1.3em; display: inline-block;
  text-shadow: 0 0 0.4em rgba(232, 180, 97, 0.6);
  animation: pop-in 0.5s 1.9s cubic-bezier(.2, 1.6, .4, 1) backwards;
}
/* Bigger than button.big: next to a title this size, a normal primary button
   read as a footnote rather than the thing to press. */
.dd-go {
  font-size: clamp(1.1rem, min(2.8vw, 4.5vh), 2.2rem);
  animation: rise-in 0.4s 2.3s ease-out backwards;
}
@keyframes dd-in {
  from { opacity: 0; transform: scale(0.7); }
  to   { opacity: 1; transform: none; }
}
@keyframes dd-out {
  to { opacity: 0; transform: scale(1.08); }
}
@keyframes dd-spin { to { transform: rotate(360deg); } }
@keyframes dd-title-in {
  0%   { opacity: 0; transform: scale(2.6) rotate(-8deg); }
  60%  { opacity: 1; transform: scale(0.94) rotate(2deg); }
  100% { transform: none; }
}
@keyframes dd-stamp {
  from { opacity: 0; transform: scale(3.2) rotate(-18deg); }
  to   { opacity: 1; transform: none; }
}
@keyframes shake {
  0%, 100% { transform: none; }
  20% { transform: translateX(-10px) rotate(-0.4deg); }
  40% { transform: translateX(9px) rotate(0.4deg); }
  60% { transform: translateX(-6px); }
  80% { transform: translateX(4px); }
}
@keyframes throb {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.18); }
}

/* Who just scored, big, on the clue screen before it closes. */
.award-pop {
  display: flex; align-items: center; gap: 0.5em;
  font-size: clamp(1.2rem, 3vw, 2.4rem); font-weight: 800; color: var(--gold);
  border: 3px solid; border-radius: 999px; padding: 0.25em 0.9em 0.25em 0.3em;
  background: var(--panel);
  animation: pop-in 0.5s cubic-bezier(.2, 1.5, .4, 1) both;
}
@keyframes pop-in {
  from { opacity: 0; transform: scale(0.4); }
  to   { opacity: 1; transform: none; }
}

/* The score box that just went up. */
.score.bump { animation: bump 0.6s cubic-bezier(.2, 1.5, .4, 1); }
.score.bump .pts { animation: glow 1.2s ease-out; }
@keyframes bump {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.14) translateY(-4px); }
  100% { transform: scale(1); }
}
@keyframes glow {
  from { text-shadow: 0 0 18px var(--gold); }
  to   { text-shadow: none; }
}

/* The leader's crown and a streak's flame, on the score strip. */
.score .crown { font-size: 1.15em; line-height: 1; }
.score .crown.drop { animation: crown-drop 0.7s cubic-bezier(.3, 1.6, .5, 1) both; }
@keyframes crown-drop {
  from { opacity: 0; transform: translateY(-1.6em) rotate(-25deg) scale(1.6); }
  to   { opacity: 1; transform: none; }
}
.score .streak {
  font-weight: 800; color: var(--hot);
  animation: flicker 1.4s ease-in-out infinite;
}
/* The pre-game option and its ❓, on the setup screen.
   In a tinted box rather than loose under the team rows: it changes how the
   game scores, and as a plain line of small text it read as fine print and was
   missed. The name is the size of something to decide on, the terms sit under
   it in one quiet line, and the ❓ holds the rest. */
.option-row {
  display: flex; align-items: center; justify-content: flex-start;
  gap: 0.6rem; margin: 0.9rem auto 0; max-width: 34rem; text-align: left;
  padding: 0.6rem 0.8rem; border-radius: 10px;
  border: 1.5px solid rgba(190, 0, 135, 0.25); background: rgba(246, 220, 236, 0.45);
}
.option-row label { cursor: pointer; display: flex; flex-direction: column; gap: 0.1rem; flex: 1; }
.option-row .option-name { font-weight: 700; font-size: 1rem; }
.option-row .option-terms { font-size: 0.8rem; opacity: 0.75; }
.option-row input { width: 1.2rem; height: 1.2rem; flex: none; cursor: pointer; }
.option-why {
  max-width: 34rem; margin: 0.35rem auto 0; padding: 0.6rem 0.8rem;
  border-radius: 8px; background: rgba(0, 0, 0, 0.04); text-align: left;
  font-size: 0.85rem; line-height: 1.4;
}
.option-why p + p { margin-top: 0.35rem; }

/* A dialog: one card over the board, everything it asks inside it.
   Above .clue-screen's z-index, since it is always asked from one. */
.modal {
  position: fixed; inset: 0; z-index: 20;
  display: flex; align-items: center; justify-content: center;
  padding: 1.5rem; background: rgba(10, 7, 12, 0.72);
}
.modal-card {
  width: min(28rem, 100%);
  display: flex; flex-direction: column; align-items: center; gap: 0.7rem;
  padding: 1.4rem 1.6rem;
  background: var(--panel); border: 2px solid var(--edge); border-radius: 18px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
  text-align: center;
  animation: pop-in 0.25s cubic-bezier(.2, 1.4, .4, 1) both;
}
.modal-title { font-size: 1.3rem; font-weight: 800; }
.modal-note { font-size: 0.95rem; }
.modal-left { font-size: 0.85rem; color: var(--muted); }
/* In the card, not along the bottom of the screen: the question and the answer
   to it have to be readable in one glance. */
.modal-buttons { display: flex; gap: 0.6rem; justify-content: center; margin-top: 0.3rem; }
.confirm-team {
  display: flex; align-items: center; justify-content: center; gap: 0.5rem;
  font-size: 1.05rem; font-weight: 700;
}

/* What the square drops to, as a picture rather than a sentence: a team
   deciding in the middle of a timed clue has a glance, not a read. The old
   value is struck through and dimmed so the eye lands on the new one, which is
   in the colour this game writes every score in. */
.price-drop { display: flex; align-items: baseline; gap: 0.6rem; }
.price-was {
  font-size: 1.5rem; font-weight: 700; color: var(--muted);
  text-decoration: line-through; text-decoration-thickness: 2px;
}
.price-arrow { font-size: 1.2rem; color: var(--muted); }
.price-now { font-size: 2.3rem; font-weight: 900; color: var(--gold); }
@keyframes flicker {
  0%, 100% { transform: scale(1) rotate(0); }
  30% { transform: scale(1.12) rotate(-4deg); }
  60% { transform: scale(0.96) rotate(3deg); }
}

/* "¡Nuevo líder!" / "¡En racha!", over the board for a moment (showMoments).
   The overlay only centres; the card is what moves, so the pop's transform
   never fights the centring. */
.board-screen { position: relative; }
.moment {
  position: absolute; inset: 0; z-index: 8;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
}
.moment.leaving { animation: fade-out 0.4s ease-in forwards; }
.moment-card {
  display: flex; flex-direction: column; gap: 0.4em; align-items: center;
  padding: 0.6em 1.4em;
  background: rgba(25, 21, 18, 0.94);
  border: 3px solid var(--gold); border-radius: 22px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6), 0 0 40px rgba(232, 180, 97, 0.25);
  font-size: clamp(1.2rem, min(3.4vw, 5.5vh), 2.8rem);
  animation: pop-in 0.5s cubic-bezier(.2, 1.5, .4, 1) both;
}
.moment-line { display: flex; align-items: center; gap: 0.5em; flex-wrap: wrap; justify-content: center; }
.moment-head { color: var(--gold); font-weight: 900; }
.moment-name { font-weight: 800; }
.moment-note { color: var(--hot); font-weight: 800; }
@keyframes fade-out { to { opacity: 0; } }

/* The podium (jeopardy.js podium). Tapping anywhere on it throws the
   confetti again. */
.podium-screen {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column; align-items: center;
  gap: clamp(0.3rem, 1.5vh, 1rem);
  cursor: pointer;
}
.podium-title {
  color: var(--gold);
  font-size: clamp(1.4rem, min(4vw, 7vh), 3.4rem);
  animation: pop-in 0.6s cubic-bezier(.2, 1.5, .4, 1) backwards;
}
.podium {
  flex: 1; min-height: 0; width: 100%;
  display: flex; align-items: stretch; justify-content: center;
  gap: clamp(0.4rem, 1.5vw, 1.4rem);
}
.podium-col {
  width: clamp(8rem, 26vw, 24rem);
  display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch;
}
.podium-teams {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: flex-end;
  gap: 0.3rem 1.2rem; padding-bottom: 0.5rem;
}
.podium-team {
  display: flex; flex-direction: column; align-items: center; gap: 0.15rem;
  text-align: center; min-width: 0;
}
.podium-badge { font-size: clamp(1.6rem, 6vh, 3.6rem); }
.place-1 .podium-badge { font-size: clamp(2rem, 8vh, 4.8rem); animation: winner-bounce 1.1s ease-in-out infinite; }
.podium-crown { font-size: clamp(1.9rem, 7.5vh, 4.4rem); line-height: 1; margin-bottom: -0.25em; }
.podium-name { font-weight: 800; font-size: clamp(0.9rem, min(1.8vw, 3.2vh), 1.6rem); }
.podium-pts { font-weight: 800; color: var(--gold); font-size: clamp(0.9rem, min(1.8vw, 3.2vh), 1.6rem); }
.podium-step {
  display: flex; align-items: flex-start; justify-content: center;
  padding-top: 0.2em;
  border-radius: 12px 12px 0 0;
  font-weight: 900; color: var(--on-bright);
  font-size: clamp(1.6rem, min(5vw, 8vh), 4.4rem);
  box-shadow: inset 0 -0.3em 0 rgba(0, 0, 0, 0.18);
}
/* Gold, silver, bronze. The accent used to stand in for bronze, back when it
   was the store's terracotta; it is the logo's magenta now, so the bronze is
   its own colour rather than a third place that isn't a metal. */
.place-1 .podium-step { height: 38%; background: var(--gold); }
.place-2 .podium-step { height: 26%; background: var(--silver); }
.place-3 .podium-step { height: 17%; background: var(--bronze); }
@keyframes winner-bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-12%); }
}

/* The reveal, once: steps rise third → second → first, each team dropping
   onto its step just after. The delays match PODIUM_BEATS in jeopardy.js. */
.podium-screen.reveal .podium-step { animation: step-rise 0.5s cubic-bezier(.2, 1.2, .4, 1) backwards; transform-origin: bottom; }
.podium-screen.reveal .podium-team { animation: team-drop 0.5s cubic-bezier(.3, 1.5, .5, 1) backwards; }
.podium-screen.reveal .place-3 .podium-step { animation-delay: 0.3s; }
.podium-screen.reveal .place-3 .podium-team { animation-delay: 0.55s; }
.podium-screen.reveal .place-2 .podium-step { animation-delay: 0.8s; }
.podium-screen.reveal .place-2 .podium-team { animation-delay: 1.05s; }
.podium-screen.reveal .place-1 .podium-step { animation-delay: 1.3s; }
.podium-screen.reveal .place-1 .podium-team { animation-delay: 1.55s; }
.podium-screen.reveal .podium-rest { animation: rise-in 0.4s 1.9s ease-out backwards; }
@keyframes step-rise {
  from { transform: scaleY(0); }
  to   { transform: none; }
}
@keyframes team-drop {
  from { opacity: 0; transform: translateY(-60px) scale(0.6); }
  to   { opacity: 1; transform: none; }
}

.podium-rest {
  flex: none; display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem;
}
.podium-rest-item {
  display: flex; align-items: center; gap: 0.5rem;
  background: var(--panel); border: 2px solid var(--edge); border-radius: 10px;
  padding: 0.3rem 0.8rem; font-size: clamp(0.85rem, 1.5vw, 1.2rem);
}
.podium-rest-place { font-weight: 900; color: var(--muted); }
.podium-rest-item .pts { font-weight: 800; color: var(--gold); }

/* The writing timer: a ring that drains, the seconds inside it. Sized by the
   height as well as the width, because on a 720p projector the clue screen
   has no height to spare for a large ring under a long prompt. */
.timer.running {
  width: clamp(4.5rem, min(11vw, 17vh), 9rem);
  aspect-ratio: 1;
  display: grid; place-items: center;
}
.timer-ring { position: absolute; inset: 0; transform: rotate(-90deg); overflow: visible; }
.timer-ring circle { fill: none; stroke-width: 8; }
.timer-ring .track { stroke: var(--edge); }
.timer-ring .bar {
  stroke: var(--gold); stroke-linecap: round;
  transition: stroke-dashoffset 1s linear, stroke 0.3s;
}
.timer-num { font-size: 0.55em; position: relative; }
.timer.urgent .bar { stroke: var(--bad); }
.timer.urgent .timer-num { color: var(--bad); animation: throb 1s ease-in-out infinite; }
.timer.done { animation: pop-in 0.4s cubic-bezier(.2, 1.5, .4, 1); }

/* Bingo: the drawn ball. A real ball's look — colour all round, a white disc
   with the number — because that is what the word "bingo" looks like to a
   child before they have played it. */
.bingo-ball {
  --ball: var(--accent);
  width: clamp(3.2rem, 10vh, 6.5rem);
  aspect-ratio: 1; border-radius: 50%;
  display: grid; place-items: center;
  /* Plain first: a browser without color-mix() drops the whole line below. */
  background: var(--ball);
  background:
    radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.55), transparent 38%),
    radial-gradient(circle at 50% 50%, var(--ball) 60%, color-mix(in srgb, var(--ball), #000 35%) 100%);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
  flex: none;
}
.bingo-ball span {
  width: 58%; aspect-ratio: 1; border-radius: 50%;
  background: var(--ink); color: var(--on-bright);
  display: grid; place-items: center;
  font-weight: 800; font-size: clamp(1rem, 3.4vh, 2.2rem);
  font-variant-numeric: tabular-nums;
}
/* A listening clue already has the big speaker glyph; a second big round
   thing beside it would fight it for the eye. */
.call-view.listening .bingo-ball { width: clamp(2.6rem, 7vh, 4.5rem); }

.call-view.fresh .bingo-ball { animation: ball-in 0.8s cubic-bezier(.25, .8, .35, 1) both; }
.call-view.fresh > :not(.bingo-ball) { animation: rise-in 0.4s 0.7s ease-out both; }
@keyframes ball-in {
  0%   { transform: translateX(-48vw) rotate(-600deg); }
  70%  { transform: translateX(0) rotate(0); }
  82%  { transform: translateY(-14%); }
  100% { transform: none; }
}
@keyframes rise-in {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

.verdict.win { animation: pop-in 0.55s cubic-bezier(.2, 1.5, .4, 1) both; }

/* ---------- La Apuesta Final ---------- */

/* The wager round takes the whole screen for three or four beats: the
   category, the bets, the clue, the result. It borrows .board-screen's column
   layout and the clue screen's head/body/controls, so only what is new to the
   round is described here.

   The round is the one part of the game with a number to type into, which is
   why the inputs are sized the way they are: a projector at the front of the
   room and a teacher at arm's length are not the same viewing distance, and
   these are read at both. */
.wager-screen { justify-content: flex-start; }

/* `isolation` is load-bearing: the sunburst below sits at z-index -1 so the
   title paints over it, and without a stacking context here it drops behind
   this element's own background instead and disappears. .dd-splash gets the
   same effect for free from its own z-index. */
.wager-stage {
  flex: 1; min-height: 0; overflow: hidden; isolation: isolate;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(0.4rem, 1.8vh, 1.2rem);
  text-align: center; position: relative;
}

/* The same turning sunburst as the Daily Double, and deliberately so: the
   room has seen it once already and learned that it means money. The radial
   ground under it comes from the same place, and does the same job — without
   it the rays run flat to all four edges and the title has nothing to sit on. */
.stage-intro .wager-stage {
  background: radial-gradient(circle at 50% 45%, #43113C 0%, var(--ground) 72%);
}
.wager-burst {
  position: absolute; left: 50%; top: 45%;
  width: 250vmax; height: 250vmax; margin: -125vmax 0 0 -125vmax;
  z-index: -1;
  background: repeating-conic-gradient(
    rgba(248, 179, 26, 0.2) 0deg 9deg, transparent 9deg 18deg,
    rgba(190, 0, 135, 0.30) 18deg 27deg, transparent 27deg 36deg);
  animation: dd-spin 30s linear infinite;
}

.wager-title {
  margin: 0;
  font-size: clamp(2rem, min(9vw, 13vh), 7rem);
  font-weight: 900; letter-spacing: 0.02em; line-height: 1;
  color: var(--gold);
  text-shadow: 0 0.06em 0 #7A5400, 0 0 0.5em rgba(248, 179, 26, 0.45);
  animation: dd-title-in 0.6s cubic-bezier(.2, 1.4, .4, 1) backwards;
}
.wager-heading {
  margin: 0; font-weight: 800;
  font-size: clamp(1.3rem, min(4.5vw, 6vh), 3rem);
}
.wager-label {
  margin: 0; color: var(--muted);
  font-size: clamp(0.8rem, min(2vw, 2.6vh), 1.3rem);
  text-transform: uppercase; letter-spacing: 0.12em;
}
/* The one thing a team bets on, so it is the biggest thing on the screen after
   the title — and it is a phrase rather than a word, so it wraps. */
.wager-cat {
  font-weight: 900; line-height: 1.1; max-width: 18ch;
  font-size: clamp(1.6rem, min(7vw, 10vh), 5rem);
  animation: pop-in 0.5s 0.35s cubic-bezier(.2, 1.5, .4, 1) backwards;
}
.wager-hint {
  margin: 0; color: var(--muted); max-width: 40ch;
  font-size: clamp(0.75rem, 1.9vh, 1.05rem);
}

/* The terms of the bet. Not muted like .wager-hint — this is the rule of the
   round, not a note about it, so it is set at the size of something meant to be
   read from the back of the room, in the two colours the verdict uses. Wraps to
   two lines when the screen is too narrow to hold both clauses side by side. */
.wager-stakes {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 0.4rem clamp(0.8rem, 3vw, 2.2rem);
  font-size: clamp(0.85rem, min(2.2vw, 2.8vh), 1.5rem); font-weight: 700;
}
.wager-stakes .win { color: var(--ok); }
.wager-stakes .lose { color: var(--bad); }

.wager-rows {
  display: flex; flex-direction: column; gap: clamp(0.25rem, 0.9vh, 0.6rem);
  width: min(46rem, 100%); overflow-y: auto; padding: 0.2rem;
}
/* Five teams and up (see rowsClass). auto-fit rather than a flat two columns,
   so a narrow window folds them back into one list instead of squeezing two
   columns of unreadable rows. */
.wager-rows.two-up, .wager-marks.two-up {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
  gap: clamp(0.25rem, 0.9vh, 0.6rem) 0.8rem;
  width: min(62rem, 100%);
}
.wager-row, .wager-mark {
  display: flex; align-items: center; gap: clamp(0.4rem, 1.4vw, 1rem);
  border: 3px solid var(--edge); border-radius: 0.7rem;
  background: var(--panel); padding: clamp(0.25rem, 0.9vh, 0.6rem) 0.8rem;
  font-size: clamp(0.85rem, min(2.1vw, 2.6vh), 1.4rem);
}
.wager-who { display: flex; align-items: center; gap: 0.5rem; flex: 1; min-width: 0; }
.wager-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wager-have, .wager-bet { color: var(--gold); font-weight: 800; }
/* Only ever shown on a team the betting floor is lifting, so it is the gold of
   the other money on the row rather than muted grey — it is the good news. */
.wager-max { color: var(--gold); font-size: 0.8em; white-space: nowrap; }

.wager-enter { display: flex; align-items: center; gap: 0.4rem; }
.wager-currency { color: var(--gold); font-weight: 800; }
/* Big enough to read from the middle of the room when a team asks the teacher
   to confirm what was typed, and wide enough for five figures.

   The spinner arrows are gone deliberately. They sit inside the field and took
   about a third of it, so a clamped "2400" scrolled its own first digits out of
   sight — and one-dollar steps are no use for a bet that moves in hundreds. */
.wager-input {
  /* Five digits, plus the padding and border that a global border-box would
     otherwise take out of them — without the em term a clamped "2400" scrolls
     its own first digit out of the field. */
  width: calc(5ch + 1.4em); text-align: right; font-weight: 800;
  font-size: clamp(1rem, min(2.6vw, 3.2vh), 1.8rem);
  padding: 0.15em 0.35em; border-radius: 0.4rem;
  border: 2px solid var(--edge); background: var(--cell); color: var(--ink);
  appearance: textfield; -moz-appearance: textfield;
}
.wager-input::-webkit-outer-spin-button,
.wager-input::-webkit-inner-spin-button { appearance: none; margin: 0; }
.wager-input:focus { outline: 3px solid var(--gold); outline-offset: 1px; }

/* This clue shares the screen with a row per team, where a square's clue has
   the whole of it. So it is set smaller than .prompt — still the biggest thing
   on the screen, but sized to leave the eight rows that decide the game
   visible underneath it rather than scrolled off. */
.stage-clue .prompt { font-size: clamp(1.2rem, min(4vw, 5.5vh), 3.2rem); max-width: 30ch; }

.wager-marks {
  display: flex; flex-direction: column; gap: 0.3rem;
  width: min(46rem, 100%); max-height: 38vh; overflow-y: auto; padding: 0.2rem;
}
.wager-mark .wager-name { flex: 1; }
.mark-yes, .mark-no {
  font-weight: 900; line-height: 1; padding: 0.2em 0.6em;
  font-size: clamp(0.9rem, min(2.4vw, 3vh), 1.6rem);
}
/* Unchosen, both read as "not yet answered" rather than as an opinion; chosen,
   the row itself takes the colour, because from the back of the room a tick
   inside a button is smaller than the row it is in. */
.mark-yes.chosen { background: var(--ok); color: var(--on-bright); border-color: var(--ok); }
.mark-no.chosen { background: var(--bad); color: var(--on-bright); border-color: var(--bad); }
/* The verdict tints the row's GROUND, not its border. The border carries the
   team's own colour as an inline style — it is how a team is picked out
   everywhere else on this site — so it both wins the cascade and should. */
.wager-mark.is-right, .wager-row.result.is-right { background: #23351F; }
.wager-mark.is-wrong, .wager-row.result.is-wrong { background: #35211C; }

.wager-row.result .wager-name { flex: 1; }
.wager-swing { font-weight: 900; }
.wager-row.result.is-right .wager-swing { color: var(--ok); }
.wager-row.result.is-wrong .wager-swing { color: var(--bad); }
.wager-total {
  font-weight: 900; color: var(--gold); min-width: 5ch; text-align: right;
  font-size: clamp(1rem, min(2.8vw, 3.4vh), 2rem);
}

@media (prefers-reduced-motion: reduce) {
  .team-badge.spin, .board.intro .cat, .board.intro .clue-cell,
  .clue-screen.dd-revealed .where .value,
  .dd-splash, .dd-splash.leaving, .dd-burst, .dd-title, .dd-line,
  .dd-base, .dd-times, .dd-eq, .dd-total, .dd-go,
  .award-pop, .score.bump, .score.bump .pts,
  .score .crown.drop, .score .streak, .moment.leaving, .moment-card,
  .podium-title, .place-1 .podium-badge,
  .podium-screen.reveal .podium-step, .podium-screen.reveal .podium-team,
  .podium-screen.reveal .podium-rest,
  .timer.urgent .timer-num, .timer.done,
  .call-view.fresh .bingo-ball, .call-view.fresh > :not(.bingo-ball),
  .verdict.win, .flash-card.arrive, .flash-card.turned,
  .wager-burst, .wager-title, .wager-cat,
  .answer:not(.hidden) { animation: none; }
  .timer-ring .bar { transition: none; }
  .clue-cell:not([disabled]):hover { transform: none; }
}

/* ---------- Phones ---------- */

/* The other way these games get played: a small class with no projector, and
   the teacher reading the caller off the phone in their hand. Everything above
   is sized for a wall, and two of its decisions are wrong for a hand.

   The first is the one-screen layout that never scrolls. It is right on a
   projector, where nobody can reach a scrollbar; on a phone, page scrolling is
   the one thing that copes with everything a phone does to a window — text set
   larger, a browser toolbar sitting over the bottom of the page, a keyboard —
   and a Start button under the toolbar of a page that will not scroll cannot
   be pressed at all. So on a small screen the setup screens and the bingo
   caller become ordinary pages. The quiz board stays fixed: it is a grid that
   has to be seen whole.

   Every part of the switch is keyed on :has(), so it arrives whole or not at
   all. A browser that can't read it keeps the locked layout, rather than an
   unlocked screen inside a locked page that scrolls nowhere.

   Sideways counts too (max-height): a phone on its side is 340px tall, and
   the bingo setup does not fit in that either. */
@media (max-width: 600px), (max-height: 500px) {
  html:has(#app > .centered, #app > .bingo-caller) {
    height: auto;
    overflow-x: hidden;
    overflow-y: auto;
  }
  html:has(#app > .centered, #app > .bingo-caller) body,
  html:has(#app > .centered, #app > .bingo-caller) #app {
    height: auto;
    overflow: visible;
  }

  /* Still at least a screen tall, so a short setup keeps sitting in the middle
     of it. The extra room at the bottom is for the thumb, and for the toolbars
     that float over the last few lines of a page. */
  html:has(#app > .centered) #app > .centered {
    min-height: 100vh;
    min-height: 100svh;
    max-height: none;
    overflow: visible;
    padding-bottom: 2.5rem;
  }

  /* The call takes what the screen has left and the controls sit under it, at
     the bottom where the thumb already is. A long clue with its English shown
     pushes them down the page instead of being drawn over them. */
  html:has(#app > .bingo-caller) .bingo-caller {
    height: auto;
    min-height: 100vh;
    min-height: 100svh;
    overflow: visible;
    padding-bottom: 2rem;
  }
  html:has(#app > .bingo-caller) .bingo-caller .call-stage { flex: 1 0 auto; padding: 0.5rem 0; }

  /* A panel over the page scrolls itself; reaching its end should not start
     scrolling the page hidden behind it. */
  .clue-body { overscroll-behavior: contain; }

  /* Sized by the width here, not the height: a card checked on a phone is read
     at arm's length, and the height-first box above made it a postage stamp on
     a screen that is tall and narrow. The panel scrolls if it has to. */
  .card-grid { height: auto; width: min(100%, 24rem, 80vh); max-width: none; }
  .card-cell { font-size: 0.7rem; hyphens: auto; -webkit-hyphens: auto; overflow-wrap: anywhere; }

  /* The top bar's three equal columns leave a phone about a hundred pixels for
     each, and five buttons do not go in a hundred pixels — they stood in a
     column down the side, over the counter and the title. Here it is one
     flowing row instead: the controls' own row dissolves (display: contents)
     so each button can be placed on its own, the topic takes whatever width
     is left and keeps fitting itself to it (fitText in ui.js), and anything
     that does not fit wraps under, centred. */
  .topbar { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 0.3rem 0.4rem; }
  .topbar > .award-row { display: contents; }
  .topbar > :first-child { order: 1; flex: 0 1 auto; min-width: 0; }
  /* A basis, not 0: with none the buttons fill the first row first and the
     topic is left a sliver to shrink its text into. */
  .topbar > .topic-wrap { order: 2; flex: 1 1 5rem; }
  /* The pill's padding is sized to be spotted across a room; at arm's length
     it was the width that kept the icons off the first row of a 360px phone. */
  .topbar .turn-pill { padding: 0.3em 0.75em; gap: 0.4rem; }
  .topbar .turn-pill .label { letter-spacing: 0.06em; }
  .topbar > .award-row > * { order: 5; }
  /* The two icons stay up beside the topic: they are the narrowest things on
     the bar, so they cost the topic least, and leaving them there is what gets
     the worded buttons under it onto one line. After the rule above, which
     matches them too with the same weight. */
  .topbar > .award-row > .fx-mute, .topbar > .award-row > .howto-btn { order: 3; }
  .call-audio { font-size: clamp(2.5rem, min(14vw, 17vh), 12rem); }
}

@media (max-width: 600px) {
  /* Upright, the worded buttons always go on a second row, whether or not the
     first could squeeze one more in: a button that jumps between the rows as
     the counter grows from "9" to "10" is one the teacher has to look for.
     A zero-height item the width of the bar is the line break (order 4,
     between the icons and the words). */
  .topbar::after { content: ''; order: 4; flex-basis: 100%; height: 0; }

  /* The one button pressed every turn, the whole width of the phone. */
  .bingo-caller .clue-controls > .award-row { width: 100%; }
  .bingo-caller .clue-controls .award-row > .big { flex: 1 1 100%; justify-content: center; }

  /* Three columns of words in 350px wrapped every article onto a line of its own. */
  .called-list { columns: 2; column-gap: 1.2rem; width: 100%; font-size: 1rem; }

  /* The word list's column minimum missed two columns on a 390px phone by a
     few pixels, and thirty words in one column is a list scrolled past rather
     than glanced at. */
  .vocab-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.4rem 1rem; }

  /* The card's word was sized by the height alone, which on a tall phone is a
     size no narrow card can hold. */
  .flash-main { font-size: clamp(2rem, min(12vh, 11vw), 8rem); }
  /* Shorter, and its three buttons narrower, so the card, ← 🔊 → and the
     options under them are all on the phone at once rather than scrolled to. */
  button.flash-card { padding: 1rem; min-height: clamp(8rem, 30vh, 26rem); }
  .flash .award-row button { padding: 0.6em 0.9em; }
}

/* ---------- The quiz on a phone ---------- */

/* On its side, a phone shows the whole board well: five columns want width,
   and 740x340 has it. What it has none of is height, and three screens were
   built around a fixed column that shrinks its middle to fit. At 340px the
   middle is gone — the clue was one line with the teacher's buttons under it,
   the bets were one team in a 40px window, the podium's steps a sliver. Those
   three scroll as a page instead, the way the setup screens do (see Phones
   above, and why it is all keyed on :has()). The board itself stays fixed:
   it fits, and it has to be seen whole. */
@media (max-height: 500px) {
  html:has(#app > .wager-screen, #app > .board-screen > .podium-screen) {
    height: auto;
    overflow-x: hidden;
    overflow-y: auto;
  }
  html:has(#app > .wager-screen, #app > .board-screen > .podium-screen) body,
  html:has(#app > .wager-screen, #app > .board-screen > .podium-screen) #app {
    height: auto;
    overflow: visible;
  }
  html:has(#app > .wager-screen, #app > .board-screen > .podium-screen) #app > .board-screen {
    height: auto;
    min-height: 100vh;
    min-height: 100svh;
    overflow: visible;
    padding-bottom: 2rem;
  }
  /* The stage keeps its overflow: hidden, which is what holds the sunburst in;
     it only stops giving up height. The lists inside it stop scrolling on
     their own, since the page now does. */
  html:has(#app > .wager-screen) .wager-stage { flex: 1 0 auto; padding-block: 0.5rem; }
  html:has(#app > .wager-screen) .wager-rows,
  html:has(#app > .wager-screen) .wager-marks { overflow: visible; max-height: none; }
  html:has(#app > .board-screen > .podium-screen) .podium-screen { flex: 1 0 auto; }
  html:has(#app > .board-screen > .podium-screen) .podium { flex: 1 0 15rem; }

  /* A clue is a panel over the board rather than a page, so the panel scrolls:
     the whole clue first, then the buttons that award it. Before, the buttons
     kept their height and the clue got what was left, which was one line. */
  .quiz-clue { overflow-y: auto; overscroll-behavior: contain; gap: 0.6rem; }
  .quiz-clue .clue-body { flex: none; overflow: visible; gap: 0.5rem; }
}

/* Upright, the board is five columns in 360px: squares a thumb covers and
   category names broken a letter at a time. Rather than a board nobody can
   read, the game asks for the phone to be turned — once it has started. The
   setup is a form, and a form works upright, so the teams can be named before
   the phone goes on its side.

   Two layers because only one of them moves: the backdrop and its words stay
   still (body::before), the phone on top of them turns (body::after). Being
   on the body, over everything at a z-index nothing else uses, it also takes
   the taps, so nothing underneath is pressed by accident. */
@media (orientation: portrait) and (max-width: 600px) {
  html:has(#app > .board-screen:not(.bingo-caller)) body::before {
    content: 'Gira el teléfono\A\A Turn your phone sideways to play';
    white-space: pre-line;
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 10rem 1.5rem 0;
    text-align: center;
    font-size: 1.3rem;
    font-weight: 800;
    line-height: 1.3;
    background: var(--ground);
    color: var(--ink);
  }
  html:has(#app > .board-screen:not(.bingo-caller)) body::after {
    content: '📱';
    position: fixed;
    top: 50%;
    left: 50%;
    z-index: 1001;
    width: 6rem;
    height: 6rem;
    margin: -8rem 0 0 -3rem;
    display: grid;
    place-items: center;
    font-size: 4.5rem;
    line-height: 1;
    animation: turn-phone 2.4s ease-in-out infinite;
    pointer-events: none;
  }
}
@keyframes turn-phone {
  0%, 25%   { transform: rotate(0); }
  50%, 85%  { transform: rotate(-90deg); }
  100%      { transform: rotate(0); }
}
@media (prefers-reduced-motion: reduce) {
  html:has(#app > .board-screen:not(.bingo-caller)) body::after { animation: none; transform: rotate(-90deg); }
}

/* Under a revealed answer, where more than one translation is right.
   Quiet and small: it is a note to the teacher standing at the board, not part
   of the answer the class is reading. */
.answer-note{display:block;margin-top:.4em;font-size:.5em;font-weight:400;opacity:.75;letter-spacing:0}
