/* Debate Court — a live TV debate stage.
 *
 * ART DIRECTION comes from the Claude Design project "Debate Master Game Design"
 * (claude.ai/design/p/51123d81-…, file DebateMaster.dc.html): a dark studio, hot orange accent,
 * Outfit for display and Nunito for body, and every screen a cream card floating over the stage.
 * The palette, type scale, radii, shadows and press states below are that file's values.
 *
 * WHAT THE DESIGN DOES NOT COVER, and is therefore mine:
 *   * Responsive behaviour. The prototype is a fixed desktop canvas with a 680px floor; the GDD
 *     makes "the text box and the timer stay visible above the phone keyboard" a hard requirement.
 *     Those rules are at the bottom and are expressed in the design's own tokens.
 *   * The fact-check panel. It postdates the prototype, so it is built from the design's chip,
 *     card and colour vocabulary rather than copied.
 *
 * LAYOUT RULES worth reading before editing:
 *   * The page is a flex COLUMN at 100dvh. `dvh` and not `vh`: on a phone the on-screen keyboard
 *     shrinks the visual viewport, and `vh` keeps sizing to the un-shrunk one, pushing the text box
 *     and the timer underneath the keyboard — the exact failure this repo has paid for once.
 *   * Fitting rules are HEIGHT questions (`max-height`); affordance rules are WIDTH or POINTER
 *     questions. Mixing them either strips a small desktop window's layout or leaves a phone with
 *     one it cannot fit.
 *   * The set never disappears between screens, so the room does not blink out from under the
 *     player — it just gets shorter.
 */

@import url("./fonts/fonts.css");

:root {
  /* Design tokens, verbatim from DebateMaster.dc.html */
  --ink: #16181d;
  --ink-2: #20262e;
  --ink-3: #2b2b33;
  --muted: #78746c;
  --muted-2: #5f5b54;
  /* The leaderboard's own quiet grey — the design uses it for a rank and a win-loss record, both
     of which sit beside the name they qualify rather than under it, and needed to recede further
     than --muted does. */
  --muted-3: #9a968e;
  --paper: #faf9f6;
  --paper-2: #ededea;
  --line: #e7e5df;
  /* The design draws a chip's resting border as ink at 20% rather than as the solid --line, so it
     reads as an outline over whatever it sits on instead of a filled edge. */
  --line-2: rgba(18, 22, 30, 0.2);
  /* Podium metals, over a darker stop each so a bar is a gradient and not a flat block. */
  --gold: #e8a93c;
  --gold-lo: #c4882a;
  --silver: #9fa8b2;
  --silver-lo: #7e8791;
  --bronze: #c08552;
  --bronze-lo: #9d6b3f;
  --accent: #ff6a3d;
  --accent-hi: #ff9c74;
  --accent-wash: #fff1ea;
  --pro: #1fa463;
  --pro-bar: #2fae6e;
  --pro-wash: #e9f7ef;
  --pro-ink: #177a4b;
  --con: #d8434e;
  --con-bar: #e0616b;
  --con-wash: #fbebec;
  --con-ink: #b23a44;
  --stage-top: #101319;
  --stage-mid: #171c26;
  --stage-bot: #232b38;
  --floor: linear-gradient(180deg, #161b24, #101319);
  --desk: linear-gradient(#39424f, #2a323f);
  --bench: linear-gradient(#333b47, #262d38);
  --card-shadow: 0 26px 52px rgba(0, 0, 0, 0.45);
  --lift-shadow: 0 12px 28px rgba(0, 0, 0, 0.35);
  --press-shadow: 0 3px 10px rgba(0, 0, 0, 0.25);
  --display: "Outfit", "Segoe UI", system-ui, sans-serif;
  --body: "Nunito", "Segoe UI", system-ui, sans-serif;

  /* THE TOP BAR'S HEIGHT, authored rather than emergent — and load-bearing, because everything
   * below the bar is sized as a fraction of what is left. It used to be whatever the tallest
   * child happened to measure, which was 94px: a 42px points pill wearing 32px of a paragraph's
   * default margins. Nothing chose that, and the start screen cannot restyle the bar's contents
   * (which it must — see the plaque block) without silently resizing the stage, the room, the
   * bench and the whole cast underneath it. Pinned here, the bar is the same box on every screen
   * whatever is drawn in it. 65 = the 45px wordmark plus the bar's 10px padding pair; 55 is the
   * same sum once the narrow rules shrink the wordmark and the pill becomes the tallest thing. */
  --topbar-h: 65px;

  /* THE PROSE MULTIPLIER. Every size a player READS several hundred words at — the transcript
   * bubbles, the composer, the collapsed last speech, the deliberation recall, the judge's notes
   * and coaching, the fact-check quotes — is authored as `calc(<its own size> * var(--prose))`,
   * and the top bar's text-size control is the only thing that moves it. 1 is the design's own
   * type, untouched.
   *
   * Nothing structural spends it. The clock, the HUD, the chips, the plaque and the scorecard are
   * GLANCED at rather than read, they are measured against the stage's own arithmetic, and scaling
   * them is what would break the one screen this setting exists to help. Every site that does
   * spend it already scrolls inside its own box, so a larger setting costs scrolling and nothing
   * else. Written by setTextSize() as `data-text` on <body>. */
  --prose: 1;
}

body[data-text="l"] {
  --prose: 1.15;
}

body[data-text="xl"] {
  --prose: 1.32;
}

* {
  box-sizing: border-box;
}

[hidden],
.hidden {
  display: none !important;
}

/* NOTHING IN THIS GAME MAY REACH THE BROWSER'S PULL-TO-REFRESH. A mid-debate reload discards the
   debate AND the draft — several hundred words typed and not yet sent, which is the only thing here
   a player cannot get back — and the phone layout puts the composer at the top of a flow column, so
   a downward swipe at scroll-top is an ordinary thing to do while writing. `overscroll-behavior`
   defaults to `auto`, which is what lets that gesture through; `none` on the document refuses both
   the refresh and the rubber band, and every scroll container below carries `contain` so a fling
   inside the transcript or a card cannot chain out to the document either. Measured as `auto` on
   html and body at the 2026-09-10 bar exam. */
html,
body {
  margin: 0;
  padding: 0;
  overscroll-behavior: none;
}

body {
  min-height: 100dvh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--stage-top);
  color: var(--ink);
  font: 400 16px/1.5 var(--body);
  overflow: hidden;
  user-select: none;
}

/* The design's animation vocabulary. Prefixed dm- because characters.js references them by name
   and a bare `blink` would be one collision away from a very odd bug. */
@keyframes dm-blink { 0%, 92%, 100% { transform: scaleY(1); } 95% { transform: scaleY(0.08); } }
@keyframes dm-talk { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(0.4); } }
@keyframes dm-gesture { 0%, 100% { transform: rotate(-8deg); } 50% { transform: rotate(14deg); } }
@keyframes dm-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
@keyframes dm-gavel { 0%, 100% { transform: rotate(6deg); } 50% { transform: rotate(-42deg); } }
@keyframes dm-pop { 0% { opacity: 0; transform: translateY(20px) scale(0.95); } 100% { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes dm-pop-out { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(-14px) scale(0.97); } }
@keyframes dm-row-in { 0% { opacity: 0; transform: translateX(-14px); } 100% { opacity: 1; transform: translateX(0); } }
@keyframes dm-podium-up { 0% { opacity: 0; transform: translateY(26px); } 100% { opacity: 1; transform: translateY(0); } }
@keyframes dm-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } }
@keyframes dm-fall { 0% { transform: translateY(-60px) rotate(0deg); opacity: 1; } 90% { opacity: 1; } 100% { transform: translateY(110vh) rotate(560deg); opacity: 0; } }
@keyframes dm-dot { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
@keyframes dm-live { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }

/* Everything above is decoration — a bobbing cast, falling confetti, cards and rows that arrive
   from somewhere. A player who has asked their OS for less motion gets the same screens with
   every arrival already finished. Collapsed rather than removed, so `backwards` fills and
   `forwards` holds still land on their end frame: an animation set to `none` would leave a row
   staggered by `--d` stuck at opacity 0 forever. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

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

/* THE BAR IS GLASS OVER THE ROOM, on every screen — the design project's start-screen artboard
 * (`Start Screen Ideas.dc.html`, 1d) and no longer only that screen. It shipped gated on the room
 * screen once, which meant pressing "Random claim" swapped it for a solid bar with a wordmark in
 * it: one game wearing two chromes.
 *
 * It KEEPS ITS BOX — still in flow, still `--topbar-h` tall — and only its skin changes. That is
 * the whole trick: everything below is sized as a fraction of the stage, so a bar that shrank
 * would resize the room, the bench and both debaters. The room reaches y=0 because the room IMAGE
 * bleeds up behind the transparent bar (see `.stage > .room`), not because the bar moved. */
.topbar {
  position: relative;
  z-index: 12;
  flex: 0 0 auto;
  height: var(--topbar-h);
  display: flex;
  /* Top-aligned, so the two clusters sit at the artboard's 16px / 14px rather than wherever
     centring in the bar happens to put them. */
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 18px;
  background: none;
  /* The bar itself is air: a full-width strip over the room that swallowed clicks would make the
     top of the plaque unreachable. Only the controls in it take a pointer. */
  pointer-events: none;
}

.topbar > * {
  pointer-events: auto;
}

/* The points pill: star + total, in the same glass as the discs opposite. It is the only place a
   returning player sees their lifetime score before they start. Glass and not the accent ring the
   older design drew: over a lit painted room a 3px orange hoop reads as a warning badge. */
.pts {
  /* A paragraph, so it arrives with 1em of margin top and bottom. Left in place those 32px were
     the tallest thing in the bar and therefore ITS height — see --topbar-h. */
  margin: 6px 0 0; /* 10px of bar padding + 6 = the artboard's 16px. */
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: rgba(12, 15, 20, 0.5);
  border: 1px solid rgba(255, 255, 255, 0.16);
  backdrop-filter: blur(8px);
  border-radius: 999px;
  padding: 6px 13px;
  font-weight: 800;
  font-size: 12.5px;
  /* `line-height: normal`, here and on every rule carrying an artboard `font:` value. The
     shorthand RESETS line-height, so the artboard's `font:800 12.5px Nunito` is 12.5px of Nunito
     on its natural leading — and this file's inherited 1.5 made every one of those boxes a few
     pixels taller than anyone drew. */
  line-height: normal;
  color: var(--paper-2);
  white-space: nowrap;
}

.pts .star {
  color: var(--accent);
  font-size: 12px;
}

/* The plaque carries the name, and no screen after it needs telling twice which game this is.
   Hidden rather than removed, because the wordmark is the child --topbar-h was measured FROM:
   keeping its box means the pin and the bar's natural height cannot quietly disagree, and the
   stage stays the same size even if the pin is ever relaxed. */
.wordmark {
  display: flex;
  align-items: center;
  gap: 14px;
  min-width: 0;
  visibility: hidden;
}

.live {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: rgba(15, 18, 24, 0.88);
  border: 1px solid rgba(18, 22, 30, 0.2);
  border-radius: 999px;
  padding: 5px 13px;
}

.live .dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #ff5d5d;
  animation: dm-live 1.4s infinite;
}

.live span {
  font-family: var(--display);
  font-weight: 800;
  font-size: 13px;
  color: var(--paper-2);
  letter-spacing: 1px;
}

.wordmark h1 {
  margin: 0;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(19px, 3vw, 30px);
  color: var(--accent);
  letter-spacing: 3px;
  text-shadow: 0 0 22px rgba(255, 106, 61, 0.45), 0 3px 0 var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.topbar-right {
  display: flex;
  gap: 8px;
  flex: 0 0 auto;
  margin-top: 4px; /* 10px of bar padding + 4 = the artboard's 14px. */
}

.ghost-btn {
  font: 800 14px/1 var(--body);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--paper-2);
  background: rgba(15, 18, 24, 0.88);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 999px;
  padding: 8px 14px;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}

.ghost-btn:hover {
  border-color: var(--accent);
  color: #fff;
}

/* Each control becomes a disc of the same glass. 36 and not the artboard's `width: 34px`, for the
   same reason the plaque is 630 and not 560: the canvas those literals were measured on has no
   box-sizing reset, so 34 is the content square and the 1px ring is drawn outside it. */
.topbar .ghost-btn {
  width: 36px;
  height: 36px;
  padding: 0;
  gap: 0;
  justify-content: center;
  border-radius: 50%;
  font-size: 14px;
  background: rgba(12, 15, 20, 0.5);
  border: 1px solid rgba(255, 255, 255, 0.16);
  backdrop-filter: blur(8px);
}

.topbar .btn-word {
  display: none;
}

/* The account disc shows the handle's INITIAL. A person glyph identical to the sign-in button's
   would say "an account", not WHICH account, and the handle itself does not fit in a disc. */
.btn-initial {
  display: none;
}

#account-btn .btn-glyph {
  display: none;
}

#account-btn .btn-initial {
  display: block;
  font-family: var(--display);
  font-weight: 800;
  font-size: 14px;
  color: var(--paper-2);
}

/* Inside a cream card the ghost button flips to dark-on-cream. */
.screen .ghost-btn {
  color: var(--muted);
  background: transparent;
  border: 1.5px dashed rgba(18, 22, 30, 0.3);
  border-radius: 12px;
  font-family: var(--display);
  font-weight: 800;
  letter-spacing: 1px;
  font-size: 17px;
  padding: 11px 16px;
}

.screen .ghost-btn:hover {
  border-color: var(--ink);
  color: var(--ink);
}

/* Every glyph in this bar is DRAWN and inherits the button's own ink. An emoji is a different
   picture on every platform and a colour photograph on some, which is a second art direction inside
   a game that has one (lessons.md). */
.btn-glyph svg {
  display: block;
}

/* The mute carries both cones and paints whichever `aria-pressed` says — the same one-source rule
   the fullscreen toggle below uses, and the reason setMuted() writes a flag rather than a picture. */
#mute-btn .mute-off,
#mute-btn[aria-pressed="true"] .mute-on {
  display: none;
}

#mute-btn[aria-pressed="true"] .mute-off {
  display: inline;
}

/* The account chip's own drawn person, hidden by the rule above it on the room screen where the
   handle's initial takes the disc. */
.person-icon,
.ts-icon {
  display: block;
}

/* Inside the account menu and the plaque's dashed buttons the glyph rides the text baseline
   rather than being the whole control. */
.menu-icon,
.masks-icon {
  vertical-align: -2px;
  margin-right: 6px;
}

/* The fullscreen toggle carries both glyphs and paints whichever `aria-pressed` says — one source
   for the icon and for the state it reports, so they cannot drift. */
.fs-icon {
  display: block;
}

#fullscreen-btn .btn-glyph {
  display: flex;
}

#fullscreen-btn .fs-exit,
#fullscreen-btn[aria-pressed="true"] .fs-enter {
  display: none;
}

#fullscreen-btn[aria-pressed="true"] .fs-exit {
  display: inline;
}

.back-link {
  font: 900 14px var(--body);
  color: var(--muted);
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  align-self: flex-start;
}

.back-link:hover {
  color: var(--ink);
}

/* ------------------------------------------------------------------ stage */

.stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: linear-gradient(180deg, var(--stage-top) 0%, var(--stage-mid) 46%, var(--stage-bot) 100%);

  /* THE FIGURE SCALE, and the one number the conversation's floor is derived from.
   *
   * The design drew the debaters at 44% of the stage and pinned the speech columns at bottom 50%,
   * one point above the 49% their heads reach. Those were two independent literals that happened
   * to agree; growing the cast on a wide screen (there is room, and at 44% they read small in a
   * painted room) would have walked the characters straight up into the conversation. So the
   * columns' floor is now COMPUTED from this, and the pair cannot drift.
   *
   * JS overrides both of these INLINE: main.js measures the stage and the HUD and writes the
   * largest scale that actually fits, up to 56 (castScale in layout.js — a media query can only
   * guess at fit, and its guess called a zoomed ~506px-wide desktop window a phone). This pair
   * and the wide-screen bump below are the no-JS fallback, and the derived floor above keeps the
   * conversation off the characters at WHATEVER value wins.
   *
   * Unitless so it can be used in arithmetic; multiplied by 1% at each use. */
  --podium-h: 44;
  --bench-h: 27.1;

  /* Where the speech columns start. JS measures the HUD and writes --hist-top; this is the
   * no-JS fallback, and the short-viewport overrides below are the same guess at smaller sizes —
   * all three of them were only ever proxies for "just under the HUD", which is what the
   * measurement gives directly. */
  --hist-top-default: 24%;
}

/* THE ROOM, full-bleed behind everything.
 *
 * This used to be a BAND across the top with the UI stacked underneath it, which put the
 * conversation BELOW the characters — the design has it the other way up, and the difference is
 * the whole read of the screen. In the design the stage fills the canvas, the debaters stand at
 * the bottom of it, and the HUD, the speech histories and the input card float over the top of
 * them. Every number below is the design's own, expressed as a fraction of the stage so it
 * survives a viewport that is not 1280x800.
 *
 * The design's vertical map, in an 800px canvas:
 *   HUD          top 64px                     = 8%
 *   histories    top 216px .. bottom 50%      = 27% .. 50%
 *   audience     bottom 69% / 61% / 52%       (nearest row lowest and largest)
 *   bench        bottom 37%, 290x217
 *   debaters     bottom 5%, 230x352
 *   floor        bottom 0, height 27%
 *   input card   bottom 22px                  = 2.75%
 */
.set {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

/* ------------------------------------------------- the painted rooms (one per difficulty) */

/* The room image is the stage's bottom layer — a sibling of `.set` rather than a child of it, so
 * the start screen can bleed it up behind a transparent top bar without asking `.set` to stop
 * clipping (it clips the rig svgs, which overflow their figure boxes deliberately).
 * Bottom-anchored cover: a viewport wider than the art's 3:2 crops vertically, and the crop must
 * eat the wall, never the floor line the figures stand on. */
/* THE PICTURE BLEEDS UP behind the transparent bar, and only the picture does: it grows by exactly
 * the bar's height while the stage and `.set` are untouched, so every painted anchor the cast
 * stands on stays where it was. Both halves or it MOVES instead of growing — and the cast would go
 * with it. A sibling of `.set` and never a child: `.set` clips, and it has to keep clipping. */
.stage > .room {
  position: absolute;
  top: calc(-1 * var(--topbar-h));
  right: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  height: calc(100% + var(--topbar-h));
  object-fit: cover;
  object-position: 50% 100%;
  pointer-events: none;
}

/* The artboard's wash over the room: dark at the very top so the glass chrome has something to
   sit against, lifting through the middle, dark again at the floor. It cannot live on `.set`
   (both its pseudo-elements are the light cones, and a painted room hides them), so it hangs off
   the stage — and it borrows the room's OWN bleed for its top edge, so the two cannot come apart:
   whatever the picture covers, the wash covers. z-index 6 puts it over `.set` and the whole cast
   and under both screen layers (7 and 8), which is where it sat when it belonged to one screen.
   Inert, so it dims the room and not the buttons standing on it. */
.stage::before {
  content: "";
  position: absolute;
  top: calc(-1 * var(--topbar-h));
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 6;
  background: linear-gradient(
    180deg,
    rgba(10, 12, 18, 0.18),
    rgba(10, 12, 18, 0.04) 45%,
    rgba(10, 12, 18, 0.14)
  );
  pointer-events: none;
}

/* While a painted room is live, the stand-ins it replaces step aside: the light cones, the
 * silhouette audience (tuned 6-11 RGB points off the DARK stage — over a lit wall they would
 * float mid-air), the gradient floor band, and the flat desk/bench boxes with their mic bars.
 * The badges and the nameplate are UI, not furniture, and stay on top of the painted pieces. */
body[data-room] .set::before,
body[data-room] .set::after,
body[data-room] .audience,
body[data-room] .floor,
body[data-room] .desk .mic {
  display: none;
}

body[data-room] .desk {
  background: none;
  border: none;
  clip-path: none;
}

body[data-room] .desk--bench {
  background: none;
  border-top: none;
}

/* On a painted bench the plate no longer has the flat desk box behind it, and every bench front
 * carries an emblem exactly where the plate sits — back it with its own quiet pill so the name
 * stays readable over gilded marble as over oak. */
body[data-room] .nameplate {
  background: rgba(18, 22, 30, 0.55);
  border-radius: 6px;
  padding: 2px 10px;
}

/* The lectern: bottom-anchored in the podium, tall enough that a speaker could rest their forearms
 * on it. DOM order stacks it above the figure and below the badge.
 *
 * The height is DERIVED, not chosen. The first attempt (51.1%, inherited from the flat desk box it
 * replaced) put the top edge at art-y ~990 on every debater — past the elbow, i.e. squarely at the
 * waist, which reads as a speaker standing behind a box rather than one leaning on a lectern.
 * The anchor that matters is the body: `cast/rig.json` carries each character's shoulder and elbow,
 * and "just below the chest" is ~70% of the way from one to the other (art-y 779..811 across the
 * four debaters = 59.5%..61.9% of the podium — they cluster, so one number serves all four).
 *
 * Then the offset nobody sees coming: each lectern PNG's top edge is the tip of its gooseneck mic,
 * and the reading surface starts 8.5-11.1% further down (measured per file). So the surface lands
 * at 0.9 x this height — 68% puts it at 60.5-62.2%, inside the target band, with the mic rising in
 * front of the chest the way a real one does. Re-derive both numbers if the art is re-rolled;
 * test/debatecourt-rooms.test.js asserts the result against the rig's own landmarks. */
.podium .lectern {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: 0;
  height: 68%;
  width: auto;
  max-width: 94%;
  /* height + max-width on a replaced element is a SQUASH if the cap ever binds (it does not at
     today's aspects — the widest lands at 81% — but a re-roll could). contain letterboxes instead
     of distorting, which is a visible-but-honest failure rather than a subtly wrong one. */
  object-fit: contain;
}

/* The bench front, sized off the JUDGE the way the lecterns are sized off the debaters — she is
 * the reason it exists, and a bench that stops lower than their pults reads as an oversight.
 *
 * It used to be width-driven (`width: 100%` of the bench box, height auto), which meant the art's
 * own aspect decided where it crossed her body: 37-39% of the box, art-y 1008-1042, below her
 * elbow at 924 — her waist. `max-height: 44%` never bound, so it looked like a considered cap and
 * capped nothing. Height-driven off the same landmark as the lecterns (70% of the way from her
 * shoulder at 493 to her elbow at 924 = art-y 795) puts it at 51.5%. These bench fronts have no
 * mic, so unlike the lecterns the image's top edge IS the surface — no offset to pay.
 *
 * The width follows to ~131-138% of the design's bench box. That is deliberate: the box is a
 * 290x217 slot from a design that drew a much narrower bench, and a real judge's bench is wide.
 * `.set` clips, so on a narrow viewport the decorative ends run off rather than shrinking it. */
.bench .bench-front {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: 0;
  height: 51.5%;
  width: auto;
  object-fit: contain;
  /* A contact shadow, so the bench reads as RESTING on the floor rather than pasted over it. The
     rooms light from the upper left, so it falls down and slightly right. This also covers the few
     pixels of daylight left under the bench on a very tall viewport, where the room art carries
     its junction lowest. */
  filter: drop-shadow(3px 6px 9px rgba(0, 0, 0, 0.45));
}

/* The nameplate sits ON the bench's top edge, the way a real one stands on the desk — not centred
 * on the front panel. That is not only more honest furniture, it is the only band that survives a
 * narrow viewport: the podiums paint OVER the bench (correctly — the debaters stand in front of
 * it), and on a tall phone the podium band is so large a fraction of the stage that anything
 * inside the panel lands behind a shoulder and reads "JU…DGE HONORIA". The rail clears their
 * heads at every width. Box matches the panel, plate pinned to its top, then lifted half its own
 * height so it straddles the edge. */
body[data-room] .desk--bench {
  height: 51.5%;
  align-items: flex-start;
}

/* A PAINTED room has a real floor, and the bench has to stand ON it.
 *
 * The design's 37% is right for the CSS set above (its floor is a 27% gradient band and the bench
 * reads as set back on a dark studio stage). Over painted art it was wrong in a way that shows:
 * every room carries its wall/floor junction at 25-28% of the ARTWORK height, and `object-fit:
 * cover` only ever raises that on screen (a stage taller than the art's 3:2 shows the full height,
 * a wider one crops and lifts it) — so the junction lands at 30-45% of the stage. At 37% the
 * bench's bottom edge sat above it, pinned to the wall with floor visible underneath: "floating
 * weirdly in space".
 *
 * 30% clears the junction at the design aspect and wider. On a very tall phone the lowest room
 * (25.3%) can still leave a few pixels, which the contact shadow covers. Lowering the BOX moves the
 * judge and her bench together, so the bench-crosses-her-below-the-chest geometry is untouched. */
body[data-room] .bench {
  bottom: 30%;
}

body[data-room] .nameplate {
  transform: translateY(-50%);
}

/* Below ~0.85 the debaters swallow the middle of the bench, so the plate cannot be read at all.
 *
 * The podiums are sized off the stage HEIGHT (0.653 x 44% = 28.7% of it each) while the stage's
 * width is its own thing, so on a tall narrow viewport two debaters plus their 8% insets leave a
 * centre gap of 0.84w - 0.574h — which goes negative around w/h = 0.85 and takes the nameplate
 * with it. It reads as "JU...DGE HONORIA" behind a shoulder, which is worse than absent, and there
 * is nowhere else on the bench to put it: at that aspect the debaters cover everything but a ~40px
 * slot. So drop it, and let the verdict screen carry her name.
 *
 * Gated on the aspect that causes it rather than on "phone": a short desktop window hits the same
 * geometry, and a wide tablet never does. */
@media (max-aspect-ratio: 17 / 20) {
  body[data-room] .nameplate {
    display: none;
  }
}

/* Two stage lights raking down from the rig — the design's clip-path cones. */
.set::before,
.set::after {
  content: "";
  position: absolute;
  top: -6%;
  width: 360px;
  height: 74%;
  background: linear-gradient(rgba(255, 140, 90, 0.09), rgba(255, 209, 102, 0) 72%);
  clip-path: polygon(42% 0, 58% 0, 100% 100%, 0 100%);
  pointer-events: none;
}

.set::before {
  left: 8%;
  transform: rotate(10deg);
}

.set::after {
  right: 8%;
  transform: rotate(-10deg);
}

/* Each row sits at its own height, nearest row lowest and largest. */
.audience-row {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--y);
  display: flex;
  justify-content: center;
  pointer-events: none;
}

/* Head and shoulders, sized per row so the crowd has depth. Drawn with pseudo-elements because a
   fan is two shapes and no text — the markup only needs to carry the numbers. */
.audience .fan {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.audience .fan::before {
  content: "";
  width: var(--hw);
  height: var(--hh);
  border-radius: 50% 50% 42% 42%;
  background: var(--c);
}

.audience .fan::after {
  content: "";
  width: var(--sw);
  height: 12px;
  border-radius: 10px 10px 0 0;
  background: var(--c);
  margin-top: -3px;
}

.floor {
  position: absolute;
  inset: auto 0 0 0;
  height: 27%;
  background: var(--floor);
  border-top: 1px solid rgba(0, 0, 0, 0.5);
  pointer-events: none;
}

/* The bench: 290x217 in the design, so its width follows its height and the judge keeps the
   design's 0.690 figure-to-furniture ratio automatically. */
/* `--bench-drop` is how far the judge steps forward so the start screen's plaque does not cut her
 * head off. Written by main.js from the measured card (benchDrop in layout.js); 0 on every screen
 * that has no card hanging over her, which is all of them but that one. A TRANSFORM and not a
 * changed `bottom`, so the box every other rule in this file is written against never moves. */
.bench {
  position: absolute;
  left: 50%;
  transform: translateX(-50%) translateY(var(--bench-drop, 0px));
  bottom: 37%;
  height: calc(var(--bench-h) * 1%);
  aspect-ratio: 290 / 217;
  z-index: 2;
}

.podiums {
  position: absolute;
  inset: auto 0 5% 0;
  height: calc(var(--podium-h) * 1%);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  padding: 0 clamp(6px, 8%, 140px);
  z-index: 3;
}

/* 230x352 in the design. Deriving the width from the height is what keeps the debater at the
   design's 0.957 of their lectern at every viewport size. */
.podium {
  position: relative;
  height: 100%;
  aspect-ratio: 230 / 352;
}

/* NO-JS FALLBACK ONLY: with JS running, main.js writes the fit-derived scale inline (castScale in
 * layout.js) and inline beats both this block and the base pair above. This media gate is the
 * guess castScale replaced — its shape was wrong for real windows (a zoomed desktop browser at
 * ~506x705 failed all three tests and got the phone scale) — but without JS a guess is all there
 * is, so it stays: gated on width, height AND aspect because each dimension has a viewport that
 * must not grow (a tall phone crowds its middle; a 1280x475 window has no vertical room and
 * growing the cast there collapsed the reading band to 10.7px — a fifth of a line). */
@media (min-width: 900px) and (min-height: 700px) and (min-aspect-ratio: 5 / 4) {
  .stage {
    /* 48, not castScale's 56 ceiling: without JS nothing measures the real HUD edge, so the
       reading-band cost of a bigger cast cannot be checked here — 48 was measured safe at
       1280x800 (44% -> 38% of an ~900-character turn visible at once) and larger was not. */
    --podium-h: 48;
    --bench-h: 30;
  }
}

.figure {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 27.3%; /* the design's 96px of 352 */
  height: 75.3%; /* 265 of 352 */
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.figure svg {
  display: block;
  height: 100%;
  width: auto;
  max-width: 100%;
}

.figure--judge {
  position: absolute;
  inset: 0 0 auto 0;
  height: 85.3%; /* 185 of 217 */
  bottom: auto;
}

/* The debater's lectern: a trapezoid, an accent stripe, and the side badge. */
.desk {
  position: absolute;
  inset: auto 0 0 0;
  height: 36.9%; /* 130 of 352 */
  background: var(--desk);
  border: 1px solid rgba(18, 22, 30, 0.14);
  border-radius: 10px;
  clip-path: polygon(7% 0, 93% 0, 100% 100%, 0 100%);
}

.desk--bench {
  position: absolute;
  inset: auto 0 0 0;
  height: 30.4%; /* 66 of 217 */
  background: var(--bench);
  border-radius: 12px;
  border-top: 6px solid var(--accent);
  clip-path: none;
  display: flex;
  align-items: center;
  justify-content: center;
}

.desk .mic {
  position: absolute;
  top: 22%;
  left: 50%;
  transform: translateX(-50%);
  width: 52%;
  height: 8px;
  background: var(--accent);
  border: 1px solid rgba(18, 22, 30, 0.2);
  border-radius: 6px;
}

.nameplate {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(9px, 1.3vw, 17px);
  color: var(--accent);
  letter-spacing: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 0 6px;
}

/* The side badge, on the front face of the lectern. */
.badge {
  position: absolute;
  left: 50%;
  bottom: 26%;
  transform: translateX(-50%);
  border-radius: 9px;
  padding: 4px 14px;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(10px, 1.2vw, 15px);
  color: #fff;
  letter-spacing: 0.5px;
  white-space: nowrap;
  /* The design's NEU: a cool slate, not the warm grey of body text. */
  background: #58616e;
}

/* The BADGE takes the design's sideC values, which are deliberately not the shirt's gradient
   stops — the shirt is a lit garment, the badge is flat ink. */
.badge--pro { background: #2fae6e; }
.badge--con { background: #e0616b; }

/* ---------------------------------------------------------------- screens */

.screen {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* A menu screen is a cream card floating OVER the stage, centred on it — the design's card layer is
 * `position:absolute; inset:0; …; z-index:8`, i.e. it overlaps the room rather than sitting under
 * it. Laying it out in flow below the set cost 320px of height and clipped three screens: the
 * topics list lost its fifth topic and its button, and the side and ready cards had their bottom
 * corners cut off.
 *
 * `margin: auto` on the card rather than `align-items: center` on purpose: a centred flex item
 * taller than its container gets its TOP cut off and cannot be scrolled to, and the verdict card is
 * routinely taller than the viewport. With auto margins it centres when it fits and scrolls when it
 * does not.
 *
 * The DEBATE screen is deliberately not in this class — it stays in flow, because that is what
 * keeps the text box above a phone keyboard. */
.screen--card {
  position: absolute;
  inset: 0;
  z-index: 8;
  display: flex;
  overflow-y: auto;
  /* A card that scrolls must not hand the leftover gesture to the document — see html/body. */
  overscroll-behavior: contain;
  padding: 16px clamp(10px, 4vw, 28px);
}

.screen--card .card {
  margin: auto;
}

/* EVERY card is a plaque. The treatment is the start screen's — design project, file
 * `Start Screen Ideas.dc.html`, artboard 1d — and it lives here rather than on `.card--plaque`
 * because a game whose title screen is engraved paper and whose next screen is a plain white sheet
 * reads as two different games. Warm paper instead of cream, the courtroom's gold instead of a
 * grey hairline, a tighter 14px corner, and a second rule engraved 6px inside the first.
 *
 * `position: relative` is for that engraving and nothing else — the only absolutely positioned
 * thing inside any card is the verdict bubble's tail, which anchors to its own paragraph. */
.card {
  position: relative;
  width: min(600px, 100%);
  background: linear-gradient(180deg, #f8f1e3, #efe3cb);
  border: 1px solid rgba(196, 136, 42, 0.5);
  border-radius: 14px;
  padding: clamp(20px, 4vw, 34px) clamp(18px, 5vw, 44px);
  box-shadow: var(--card-shadow);
  text-align: center;
  animation: dm-pop 0.35s ease-out;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* The engraving: a second rule inside the first, the way a brass or painted plaque is
   double-bordered. Inert, and a pseudo-element so it cannot become a flex item of the column it
   decorates. */
.card::after {
  content: "";
  position: absolute;
  inset: 6px;
  border: 1px solid rgba(196, 136, 42, 0.3);
  border-radius: 9px;
  pointer-events: none;
}

.card--wide {
  width: min(700px, 100%);
}

/* The board is narrower than the verdict it can be opened from: 650 less the 44px padding pair
   and the 1px border pair is the design's 560px of content, which is what the podium and the rows
   are drawn against. The padding itself stays clamped — that part is this file's, not the
   design's, and it is what makes the card fit a phone. */
.card--board {
  width: min(650px, 100%);
}

/* The design hands one card to the next instead of cutting between them: the outgoing card plays
   `popOut .17s ease-in forwards` and only then does the next pop in. Both classes are owned by
   show(), which clears them on one 170ms timer — the same number as the animation, in one place
   in each file. The incoming section is un-hidden IMMEDIATELY even so, because every caller paints
   into it the moment show() returns and a display:none section measures zero; it holds its own
   start frame through the handover with `backwards`, and stays untouchable while it is invisible
   so a fast second click cannot land on a card nobody can see.

   Card screens only. The debate screen is in the stage's flow rather than floating over it, and
   holding it an extra beat would leave two layouts on screen at once. */
.screen--card.is-leaving {
  pointer-events: none;
}

.screen--card.is-leaving .card {
  animation: dm-pop-out 0.17s ease-in forwards;
}

.screen--card.is-entering {
  pointer-events: none;
}

.screen--card.is-entering .card {
  animation-delay: 0.17s;
  animation-fill-mode: backwards;
}

/* The design sizes each screen's own heading; one clamp for all of them shouted every screen at
   the hero's volume. Only the hero carries negative tracking. */
.card h2 {
  margin: 0;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(20px, 3.2vw, 26px);
  color: var(--ink);
  line-height: 1.15;
}

#screen-category .card h2 { font-size: clamp(24px, 4vw, 34px); }
#screen-topic .card h2 { font-size: clamp(23px, 3.8vw, 32px); }
#screen-deliberation .card h2 { font-size: clamp(22px, 3.6vw, 30px); }
/* 32px and -.5px tracking: the design's LEADERBOARD title. The 50px .hero below it is the main
   menu's, and wearing it here shouted the board's heading half again as loud as drawn. */
#screen-leaderboard .card h2 { font-size: clamp(23px, 3.8vw, 32px); letter-spacing: -0.5px; }

.card h2.hero {
  font-size: clamp(32px, 6vw, 50px);
  letter-spacing: -1px;
}

.lede {
  margin: 0;
  font-weight: 800;
  color: var(--muted-2);
  font-size: 16px;
}

/* The topic, quoted, in its warm frame — the design uses this on side-select and ready. */
.proposition {
  margin: 0;
  background: var(--accent-wash);
  border: 1px solid rgba(230, 90, 40, 0.35);
  border-radius: 12px;
  padding: 12px 18px;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(17px, 2.6vw, 22px);
  color: var(--ink);
  line-height: 1.25;
}

.row {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: center;
}

.row--split {
  justify-content: space-between;
  align-items: center;
}

/* Buttons: the design gives everything a lift on hover and a real press on active. */
.big-btn {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(17px, 2.4vw, 22px);
  letter-spacing: 1px;
  color: #fff;
  background: var(--accent);
  border: none;
  border-radius: 16px;
  padding: 13px 26px;
  cursor: pointer;
  /* Stated rather than inherited from <button>, because this class is also worn by an <a>: the
     rank-home card's action is a real navigation to another domain, and a link is what that is. An
     inline <a> ignores vertical padding for layout purposes, so the pill would overlap its
     neighbours. Harmless on the button form, which is inline-block anyway. */
  display: inline-block;
  text-decoration: none;
  text-align: center;
  box-shadow: var(--press-shadow);
  transition: transform 0.08s ease, box-shadow 0.08s ease, background 0.15s ease;
}

.big-btn:hover {
  transform: translateY(-3px);
  background: var(--accent-hi);
}

.big-btn:active {
  transform: translateY(3px);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}

.big-btn--plain {
  color: var(--ink);
  background: #fff;
  border: 1.5px solid rgba(18, 22, 30, 0.25);
}

.big-btn--plain:hover {
  background: #fff;
}

.big-btn--go {
  font-size: clamp(20px, 3.4vw, 28px);
  letter-spacing: 2px;
  padding: 16px 44px;
  border-radius: 18px;
  animation: dm-pulse 1.6s infinite;
}

.big-btn--go:hover {
  animation: none;
}

/* Category tiles. */
.cats {
  display: flex;
  gap: 14px;
  justify-content: center;
  flex-wrap: wrap;
}

/* Four sources read as a deliberate 2×2 chooser, not the old three-up row with one orphan. */
.cats.has-weekly-claims {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  width: min(390px, 100%);
  margin-inline: auto;
}

.cats.has-weekly-claims .cat {
  width: auto;
  max-width: none;
}

.cat {
  width: 164px;
  flex: 1 1 140px;
  max-width: 180px;
  background: #fff;
  border: 1px solid rgba(18, 22, 30, 0.14);
  border-radius: 18px;
  padding: 18px 12px;
  cursor: pointer;
  box-shadow: var(--press-shadow);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  transition: transform 0.1s ease, background 0.15s ease, box-shadow 0.1s ease;
}

.cat:hover {
  transform: translateY(-4px);
}

.cat:active {
  transform: translateY(2px);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}

.cat[data-cat="politics"]:hover { background: #fff3d6; }
.cat[data-cat="science"]:hover { background: #e4fbf1; }
.cat[data-cat="philosophy"]:hover { background: #ffe9ee; }
.cat[data-cat="weekly-claims"]:hover { background: #fff3d6; }

.cat-name {
  font-family: var(--display);
  font-weight: 800;
  font-size: 21px;
  color: var(--ink);
}

.cat-sub {
  font-size: 12px;
  font-weight: 800;
  color: var(--muted);
}

/* Opponent select — same card language as the categories, with a portrait on top. */
.opp-cards {
  display: flex;
  gap: 14px;
  justify-content: center;
  flex-wrap: wrap;
}

.opp-card {
  width: 172px;
  flex: 1 1 150px;
  max-width: 190px;
  background: #fff;
  border: 1px solid rgba(18, 22, 30, 0.14);
  border-radius: 18px;
  padding: 14px 12px;
  cursor: pointer;
  box-shadow: var(--press-shadow);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  transition: transform 0.1s ease, background 0.15s ease, box-shadow 0.1s ease;
}

.opp-card:hover {
  transform: translateY(-4px);
}

.opp-card:active {
  transform: translateY(2px);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}

/* The remembered pick. Drawn INSIDE the border box (negative offset), so the ring can never be
 * shaved by a scrolling ancestor's clip (lessons.md). */
.opp-card.is-on {
  outline: 3px solid #e8a93c;
  outline-offset: -3px;
}

.opp-figure {
  height: 118px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

/* Fit by the constrained dimension: height-driven, width follows the art's own aspect —
 * width:100% + max-height is a squash, not a fit (lessons.md). */
.opp-figure svg {
  display: block;
  height: 100%;
  width: auto;
  max-width: 100%;
}

.opp-name {
  font-family: var(--display);
  font-weight: 800;
  font-size: 19px;
  color: var(--ink);
}

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

.opp-level {
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: #fff;
  border-radius: 999px;
  padding: 3px 9px;
}

.opp-level--easy { background: var(--pro, #2fae6e); }
.opp-level--medium { background: #e8a93c; }
.opp-level--hard { background: var(--con, #e0616b); }

.opp-mult {
  font-size: 12px;
  font-weight: 900;
  color: var(--muted);
}

.opp-tagline {
  font-size: 12px;
  font-weight: 700;
  line-height: 1.35;
  color: var(--muted);
}

/* Topic list. */
.topic-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.topic-btn {
  font: 800 16px/1.35 var(--body);
  width: 100%;
  text-align: left;
  background: #fff;
  border: 1px solid rgba(18, 22, 30, 0.2);
  border-radius: 12px;
  padding: 12px 16px;
  cursor: pointer;
  color: var(--ink);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.22);
  transition: transform 0.1s ease, background 0.15s ease;
}

.topic-btn:hover {
  transform: translateX(4px);
  background: #fff3d6;
}

.topic-btn:active {
  transform: translateY(2px);
}

/* Side select. */
.side-btn {
  width: 190px;
  flex: 1 1 150px;
  max-width: 220px;
  border-radius: 18px;
  padding: 16px 12px;
  cursor: pointer;
  box-shadow: var(--press-shadow);
  transition: transform 0.1s ease, box-shadow 0.1s ease;
}

.side-btn:hover { transform: translateY(-4px); }
.side-btn:active { transform: translateY(2px); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); }

.side-btn strong {
  display: block;
  font-family: var(--display);
  font-weight: 800;
  font-size: 28px;
}

.side-btn span {
  display: block;
  font-weight: 900;
  font-size: 13px;
}

.side-btn--pro { background: var(--pro-wash); border: 1px solid rgba(31, 164, 99, 0.45); }
.side-btn--pro strong { color: var(--pro); }
.side-btn--pro span { color: var(--pro-ink); }
.side-btn--con { background: var(--con-wash); border: 1px solid rgba(216, 67, 78, 0.45); }
.side-btn--con strong { color: var(--con); }
.side-btn--con span { color: var(--con-ink); }

/* Ready: the versus strip and the running order. */
.versus {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  flex-wrap: wrap;
}

.vs-name {
  border-radius: 10px;
  padding: 8px 18px;
  font-family: var(--display);
  font-weight: 800;
  font-size: 16px;
  color: #fff;
  white-space: nowrap;
  background: var(--muted-2);
}

.vs-name--pro { background: var(--pro); }
.vs-name--con { background: var(--con); }

/* Who was picked and what beating them pays, with the way back to change it. */
.ready-opp {
  margin: 2px 0 0;
  font-size: 13px;
  font-weight: 800;
  color: var(--muted);
  text-align: center;
}

.vs-word {
  font-family: var(--display);
  font-weight: 800;
  font-size: 24px;
  color: var(--muted);
}

.running-order {
  display: flex;
  justify-content: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 0;
  padding: 0;
  list-style: none;
  font-weight: 800;
  font-size: 13px;
  color: var(--muted-2);
}

/* No per-turn highlight: the design paints all four the same, and the YOU · VS · <opponent> strip
   directly above already says which side you are. */

/* ------------------------------------------------------------- the debate */

/* The debate UI floats OVER the room, PINNED the way the design pins it.
 *
 * This was a four-row grid, and that was wrong in a way that only showed up in the live game:
 *   * rows 2 and 3 shared the LEFTOVER space, so swapping the 163px input card for the 42px
 *     "Max is speaking" pill handed the columns 60px more height — the conversation reflowed
 *     twice a round and, on the opponent's turn, its bottom edge dropped 5.6% INTO the debaters;
 *   * `.compose` and `.waiting` were both `grid-row: 4` with no column, so the moment both were
 *     on screen at once — which is exactly what the moderation round-trip does on every real
 *     submission — auto-placement invented a second column and shunted the entire UI 115px
 *     sideways for the duration.
 * Both are the same mistake: deriving a fixed anchor from variable content. The design pins the
 * columns at `bottom: 50%` and the card at `bottom: 22px`, so neither can move. So does this now.
 */
.screen--debate {
  position: absolute;
  inset: 0;
  z-index: 7;
  pointer-events: none;
}

/* The room behind stays clickable-through; only the controls take input. */
.screen--debate > * {
  pointer-events: auto;
}

/* The HUD is a cream card floating over the stage: phase, speaker, and a countdown ring. */
/* The design puts the HUD at top 64px of an 800px canvas. That 64px is already spent here by the
   top bar above the stage, so the HUD sits at the top of the stage rather than 8% down it. */
.hud {
  position: absolute;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  max-width: min(700px, 96%);
}

.hud-card {
  display: flex;
  align-items: center;
  gap: clamp(10px, 2vw, 18px);
  background: var(--paper);
  border: 1px solid rgba(18, 22, 30, 0.14);
  border-radius: 18px;
  padding: 9px 18px;
  box-shadow: var(--lift-shadow);
  animation: dm-pop 0.3s ease-out;
  max-width: 100%;
}

.hud-phase {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
}

.hud-round {
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 2px;
  color: var(--muted);
  white-space: nowrap;
}

.hud-title {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(17px, 3vw, 26px);
  color: var(--ink);
  letter-spacing: 1px;
  white-space: nowrap;
}

.hud-rule {
  width: 4px;
  height: 40px;
  background: var(--line);
  border-radius: 2px;
  flex: 0 0 auto;
}

.hud-speaker {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(14px, 2.4vw, 22px);
  letter-spacing: 1px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The countdown ring. The stroke-dashoffset is set from JS; 163.4 is 2*pi*26. */
.timer-ring {
  position: relative;
  width: 60px;
  height: 60px;
  flex: 0 0 auto;
}

.timer-ring svg {
  display: block;
  width: 100%;
  height: 100%;
}

.timer-ring .track {
  fill: none;
  stroke: var(--line);
  stroke-width: 8;
}

.timer-ring .sweep {
  fill: none;
  stroke: var(--pro-bar);
  stroke-width: 8;
  stroke-linecap: round;
  stroke-dasharray: 163.4;
  transition: stroke-dashoffset 0.25s linear, stroke 0.3s ease;
}

.timer-ring.is-low .sweep { stroke: var(--con); }

.timer-ring .value {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 900;
  font-size: 16px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.timer-ring.is-low .value { color: var(--con); }

/* Four dots: the turns, filled as they are spent. */
.phase-dots {
  display: flex;
  gap: 8px;
}

.phase-dots span {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(255, 255, 255, 0.12);
}

.phase-dots span.done { background: var(--muted); }
.phase-dots span.now { background: var(--accent); border-color: var(--accent); }

.hud-topic {
  margin: 0;
  background: rgba(15, 18, 24, 0.88);
  border: 1px solid rgba(18, 22, 30, 0.2);
  border-radius: 999px;
  padding: 5px 16px;
  color: var(--paper-2);
  font-weight: 800;
  font-size: 14px;
  /* The design's ceiling. Without it a long topic ran up to 140px wider before ellipsising. */
  max-width: min(560px, 100%);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Two speech histories, flanking, each scrolling on its own.
 *
 * The design's proportions verbatim: each column 31% wide with a 400px ceiling, pinned 3.5% from
 * its own edge, which leaves ~31% of stage showing between them. The player's turns are on the
 * player's side of the screen and the opponent's on theirs, so which column is whose is answered by
 * position — the same thing the podium under it says. */
/* Pinned to the design's own band: from below the HUD down to exactly `bottom: 50%`, which is one
 * percentage point above where the debaters' heads start (they occupy bottom 5% + 44% = 49%). That
 * arithmetic is what guarantees the conversation never lands on top of a character, at any
 * viewport and whatever the input card is doing.
 *
 * The 3.5% inset and 31% columns are GRID TRACKS, not padding plus a percentage of what padding
 * left over — that compounding had them at 5.24% and 27.75% while a comment claimed "verbatim". */
.histories {
  position: absolute;
  left: 0;
  right: 0;
  top: var(--hist-top, var(--hist-top-default));
  /* The figure is 27.3% + 75.3% = 102.6% of its podium box, so its head clears the box TOP by
     2.6% of the podium height — the design's flat "one point above 5 + 44" was measured against
     the box and so overlapped the hair by ~1px all along. Harmless at 44, but it scales: at 52 it
     is 2.5px and growing. Multiply the overshoot in and the clearance is real at every scale. */
  bottom: calc((5 + var(--podium-h) * 1.026 + 0.5) * 1%);
  z-index: 4;
  pointer-events: none;
  display: grid;
  /* --hist-col-cap is written by JS when the columns move up BESIDE the HUD (see trackHudEdge):
     it trims them just enough to clear the centred card. Unset it is a no-op. */
  grid-template-columns:
    3.5%
    minmax(0, min(31%, 400px, var(--hist-col-cap, 100%)))
    auto
    minmax(0, min(31%, 400px, var(--hist-col-cap, 100%)))
    3.5%;
  align-items: stretch;
}

.history--you {
  grid-column: 2;
}

.history--them {
  grid-column: 4;
}

.history {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  gap: 9px;
  padding: 4px;
}

.history--you {
  padding-right: 8px;
}

.history--them {
  padding-left: 8px;
}

/* Wider columns are how the bigger cast is paid for, and this block MUST live after the rule it
   overrides — it carries the same specificity, so source order is what decides. The extra height
   the debaters take comes straight off the conversation; a wider column fits the same words in
   fewer lines, and a wide screen has the horizontal room to give (the centre stays clear of the
   columns, and the bench sits below them either way). The JS-computed cast scale (layout.js) can
   also grow the cast at windows OUTSIDE this gate — there the band price is capped instead, by
   castScale's own 12%-and-90px reading-band floor. */
@media (min-width: 900px) and (min-height: 700px) and (min-aspect-ratio: 5 / 4) {
  .histories {
    grid-template-columns:
      3.5%
      minmax(0, min(33%, 460px, var(--hist-col-cap, 100%)))
      auto
      minmax(0, min(33%, 460px, var(--hist-col-cap, 100%)))
      3.5%;
  }
}

/* One turn. The tail corner points back at the speaker's own side. */
.bubble {
  background: var(--paper);
  border: 1px solid rgba(18, 22, 30, 0.14);
  border-radius: 14px 14px 14px 4px;
  padding: 9px 13px;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
  flex-shrink: 0;
  animation: dm-pop 0.25s ease-out;
}

.history--them .bubble {
  border-radius: 14px 14px 4px 14px;
  align-self: flex-end;
  width: calc(100% - 20px);
}

/* The turn in progress: the accent border is the only thing marking it, so a settled turn and a
   live one are never confusable. */
.bubble.is-live {
  border-color: rgba(255, 106, 61, 0.75);
}

/* A forfeited turn reads quieter than one that was argued — but NOT by going translucent. `opacity`
   fades the panel as well as the ink, and the 2026-09-10 bar exam photographed the result: a
   chandelier reading through the left column's text while the right column was an opaque cream
   panel, so the two columns did not read as the same object on the one screen whose whole task is
   reading. The quiet comes from the ink and the edge instead, and the paper stays paper. */
.bubble--forfeit {
  background: var(--paper-2);
  border-style: dashed;
  border-color: rgba(18, 22, 30, 0.26);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
}

.bubble h3 {
  margin: 0 0 3px;
  font-family: var(--body);
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 1.4px;
  color: var(--muted);
}

/* The header takes the SIDE's colour, not the speaker's — it is PRO and CON being scored. */
.bubble--pro h3 { color: var(--pro); }
.bubble--con h3 { color: var(--con); }

.bubble p {
  margin: 0;
  font-size: calc(13.5px * var(--prose));
  font-weight: 700;
  line-height: 1.45;
  color: var(--ink);
  white-space: pre-wrap;
}

.bubble.is-streaming .bubble-text::after {
  content: "▌";
  color: var(--accent);
  animation: dm-live 0.7s infinite;
}

.forfeit {
  color: var(--con);
  font-weight: 900;
}

/* The design's input card: 640px wide, 92% ceiling, pinned 22px off the bottom of an 800px canvas
   (2.75%). Pinned, so nothing it does can move the conversation above it. */
.compose {
  position: absolute;
  bottom: 2.75%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 7;
  pointer-events: auto;
  width: min(640px, 92%);
  background: var(--paper);
  border: 1px solid rgba(18, 22, 30, 0.14);
  border-radius: 18px;
  padding: 12px;
  box-shadow: var(--lift-shadow);
  animation: dm-pop 0.3s ease-out;
}

.compose.is-busy {
  opacity: 0.6;
  pointer-events: none;
}

.draft {
  font: 700 16px/1.4 var(--body);
  font-size: calc(16px * var(--prose));
  /* The box the words nobody wants to lose are typed into, and it scrolls once they overflow it —
     so it is the one place a flick past the end must not become a page refresh. */
  overscroll-behavior: contain;
  width: 100%;
  padding: 10px 12px;
  border-radius: 12px;
  border: 1px solid rgba(18, 22, 30, 0.2);
  background: #fff;
  color: var(--ink);
  resize: none;
  outline: none;
  /* Never grows past what is left of the screen: on a phone with the keyboard up this is the
     difference between seeing the End turn button and not. */
  height: 76px;
  max-height: 26dvh;
}

.draft:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(255, 106, 61, 0.25);
}

.compose-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
}

.hint {
  font-size: 13px;
  font-weight: 700;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.counter {
  font-variant-numeric: tabular-nums;
  font-weight: 800;
  font-size: 13px;
  color: var(--muted);
  white-space: nowrap;
}

.counter.is-full {
  color: var(--con);
  font-weight: 900;
}

.compose-row .big-btn {
  font-size: 19px;
  padding: 10px 24px;
  border-radius: 14px;
  margin-left: auto;
}

.reject {
  margin: 0 0 8px;
  padding: 9px 13px;
  border-radius: 12px;
  background: var(--con-wash);
  border: 1px solid rgba(216, 67, 78, 0.45);
  color: var(--con-ink);
  font-size: 14px;
  font-weight: 800;
}

/* "Mei Point is thinking" — the pill under the stage while the AI has the floor (named for whichever opponent was picked). */
.waiting {
  position: absolute;
  bottom: 3.75%; /* the design's 30px of 800 */
  /* CENTRED BY AUTO MARGINS, not by `left: 50%` + a translate. A shrink-to-fit absolute box offset
   * from the left is capped at the space remaining to its right — half the stage — so on a 360px
   * phone this pill was clamped to 180px and "Max Rebuttal is speaking" stacked three lines deep
   * inside it. Measured before this comment existed, so it is not the ready button's doing; the
   * button just made an 88px pill a 110px one. Between two zero-ish offsets with `width: fit-content`
   * the box sizes to its content and centres, and the same pill measures 273x43. */
  left: 12px;
  right: 12px;
  width: fit-content;
  max-width: calc(100% - 24px);
  z-index: 7;
  pointer-events: auto;
  /* `0 auto`, and not the `0` this was: the auto pair is what centres the box between the two
     offsets above, and a later blanket `margin: 0` would quietly undo it. */
  margin: 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  align-self: center;
  background: var(--paper);
  border: 1px solid rgba(18, 22, 30, 0.14);
  border-radius: 999px;
  padding: 9px 22px;
  box-shadow: var(--lift-shadow);
  font-weight: 900;
  font-size: 15px;
  color: var(--ink);
}

/* The button that ends the opponent's turn, sized down to sit inside the pill rather than beside
   it — it is the same `.big-btn--go` the Start button wears, so "this is the thing to press" reads
   the same way in both places, but a full-size one would make the pill taller than the compose card
   it replaces and shift the whole reading column. Negative margins pull the pill's own padding back
   on the button's side so the pill does not grow a second inset around it. */
#btn-ready {
  font-size: 15px;
  padding: 7px 16px;
  margin: -3px -12px -3px 0;
}

.dots {
  display: inline-flex;
  gap: 5px;
}

.dots i {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--con-bar);
  animation: dm-dot 0.7s infinite;
}

.dots i:nth-child(2) { animation-delay: 0.15s; }
.dots i:nth-child(3) { animation-delay: 0.3s; }

/* The deliberation card's scales of justice, tipping slowly while the judge works. The beam
   rotates about the pivot and each pan counter-rotates by the same angle on its hang point, so
   the pans stay level the way real hanging pans do. `transform-box: view-box` pins the px
   origins to the svg's own coordinate grid; the global reduced-motion collapse above freezes
   this like every other decoration. */
.delib-scales {
  display: flex;
  justify-content: center;
}

.delib-scales svg {
  width: 108px;
  height: auto;
  fill: var(--gold);
  stroke: var(--gold-lo);
  stroke-width: 2;
}

.delib-scales__beam,
.delib-scales__pan {
  transform-box: view-box;
  animation: dm-weigh 3s ease-in-out infinite;
}

.delib-scales__beam { transform-origin: 60px 15px; }
.delib-scales__pan { animation-name: dm-weigh-counter; }
.delib-scales__pan--l { transform-origin: 18px 15px; }
.delib-scales__pan--r { transform-origin: 102px 15px; }

@keyframes dm-weigh {
  0%, 100% { transform: rotate(-7deg); }
  50% { transform: rotate(7deg); }
}

@keyframes dm-weigh-counter {
  0%, 100% { transform: rotate(7deg); }
  50% { transform: rotate(-7deg); }
}

/* Each status line arrives fresh: the swap rebuilds the span, which is what restarts this. */
#deliberation-note .delib-line {
  display: inline-block;
  animation: dm-line-in 0.45s ease both;
}

@keyframes dm-line-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

/* THE CRITERIA STRIP. The same four labels the scorecard's own rows wear — same weight, same
   tracking, same caps — so a player meets the words on the composer and recognises the bars when
   the verdict arrives. Two mounts, one painter (howto.js `criteriaStripHtml`): beside the composer
   while they write, and on the deliberation card while she scores. */
.crit-strip {
  margin: 0 0 8px;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 4px 6px;
}

.crit-chip {
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--accent-wash);
  border: 1px solid rgba(255, 106, 61, 0.22);
  font-weight: 900;
  font-size: 10.5px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--muted-2);
  white-space: nowrap;
}

/* WHAT THEY ARGUED, held still for the wait. The judge is the longest single wait in the game and
   it arrives just after the player has written several hundred words they can no longer see, so
   the card hands them back rather than filling the time with jokes alone. It scrolls inside its
   own box: the recovery row under it is the only way out of a stalled judge and must not be
   pushed off a short screen by a pair of 1,200-character openings. */
.delib-recall {
  display: grid;
  gap: 6px;
}

.delib-recall-head {
  margin: 0;
  font-family: var(--display);
  font-weight: 800;
  font-size: 12px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--muted);
}

.delib-recall-mine {
  display: grid;
  gap: 6px;
}

/* THE FACT-CHECKS, ARRIVING. One row per check that was fired: the speaker and the turn on the
   left, what came back on the right. They begin as "checking…" and fill in one at a time while she
   reads, which is the only thing that HAPPENS during the longest wait in the catalogue.

   Compact on purpose, and scrolling past four: this card already measured 776px at 375x812 with
   the recall block at its cap, and the recovery row beneath both is the only way out of a stalled
   judge. `.has-checks` below is what pays for these rows out of the recall's own height rather
   than out of the card's. */
.delib-checks {
  display: grid;
  gap: 6px;
}

.delib-check-list {
  margin: 0;
  padding: 6px 10px;
  list-style: none;
  display: grid;
  gap: 4px;
  max-height: min(16vh, 116px);
  overflow-y: auto;
  overscroll-behavior: contain;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.55);
  border: 1px solid rgba(18, 22, 30, 0.1);
  text-align: left;
}

.delib-check {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-size: 12.5px;
  font-weight: 700;
  line-height: 1.5;
}

.delib-check-who {
  font-family: var(--display);
  font-weight: 800;
  font-size: 11px;
  letter-spacing: 1px;
  color: var(--bronze-lo);
  white-space: nowrap;
}

.delib-check-line {
  color: var(--muted-2);
  text-align: right;
}

/* A row still waiting reads as waiting: the ellipsis pulses on the same beat the status line does,
   and stops the moment the row has news. */
.delib-check--pending .delib-check-line {
  color: var(--muted);
  animation: dm-live 1.6s infinite;
}

/* And a row that has landed says so in the colour of its WORST verdict — the same vocabulary the
   fact-check panel's own chips use, so a player meets the words twice rather than two schemes. */
.delib-check--checked .delib-check-line {
  color: var(--pro-ink);
}

.delib-check--v-false .delib-check-line { color: var(--con-ink); }
.delib-check--v-misleading .delib-check-line { color: #8a6508; }
.delib-check--v-contested .delib-check-line { color: #4a5b7a; }
.delib-check--v-unverifiable .delib-check-line,
.delib-check--missed .delib-check-line { color: var(--muted); }

.delib-recall-turns {
  display: grid;
  gap: 10px;
  max-height: min(30vh, 240px);
  /* The checks block above shares this card, so the recall gives up the height it takes rather
     than the two together pushing the recovery row off a short screen. */
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 10px 12px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.55);
  border: 1px solid rgba(18, 22, 30, 0.1);
  text-align: left;
}

.delib-turn h4 {
  margin: 0 0 2px;
  font-family: var(--display);
  font-weight: 800;
  font-size: 12px;
  letter-spacing: 1px;
  color: var(--bronze-lo);
}

.delib-turn p {
  margin: 0;
  font-size: calc(14px * var(--prose));
  line-height: 1.45;
  color: var(--ink-2);
  white-space: pre-wrap;
}

.delib-recall.has-checks .delib-recall-turns {
  max-height: min(22vh, 160px);
}

@media (max-height: 700px) {
  .delib-recall-turns {
    max-height: 22vh;
    gap: 6px;
  }
  .delib-recall.has-checks .delib-recall-turns {
    max-height: 16vh;
  }
  .delib-check-list {
    max-height: 13vh;
  }
}

/* ------------------------------------------------- the judge, inside the card */

/* THE CARD LAYER AND HER BENCH ARE BOTH CENTRED ON THE STAGE. `.screen--card` is
 * `position:absolute; inset:0` with `margin:auto` on the card (correct — laying a card in flow
 * under the set clips three screens, lessons.md), and `body[data-room] .bench` stands at
 * `bottom:30%`, centred, ~320px wide at 1280x860. So ANY card wider than about 340px covers the
 * judge completely — and the two screens that are entirely about her, deliberation and the verdict,
 * both wear one. She was named, quoted and read aloud while off screen.
 *
 * Moving the cards is the wrong fix: the verdict card's geometry is load-bearing (the action row
 * has to stay above the fold, and `margin:auto` is what lets a card taller than the viewport scroll
 * instead of losing its top — both lessons this file has already paid for). So she comes INTO the
 * card, cut from the same rig by viewBox alone (rig.js `bustBox`). One Honoria, one set of art, one
 * animation: the bust blinks, sways and talks exactly as the figure on the bench does.
 *
 * The frame's aspect is written by main.js from the rig's own box, so the crop and the hole it
 * sits in cannot drift apart. The 0.9 fallback is only what an unpainted mount looks like. */
.bust {
  --bust: 132px;
  width: var(--bust);
  aspect-ratio: var(--bust-aspect, 0.9);
  flex: 0 0 auto;
  overflow: hidden;
  border-radius: 20px;
  /* The bench's own gilding, which is also what backs the nameplate over painted marble. */
  border: 2px solid var(--gold);
  background: radial-gradient(118% 96% at 50% 18%, var(--paper), var(--paper-2) 60%, var(--line));
  box-shadow: 0 6px 18px rgba(18, 22, 30, 0.22);
}

/* The rig svg is deliberately `overflow: visible` so a gesture pose's raised hands are not sawn
 * off at the artwork frame — which for a CROP means no crop at all, and the judge's whole lower
 * body painting straight out of the portrait onto the scorecard. Clipped at the viewBox here.
 *
 * BOTH CLASSES, and not just `.char--bust`: `.char--rigged { overflow: visible }` lives ~1200 lines
 * further down, so at equal specificity it wins and a single-class rule here is dead CSS that reads
 * exactly like a live one. (It was, until QC measured the computed value instead of trusting the
 * cascade — the mount's own `overflow: hidden` was doing all the work.) Two classes outrank one
 * wherever this rule ends up in the file. */
.char--rigged.char--bust {
  overflow: hidden;
  display: block;
  width: 100%;
  height: 100%;
}

/* Deliberation: she IS the screen, so she is the biggest thing on it. Capped against the VIEWPORT
 * height as well as in pixels — the card also carries a heading, three dots and a note, and on a
 * short window a fixed 190px bust pushes the note off the bottom. */
.bust--large {
  --bust: min(190px, 26vh);
  margin: 0 auto;
}

.card--judge {
  gap: 12px;
}

/* ------------------------------------------------------------- the verdict */

/* Her line, beside her, in a bubble with a tail — she reads it aloud in her own voice and her
   mouth runs for as long as the delivery lasts (paintJudgeBust / setJudgeSpeaking in main.js). */
.verdict-say {
  display: flex;
  align-items: center;
  gap: 14px;
  text-align: left;
}

.verdict-statement {
  margin: 0;
  font-weight: 800;
  font-style: italic;
  color: var(--muted-2);
  font-size: 15px;
}

.verdict-say .verdict-statement {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  /* A shade larger and darker than the same paragraph used to be on its own: in the bubble it is
     no longer a caption under the heading, it is the sentence she is reading out. */
  font-size: 16px;
  color: var(--ink-2);
  padding: 12px 16px;
  border-radius: 16px;
  background: var(--accent-wash);
  border: 1px solid rgba(255, 106, 61, 0.28);
}

/* The tail: a rotated square wearing the bubble's own fill and two of its borders, so it stays in
   step with the bubble however that is restyled. */
.verdict-say .verdict-statement::before {
  content: "";
  position: absolute;
  left: -7px;
  top: 50%;
  width: 12px;
  height: 12px;
  transform: translateY(-50%) rotate(45deg);
  background: var(--accent-wash);
  border-left: 1px solid rgba(255, 106, 61, 0.28);
  border-bottom: 1px solid rgba(255, 106, 61, 0.28);
}

/* Score rows: a label and two bars. The design reads a scorecard as two bars racing, not as a
   table of digits — the winner is visible before you read a number. */
.scores {
  display: flex;
  flex-direction: column;
  gap: 10px;
  text-align: left;
}

.score-head,
.score-row,
.score-total {
  display: grid;
  grid-template-columns: minmax(70px, 110px) 1fr 1fr;
  gap: 12px;
  align-items: center;
}

.score-head {
  font-family: var(--display);
  font-weight: 800;
  font-size: 15px;
}

.score-head .pro { color: var(--pro); }
.score-head .con { color: var(--con); }

.score-row .label {
  font-weight: 900;
  font-size: 12px;
  letter-spacing: 1px;
  color: var(--muted);
  text-transform: uppercase;
}

.meter {
  display: flex;
  align-items: center;
  gap: 8px;
}

.meter .bar {
  flex: 1;
  height: 16px;
  background: var(--line);
  border: 1px solid rgba(18, 22, 30, 0.2);
  border-radius: 8px;
  overflow: hidden;
}

.meter .bar i {
  display: block;
  height: 100%;
  width: 0;
  transition: width 0.5s ease;
}

.meter--pro .bar i { background: var(--pro-bar); }
.meter--con .bar i { background: var(--con-bar); }

.meter .n {
  font-weight: 900;
  color: var(--ink);
  width: 18px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.score-total {
  font-family: var(--display);
  font-weight: 800;
  font-size: 20px;
  color: var(--ink);
  border-top: 3px solid var(--line);
  padding-top: 8px;
}

.score-total .label {
  font-size: 14px;
  letter-spacing: 1px;
}

.winner {
  display: inline-block;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(22px, 4.6vw, 32px);
  border: 2px solid currentColor;
  border-radius: 12px;
  padding: 6px 22px;
  background: #fff;
  animation: dm-pop 0.3s ease-out;
}

/* Coloured by which SIDE won, never by whether the player did. Keying it on the player's result
   put a red "<OPPONENT> WINS" under a scorecard painting CON red and PRO green — the badge
   contradicted the table directly above it — and would have painted a winning CON player green. */
.winner--pro { color: var(--pro); }
.winner--con { color: var(--con); }

/* THE MOMENT, under the badge. A first win and a new best single debate are the two results this
   game used to hand back exactly as it handed back a fiftieth ordinary one; the confetti says how
   big, and this says what it WAS. It arrives with the badge (paintMilestone) rather than with the
   card, so it cannot spoil the reveal, and it rises in rather than appearing, because a line that
   pops into a settled layout reads as an error message. */
.milestone {
  margin: 10px 0 0;
  font-family: var(--display);
  font-weight: 800;
  font-size: 15px;
  letter-spacing: 0.4px;
  color: var(--bronze-lo);
  animation: dm-line-in 0.5s ease both;
}

/* A first win is the gold one. A new best keeps the bronze the coaching headings wear — it is the
   game's own high score rather than a rite of passage. */
.milestone--first {
  color: var(--gold-lo);
}

.result-pts {
  display: inline-block;
  font-family: var(--display);
  font-weight: 800;
  font-size: 20px;
  /* Ink, not white: the design's #16181D on #FF6A3D is ~7:1, white on it is ~2.9:1. */
  color: var(--ink);
  background: var(--accent);
  border: 1px solid rgba(18, 22, 30, 0.2);
  border-radius: 999px;
  padding: 6px 18px;
}

.win-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  flex-wrap: wrap;
}

.feedback,
.factcheck {
  text-align: left;
}

.feedback h3,
.factcheck h3 {
  margin: 10px 0 6px;
  font-family: var(--display);
  font-size: 13px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 800;
}

.feedback-col {
  margin-bottom: 10px;
}

.feedback h4 {
  margin: 0 0 4px;
  font-family: var(--display);
  font-size: 14px;
  font-weight: 800;
  color: var(--ink);
}

.feedback dl {
  margin: 0;
  display: grid;
  grid-template-columns: minmax(84px, auto) 1fr;
  gap: 4px 12px;
}

.feedback dt {
  font-weight: 900;
  font-size: 12px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--muted);
  padding-top: 2px;
}

.feedback dd {
  margin: 0;
  font-weight: 700;
  /* Named at the size it already inherited, so the multiplier can reach it. */
  font-size: calc(16px * var(--prose));
}

.feedback dd.no-note {
  color: var(--muted);
  font-style: italic;
  font-weight: 600;
}

/* The plate's width and the gap beside it, in ONE place: the heading sits to the right of the
   portrait, so anything else that has to line up under that heading needs the same number, and two
   copies of it drift the first time the plate is resized. */
.factcheck {
  --plate: 52px;
  --plate-gap: 11px;
}

/* 18px, not 12. QC measured the old rhythm at 6px INSIDE a block against 12px between blocks — six
   pixels of air to mark a boundary, with the portrait doing nearly all of the separating. That
   reads fine while the plates are there and would leave the panel worse than before if they ever
   went; the gap between blocks should carry its own weight. */
.fc-block {
  margin-bottom: 18px;
}

.fc-block:last-child {
  margin-bottom: 0;
}

/* WHO SAID IT, AS A FACE. The panel runs one block per argument and every heading is the same
 * 12px uppercase grey, so finding "the one where Sam was wrong" meant reading four of them. A
 * portrait is the only thing on this screen a reader can find without reading, which is what makes
 * the list skimmable — the heading text is unchanged and still carries the phase.
 *
 * The frame is the same `.bust` the judge wears, at a name-plate size, and it is STILL: four
 * thumbnails blinking beside a list of claims is noise, and each one is six running animations over
 * thirteen layers. */
.fc-who {
  display: flex;
  align-items: center;
  gap: var(--plate-gap);
  margin: 0 0 6px;
}

/* 52px, not the 40 this started at: at 40 the faces are present but not RECOGNISABLE at a glance,
   which is the entire job. The extra height is free — this panel sits below the action row, so
   nothing here is competing with the fold. */
.fc-who .bust {
  --bust: var(--plate);
  border-width: 2px;
  border-radius: 14px;
}

/* Keyed on the SIDE the argument was made for, which is what the heading beside it already says —
   never on whose argument it is, so a player on CON is red here exactly as the scorecard paints
   CON red (lessons.md: a status colour keys on the fact it names). */
.fc-who--pro .bust { border-color: var(--pro-bar); }
.fc-who--con .bust { border-color: var(--con-bar); }

.fc-block h4 {
  margin: 0;
  font-family: var(--display);
  font-size: 12px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 800;
}

.fc-none {
  margin: 0;
  color: var(--muted);
  font-size: 14px;
  font-weight: 700;
}

/* Line up under the HEADING, not under the portrait. A claim list is its own bordered card, so
   starting at the block's left edge reads as deliberate there — but a bare sentence starting to the
   LEFT of the heading it belongs to just looks detached from it. Only the two plain-paragraph
   shapes need this ("nothing check-worthy", and "not checked in time"). */
.fc-block > .fc-none {
  padding-left: calc(var(--plate) + var(--plate-gap));
}

.fc-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.fc-claim {
  background: #fff;
  border: 1px solid rgba(18, 22, 30, 0.14);
  border-left-width: 5px;
  border-radius: 12px;
  padding: 9px 13px;
}

.chip {
  display: inline-block;
  font-family: var(--display);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 1px;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 999px;
  color: #fff;
}

.chip--true { background: var(--pro-bar); }
.fc-claim--true { border-left-color: var(--pro-bar); }
.chip--false { background: var(--con); }
.fc-claim--false { border-left-color: var(--con); }
.chip--misleading { background: #b8860b; }
.fc-claim--misleading { border-left-color: #b8860b; }
.chip--unverifiable { background: var(--muted); }
.fc-claim--unverifiable { border-left-color: var(--muted); }
.chip--contested { background: #4a5b7a; }
.fc-claim--contested { border-left-color: #4a5b7a; }

.fc-claim blockquote {
  margin: 6px 0 4px;
  /* 16px is what it already inherited from the card; naming it is what lets it take the prose
     multiplier without changing at the resting size. */
  font-size: calc(16px * var(--prose));
  padding-left: 10px;
  border-left: 2px solid var(--line);
  font-style: italic;
  font-weight: 700;
}

.fc-falsifier,
.fc-note {
  margin: 4px 0 0;
  font-size: calc(14px * var(--prose));
  font-weight: 700;
}

.fc-sources {
  margin: 5px 0 0;
  font-size: 12px;
  font-weight: 800;
}

.fc-sources a {
  color: var(--accent);
  text-decoration: none;
}

.fc-sources a:hover { color: var(--accent-hi); text-decoration: underline; }

.record-strip {
  margin: 0;
  font-weight: 800;
  color: var(--muted);
  font-size: 14px;
}

/* Confetti, on a win. */
.confetti {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 9;
}

/* ONE fall each, not `infinite`. The duration and delay are per-piece inline values derived from
   `CONFETTI_MS` in main.js, which is also what the timer that empties this container reads — so the
   longhands are spelled out here rather than a shorthand, which would reset `animation-duration`
   back to 0s and leave the inline value doing a job nothing in this file admits to. */
.confetti i {
  position: absolute;
  top: -60px;
  width: 11px;
  height: 17px;
  border-radius: 3px;
  animation-name: dm-fall;
  animation-timing-function: linear;
}

.fatal {
  flex: 0 0 auto;
  margin: 0;
  padding: 10px 16px;
  background: var(--con);
  color: #fff;
  text-align: center;
  font-weight: 800;
}

/* ------------------------------------------------------------- responsive
 * Fitting is a HEIGHT question. The room is full-bleed now, so nothing has to give it space —
 * what gives instead is the empty band the characters stand in, and then the characters
 * themselves.
 */

/* Short: the conversation reaches further down, but never past the debaters' heads at 51%. */
/* THE HUD IS THE "SPACE ON TOP".
 *
 * At the owner's window (508x718) the HUD ate 156px of a 634px stage — a quarter of it — against
 * a 122px reading band holding 238px of text. Its height is driven by the 60px timer ring plus a
 * phase-dot row that repeats what "ROUND 1 OF 4" already says two lines above it, and none of
 * that scales with the room available. Compacted below 900px of viewport it costs ~106px instead,
 * and every pixel saved goes to the conversation: the band grows ~40% at that window with no
 * meaningful loss of cast (castScale re-derives against the new top).
 *
 * Gated on HEIGHT, because that is the axis that is short — a narrow but tall phone has room for
 * the full card, and a wide 1280x800 laptop does not. The 470px block below compacts it further
 * still; these values are the middle step it never had. */
@media (max-height: 900px) {
  .hud {
    top: 8px;
    gap: 5px;
  }
  .hud-card {
    padding: 6px 15px;
    border-radius: 15px;
  }
  .timer-ring {
    width: 46px;
    height: 46px;
  }
  /* Redundant with the "ROUND n OF 4" line inside the card, and it costs a fifth of what the
     reading band has. The round is still stated in words, and the dots return above 900px. */
  .phase-dots {
    display: none;
  }
}

@media (max-height: 700px) {
  .stage {
    --hist-top-default: 27%;
  }
  /* The verdict card is routinely taller than the viewport and the action row has to survive it,
     so the bust gives ground on the axis that is short. Same reasoning as the width rule below.
     Scoped to the verdict row: the deliberation card holds a heading, three dots and a note, and
     its own `min(190px, 26vh)` already answers a short window without help. */
  .verdict-say .bust {
    --bust: 104px;
  }
  /* AND THE CARD PAYS ITS OWN SHARE. This card was already at capacity before the judge existed: at
   * 360x640 with no judge on it at all, an above-average verdict puts Rematch 9px past the fold and
   * the canned one clears it by 39. So anything added anywhere above that row breaks something.
   * Gaps and block padding are the cheapest height in the card and cost nothing legible — together
   * they are worth ~30px, which is what lets her fit at 360x640 (-15) rather than overflow (+15).
   * Gated on HEIGHT because that is the axis that is short; a tall phone keeps the full rhythm. */
  #screen-verdict .card {
    gap: 10px;
    padding-top: 14px;
    padding-bottom: 14px;
  }
  #screen-verdict .scores {
    gap: 8px;
  }
}

@media (max-height: 560px) {
  .audience {
    display: none;
  }
  .draft {
    max-height: 22dvh;
  }
  /* Only the TOP moves. The bottom stays derived from --podium-h while the room is on screen —
     lowering the columns here would drop the conversation onto the debaters for every viewport
     between 471px and 560px tall. */
  .stage {
    --hist-top-default: 30%;
  }
}

/* With a keyboard up on a phone the room is pure cost: it is behind the one thing the player is
 * doing, and every pixel of it is a pixel the text box does not have. Gated on height, so a short
 * desktop window gets the same treatment and a tall phone keeps its room. */
@media (max-height: 470px) {
  body[data-screen="debate"] .set {
    display: none;
  }
  .topbar {
    padding: 4px 10px;
  }
  /* With the room gone the columns own everything between the HUD and the card — but they must
     still STOP above the card. At 30% the conversation ran 45px behind it: the input card is
     ~119px of a ~320px stage here (37%) plus its 2.75% offset, so anything under ~40% is hidden. */
  .stage {
    --hist-top-default: 34%;
  }
  .histories {
    bottom: 46%;
  }
  .hud {
    top: 6px;
    gap: 4px;
  }
  .hud-card {
    padding: 5px 12px;
    gap: 10px;
    border-radius: 14px;
  }
  .hud-title {
    font-size: 16px;
  }
  .hud-round {
    font-size: 10px;
  }
  .timer-ring {
    width: 44px;
    height: 44px;
  }
  .timer-ring .value {
    font-size: 14px;
  }
  .hud-rule {
    height: 30px;
  }
  .phase-dots {
    display: none;
  }
  .hud-topic {
    padding: 3px 12px;
    font-size: 12px;
  }
  .compose {
    padding: 8px;
    border-radius: 14px;
  }
  .compose-row {
    margin-top: 6px;
  }
  .compose-row .big-btn {
    font-size: 16px;
    padding: 7px 16px;
  }
  .draft {
    height: 58px;
    max-height: 18dvh;
  }
}

@media (max-width: 620px) {
  /* The wordmark shrinks to 16px here and the points pill becomes the bar's tallest child, so the
     bar's authored height follows it down. Same sum, different winner. */
  :root {
    --topbar-h: 55px;
  }

  /* The design's 3.75% puts the pill in the middle of the podium furniture on a phone, where the
   * cast is a far bigger fraction of the stage than it is on the canvas the number came from: at
   * 360px the pill is wide enough to reach the opponent's name badge and cover all but 7px of it,
   * which reads as a rendering glitch rather than as layering.
   *
   * 17% and not the 13% this first shipped as: 13% cleared the badges on a 740px-tall phone and
   * missed by 2px on a 640px one, because the offset is a percentage of a stage whose height varies
   * while the pill's own height does not. 17% leaves 20px of daylight at 360x640 and still sits
   * 122px below the speech columns, so there is room for the pill to grow (a longer name wraps it
   * to two lines) without landing back on the furniture. */
  .waiting {
    bottom: 17%;
  }

  /* 2px of tracking on a 12px heading is a desktop value, and on the longest pairing the panel can
     produce — "MAX REBUTTAL · CON RESPONSE" — it costs 13px more than the column has at 360px, so
     the heading wrapped to two lines beside a portrait that is only one line tall. Tightened rather
     than shrunk: the type stays 12px and the longest heading comes back to 217px in a ~245px
     column. Measured; a longer name than Max's would wrap again, and degrades cleanly when it does. */
  .fc-block h4 {
    letter-spacing: 1px;
  }

  /* SHE MOVES INSIDE HER OWN QUOTE PANEL, and the words wrap round her.
   *
   * WHAT COSTS HEIGHT HERE IS NOT HER HEIGHT. Beside the bubble she is ~107px tall against a bubble
   * already taller than that — what she actually costs is the ~110px she takes out of the bubble's
   * WIDTH, which the bubble pays back in LINES. Floated, the words keep the panel's full width and
   * only wrap around her, and the same layout serves her at 76px, 84px or 92px for the same price,
   * because the wrap is the cost and the picture is free.
   *
   * EVERY NUMBER BELOW IS `#btn-rematch`'s bottom edge minus `innerHeight` (negative = above the
   * fold), taken with entrance animations settled, on THREE statements: the 113-character canned
   * one, a 176-character verdict copied from a live debate, and a 15-character stub. An earlier
   * draft of this block was checked at 390x760 on the canned line only and was wrong about both the
   * mechanism and the size.
   *
   *                canned   long   long with NO judge at all
   *   360x640       -15     +33     +9      <- the card is genuinely full here
   *   375x667       -42      +6    -42
   *   390x664       -39      +9    -39      <- an iPhone 12/13/14 with the URL bar showing
   *   390x844      -193    -145   -193
   *
   * So an above-average verdict still overruns a 640-667px phone. It did that before she existed
   * too (360x640 is +9 with no judge on screen), which is what the height-gated rules further up
   * this file are clawing back; the screen scrolls, and the common case clears the fold at every
   * size.
   *
   * `position: relative; z-index: 1` is load-bearing, not decoration. The bubble is a POSITIONED
   * element that comes later in the DOM, so at `z-index: auto` it paints its own background straight
   * over the float: the judge vanishes completely while still measuring 92x102, with no error
   * anywhere. Verified by elementFromPoint at the frame's own centre. */
  /* THE ROW BECOMES THE PANEL, and the paragraph inside it is left plain.
   *
   * `display: flow-root` is the whole trick: it makes the row establish a block formatting context,
   * so the row CONTAINS the float and the tint is painted behind both of them. Without it the panel
   * is the paragraph, a float is not counted in its height, and a SHORT verdict leaves her standing
   * out of the bottom of her own bubble onto bare card — 8px on an ordinary statement, 56px on an
   * 11-character one, i.e. the common case and not the edge.
   *
   * The first attempt gave the paragraph `min-height: calc(var(--bust) / 0.9)` instead, and it
   * introduced exactly the drift it was meant to prevent: `--bust` set on the ROW is still beaten on
   * the FRAME by any rule matching the frame itself (an element's own declaration wins over an
   * inherited value), so the height-gated 104px override applied to her while the paragraph sized
   * itself against 92px. Containment needs no arithmetic and cannot disagree with itself. */
  .verdict-say {
    display: flow-root;
    text-align: left;
    /* 8px, not the 12 the bubble wore: the row's padding now sits OUTSIDE the float, so every pixel
       of it is taken off the width the words wrap into and paid back in lines. Measured at 375x667
       on a 176-character verdict: 12px of padding costs 24px of card height. */
    padding: 8px;
    border-radius: 16px;
    background: var(--accent-wash);
    border: 1px solid rgba(255, 106, 61, 0.28);
  }
  .verdict-say .bust {
    float: left;
    position: relative;
    z-index: 1;
    --bust: 92px;
    /* Flush with the panel's own corner: the negative pair cancels the row's padding for the FRAME
     * only, so the words keep their inset and her first line of text sits beside the top of her
     * head instead of 8px below it. That is worth 24px of card height at 360x640 — an 8px inset
     * above a float costs a whole line further down. */
    margin: -8px 10px 2px -8px;
    shape-outside: inset(0 round 20px);
  }
  /* The paragraph gives its panel dressing up to the row and is just words now. */
  .verdict-say .verdict-statement {
    padding: 0;
    background: none;
    border: 0;
  }
  /* The tail is what said "she is saying this" while she stood OUTSIDE the bubble. Inside it, the
     containment says it, and a tail pointing at a portrait in the same box is nonsense. */
  .verdict-say .verdict-statement::before {
    display: none;
  }
  /* The old bar's phone rules — a narrower ghost pill, a hidden LIVE badge, a tightened wordmark,
     a smaller points pill — are all gone: the bar is glass discs and a hidden wordmark at every
     width now, and a `padding` here would only fight the disc's own square. */
  .pts {
    padding: 5px 11px;
  }
  .hud-rule {
    display: none;
  }
  .hint {
    display: none;
  }
  .big-btn {
    flex: 1 1 40%;
  }
  .feedback dl {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .feedback dt {
    margin-top: 8px;
  }
  .score-head,
  .score-row,
  .score-total {
    grid-template-columns: minmax(58px, 76px) 1fr 1fr;
    gap: 8px;
  }
  /* Two 31% columns at 375px are ~110px each — unreadable for prose. Collapse to ONE column and
     put the turns back in chronological order with `order`, aligned by speaker: the columns become
     `display: contents` so their bubbles are laid out by .histories itself. Grouping by speaker is
     what the two-column form buys, and on a phone it costs more than it buys. */
  .histories {
    display: flex;
    flex-direction: column;
    gap: 9px;
    padding: 0 6px;
    overflow-y: auto;
    overscroll-behavior: contain;
    pointer-events: auto;
  }

  .history {
    display: contents;
  }

  .history .bubble {
    order: var(--turn);
    max-width: 92%;
    width: auto;
  }

  /* Aligned to the side its OWN character stands on, not to chat convention: you are on the left
     of the stage, so your turns are on the left. The whole justification for splitting the history
     by speaker is that it maps onto the room; right-aligning the player contradicted the podium
     directly beneath it. */
  .history--you .bubble {
    align-self: flex-start;
    border-radius: 14px 14px 14px 4px;
  }

  .history--them .bubble {
    align-self: flex-end;
    border-radius: 14px 14px 4px 14px;
  }
}

/* --------------------------------------------------------------------------
   Accounts and the leaderboard.

   Everything here is additive: with no key configured for the host, none of these elements is
   ever unhidden, and the game looks exactly as it did before accounts existed. */

.account-wrap {
  position: relative;
}

/* Anchored to the chip rather than placed in flow, so opening it never reflows the topbar (a
   header that grows on click pushes the whole stage down mid-game). */
.account-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
  min-width: 148px;
  background: var(--paper);
  border: 2px solid var(--ink);
  border-radius: 12px;
  box-shadow: var(--lift-shadow);
}

.account-menu .link-btn {
  width: 100%;
  text-align: left;
  padding: 7px 10px;
  border-radius: 8px;
}

.account-menu .link-btn:hover {
  background: var(--accent-wash);
}

/* A text-weight control for secondary actions. Kept a real <button> — it does not navigate. */
.link-btn {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-weight: 800;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.link-btn:hover {
  color: var(--ink);
}

/* Record and board share one line at the foot of the plaque, 18px apart — the record is what the
   board is ABOUT, and stacking them made two footnotes out of one. */
.room-links {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 18px;
}

/* The way into the board. It was a dashed outline, sized to read as quieter than "Random claim"
   and "Pick a topic" but louder than a footnote — because on the old card it stood alone under
   them. On the plaque the dashed outline belongs to "Watch an AI exhibition" and this sits BESIDE
   the record, so the design makes it the quiet thing it now is. The trophy stays drawn rather
   than typed: that is the courtroom's gold on every platform instead of whichever emoji the OS
   ships. */
.trophy-btn {
  display: inline-flex;
  align-items: center;
  /* 14 + 2 is the 16px the artboard's emoji-plus-space occupies, so the control is the width it
     was drawn at. The drawn trophy keeps its 16-unit viewBox and simply renders smaller — at 14
     its stroked ink is 12.6px, which is the emoji's own, and its stroke stays above 1.4px. */
  gap: 2px;
  font-family: var(--body);
  font-weight: 800;
  font-size: 12.5px;
  color: var(--bronze-lo);
  background: none;
  border: 0;
  border-radius: 8px;
  /* The negative margin pair gives the words a real hit area without moving them: the artboard's
     18px gap is measured between the TEXT, and padding alone would push the trophy off it. */
  padding: 6px 4px;
  margin: -6px -4px;
  cursor: pointer;
  transition: all 0.18s;
}

.trophy-btn svg {
  width: 14px;
  height: 14px;
}

.trophy-btn:hover {
  color: var(--ink);
}

/* ------------------------------------------- the start screen: "Parchment plaque" (1d)
 *
 * ART DIRECTION: design project "Debate Master Game Design", file `Start Screen Ideas.dc.html`,
 * artboard 1d — "the cream card, warmed up: engraved double border and a gold fan, keeps today's
 * contrast". That file draws four treatments of one start screen (open floor / brass plaque /
 * frosted glass / parchment plaque); 1d is the chosen one.
 *
 * The artboard's PAGE CHROME — glass pill, glass discs, no wordmark, room running edge to edge —
 * is shared by all four and is now every screen's, so it lives in the top bar section above. What
 * is left here is the one thing that really is only this screen's: THE CARD HANGS, 86px from the
 * top of the screen instead of centring, because the room and the two debaters are meant to read
 * under it. (The plaque's paper, frame and engraving are `.card`'s — see there.)
 *
 * The artboard is a fixed 1280x673 desktop canvas, so its literals are the DESKTOP end of the
 * clamps here; the responsive half is this file's, as the header says.
 *
 * DELIBERATE DEVIATIONS from the artboard, all of them additive:
 *   * A fullscreen disc, which the artboard does not draw at all — it took the slot "Back to the
 *     Hall" used to hold.
 *   * The artboard only ever draws a SIGNED-IN chip (a "D"), so the signed-out sign-in control
 *     keeps its own disc with the person glyph.
 *   * Coarse pointers get their tap targets back below — the artboard is a mouse-sized canvas.
 */

/* THE PLAQUE hangs rather than centres, 86px from the top OF THE SCREEN — which is why this layer
   is anchored to the viewport rather than to the stage it usually fills. 86px literal wherever
   the viewport can pay for it, and a proportion of the height where it cannot: 12.8vh is the
   artboard's own 86/673, so the two agree exactly at the canvas it was drawn on and the plaque
   never eats a short screen. */
#screen-room {
  position: fixed;
  padding-top: 0;
}

#screen-room .card {
  /* Never above the bar, whatever the shrink does: the bar is transparent here but its controls
     are not, and on a short narrow screen a full-width plaque would slide under the discs. */
  margin-top: max(var(--topbar-h), min(86px, 12.8vh));
  margin-bottom: auto;
}

/* The start screen's own box. The paper, the frame and the engraving are `.card`'s now; what is
   left here is the artboard's measurements for THIS card. 630 and not its `width: 560px`: the
   design canvas ships no box-sizing reset, so 560 is a CONTENT width and the plaque it draws is
   560 + the 34px padding pair + the 1px border pair. This file is border-box throughout, so the
   same plaque has to be written as its outer width — and 630 is what keeps the blurb's 440px
   column sitting in the margins it was drawn in. */
.card--plaque {
  width: min(630px, 100%);
  align-items: center;
  gap: 12px;
  padding: 26px clamp(18px, 5vw, 34px) 24px;
}

.plaque-fan {
  display: block;
  flex: 0 0 auto;
}

/* Heavier and tighter than the hero on any other card, and lifted 4px into the fan: the plaque is
   engraved type, not a headline. The white shadow is the bevel — one pixel of highlight under the
   letters is what makes ink look cut into paper rather than printed on it. */
#screen-room .card h2.hero {
  font-weight: 900;
  font-size: clamp(26px, 4.4vw, 32px);
  line-height: normal;
  letter-spacing: 0.5px;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
  margin-top: -4px;
}

/* Upper-cased here rather than in the markup, so a screen reader says the words instead of
   spelling them out. */
.card--plaque .tagline {
  margin: -6px 0 0;
  font-family: var(--display);
  font-weight: 800;
  font-size: 12.5px;
  line-height: normal;
  letter-spacing: 2.5px;
  text-transform: uppercase;
  color: var(--bronze-lo);
}

.card--plaque .blurb {
  margin: 0;
  max-width: 440px;
  font-weight: 700;
  font-size: 13px;
  line-height: 1.55;
  color: var(--muted-2);
}

.card--plaque .row--plaque {
  gap: 12px;
  margin-top: 6px;
}

.featured-topic-btn {
  width: min(440px, 100%);
  display: grid;
  gap: 3px;
  padding: 9px 14px;
  border: 1px solid rgba(196, 136, 42, 0.65);
  border-radius: 12px;
  background: rgba(255, 248, 225, 0.72);
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5);
}

.featured-topic-btn[hidden] {
  display: none;
}

.featured-topic-btn span,
.topic-context-kicker {
  color: var(--bronze-lo);
  font-family: var(--display);
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1.5px;
  line-height: 1.1;
  text-transform: uppercase;
}

.featured-topic-btn strong {
  font-size: 13px;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

.featured-topic-btn:hover,
.featured-topic-btn:focus-visible {
  border-color: var(--accent);
  background: rgba(255, 255, 255, 0.88);
}

.card--topic-context {
  width: min(700px, 100%);
  align-items: stretch;
  text-align: left;
}

.card--topic-context > h2 {
  max-width: 620px;
  margin-inline: auto;
  text-align: center;
}

/* Screen changes move focus to this non-interactive heading for assistive technology. The browser
   default ring reads like a giant text field after a mouse click; real controls keep their normal
   focus-visible treatment. */
.card--topic-context > h2:focus {
  outline: none;
}

.topic-context-kicker,
.topic-context-disclaimer {
  margin: 0;
  text-align: center;
}

.topic-context-disclaimer {
  color: var(--muted);
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.topic-context {
  width: 100%;
  padding: 14px 16px;
  border: 1px solid var(--line-2);
  border-radius: 14px;
  background: var(--paper-2);
  text-align: left;
}

.topic-context-brief,
.topic-context-motion {
  margin: 0;
  line-height: 1.55;
}

.topic-context-motion {
  margin-bottom: 8px;
  font-weight: 900;
}

.topic-context h3 {
  margin: 14px 0 6px;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.topic-sources {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.topic-sources li {
  display: grid;
  gap: 1px;
}

.topic-sources a {
  color: var(--ink);
  font-weight: 800;
}

.topic-sources span {
  color: var(--muted);
  font-size: 11px;
}

.topic-context--replay {
  margin-bottom: 16px;
}

/* Smaller and softer-cornered than the buttons on the flow screens: on the plaque they are two
   choices on a notice, not the one thing to press on a full-screen card. The accent becomes a
   GRADIENT here — flat vermilion against warm paper went muddy where the plaque is darkest. */
.card--plaque .big-btn {
  font-size: 15px;
  letter-spacing: 0.5px;
  padding: 12px 24px;
  border-radius: 14px;
  background: linear-gradient(#ff7a4e, #ff6a3d);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}

.card--plaque .big-btn:hover {
  background: linear-gradient(var(--accent-hi), #ff7a4e);
}

.card--plaque .big-btn--plain {
  padding: 11px 24px;
  background: var(--paper);
  border: 1px solid var(--line-2);
  box-shadow: none;
}

.card--plaque .ghost-btn {
  font-size: 12px;
  line-height: normal;
  padding: 8px 14px;
  border-radius: 10px;
}

.card--plaque .record-strip {
  font-size: 12px;
  line-height: normal;
}

/* A mouse can hit a 12px line; a thumb cannot. Both of the plaque's quiet controls get their
   height back wherever the pointer is coarse — a POINTER question, not a width one. */
@media (pointer: coarse) {
  .card--plaque .ghost-btn {
    padding: 11px 16px;
  }

  /* 44px, like every other control a thumb has to find — the top bar's discs, the turn buttons,
     the pause card, the recovery row. Padding alone took these to 35px (measured at 375x812 with
     `(pointer: coarse)` matching), and `How to play` is the control a first-timer who has not
     understood the room reaches for.

     The height is REAL here rather than negative margin over a 15px line, and that is the whole
     lesson of the first attempt at this: `min-height: 44px; margin: -14.5px -8px` kept the row
     exactly where it was and made the two boxes OVERLAP by 11px once the footer wraps, because
     each then reached 14.5px past its own line into an 18px gap — so the lower ten pixels of
     `Leaderboard` opened How to play, which is worse than a small target. A hit area a layout has
     not been given room for is not a hit area.

     So the row pays for it: the boxes stand at their full height and `row-gap` comes off, since
     each already carries 10px of its own padding and two of those are the 18px between the words
     the artboard measures. The horizontal margin pair stays — it is what keeps the 18px COLUMN gap
     between the words rather than between the boxes. */
  .room-links {
    row-gap: 0;
  }

  .trophy-btn {
    min-height: 44px;
    padding: 10px 8px;
    margin: 0 -8px;
  }
}

.published {
  margin: 6px 0 0;
  text-align: center;
  font-weight: 800;
  color: var(--pro-ink);
}

/* Signed in here, a guest to the server: the debate was stored with no account and cannot rank or
   be published. Not the green of a published score — this line reports a loss. */
.published--warn {
  color: var(--con-ink);
}

/* Publishing this debate's transcript. Quieter than the sign-in pitch above it on purpose: that
   one is an offer, this one is a question about the player's own writing, and a card-sized frame
   around a single checkbox reads as another thing to get past. */
.share-debate {
  margin: 12px auto 0;
  max-width: 42ch;
  text-align: center;
}

.share-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  cursor: pointer;
}

.share-toggle input {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
  cursor: pointer;
}

.share-note {
  margin: 4px 0 0;
  font-size: 0.86rem;
  color: var(--muted);
}

/* The one state that must not be mistaken for the ordinary explanation under the toggle: the
   player pressed something and it did NOT take effect. */
.share-note.is-error {
  color: var(--con-ink);
  font-weight: 700;
}

.share-link {
  display: flex;
  gap: 8px;
  margin: 8px 0 0;
}

.share-link input {
  flex: 1 1 auto;
  min-width: 0; /* a flex item's default min-width:auto refuses to shrink and overflows the card */
  padding: 8px 10px;
  border: 2px solid var(--line);
  border-radius: 10px;
  font: inherit;
  font-size: 0.85rem;
  background: #fff;
  color: var(--ink);
}

/* A board entry is a door to that player's published debates. */
.lb-row[data-user],
.lb-step[data-user] {
  cursor: pointer;
}

.lb-row[data-user]:focus-visible,
.lb-step[data-user]:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.replay-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 8px;
}

.replay-item {
  width: 100%;
  display: grid;
  gap: 2px;
  padding: 12px 14px;
  text-align: left;
  background: #fff;
  border: 2px solid var(--line);
  border-radius: 14px;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
}

.replay-item:hover {
  border-color: var(--accent);
}

.replay-item-topic {
  font-weight: 800;
}

.replay-item-meta {
  font-size: 0.82rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.replay-by {
  margin: 0 0 14px;
  text-align: center;
  color: var(--muted);
  font-weight: 700;
}

/* The exchange itself, in the order it happened. Tinted by SIDE, matching the scorecard below it
   rather than by who the reader happens to be — a replay has no "you". */
.replay-turn {
  margin: 0 0 12px;
  padding: 12px 14px;
  border-radius: 14px;
  border-left: 5px solid var(--line);
  background: var(--paper-2);
  /* The card centres its contents, which is right for a verdict and wrong for four paragraphs of
     argument: centred prose has no stable left edge for the eye to return to on each new line. */
  text-align: left;
}

.replay-turn--pro {
  border-left-color: var(--pro-ink);
}

.replay-turn--con {
  border-left-color: var(--con-ink);
}

.replay-turn h4 {
  margin: 0 0 6px;
  font-size: 0.74rem;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.replay-turn p {
  margin: 0;
  white-space: pre-wrap; /* the player's own paragraphing is part of what was argued */
}

/* The pitch. Washed rather than outlined so it reads as an offer beside the verdict, not as
   another panel of results. */
.benefits {
  margin: 14px 0 0;
  padding: 16px 18px;
  background: var(--accent-wash);
  border: 2px solid var(--accent-hi);
  border-radius: 16px;
}

.benefits-head {
  margin: 0 0 10px;
  font-family: var(--display);
  font-size: 20px;
  font-weight: 800;
  color: var(--ink);
}

/* The one concrete sentence on a card of promises: where the points this player already has would
   put them. It sits between the headline and the bullets and is weighted to be read first, because
   it is the only line on the card that is about them rather than about accounts. Centred with the
   card like the headline, not left-aligned like the list, since it is one sentence and not a set. */
.benefits-standing {
  margin: 0 0 12px;
  font-family: var(--display);
  font-size: 15px;
  font-weight: 700;
  line-height: 1.4;
  color: var(--accent-ink, var(--ink));
}

.benefits-list {
  margin: 0 0 14px;
  padding-left: 20px;
  display: grid;
  gap: 6px;
  /* The card centres its text; a bulleted list inherits that and renders as ragged lines floating
     beside their own bullets. Alignment is a per-element decision here, not a card-wide one. */
  text-align: left;
  font-size: 14px;
  line-height: 1.45;
  color: var(--ink);
}

/* The rank-home card's one paragraph, where the benefits card has its bulleted list. Centred with
   the card rather than left-aligned like the list, because it is one sentence and not a set. */
.benefits-body {
  margin: 0 0 14px;
  font-size: 14px;
  line-height: 1.45;
  color: var(--ink);
}

.benefits-foot {
  margin: 10px 0 0;
  font-size: 12px;
  color: var(--muted);
  text-align: center;
}

/* The once-dismissed state: same offer, one line, no card. */
.benefits-quiet {
  margin: 10px 0 0;
  text-align: center;
  font-size: 14px;
}

/* The design puts BOTH chip groups on one row with a hairline between them — the window picker
   and the category picker are one question ("which slice?") asked twice, and stacking them read
   as two unrelated toolbars. Wrapping, because six chips do not fit 375px and the design's card
   is a 560px desktop canvas. */
.lb-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 8px;
  /* No margin of its own. The card is a flex column with a 14px gap, and in a flex container a
     margin ADDS to the gap rather than collapsing into it — carrying the old 14px here is what
     put 36px above the podium where the design has 22. */
  margin: 0;
}

.lb-tabs {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
}

/* The hairline belongs to the category group so it cannot outlive it: when there is nothing to
   filter, paintBoardFilters() hides #lb-cats and the rule goes with it. */
.lb-cats::before {
  content: "";
  align-self: stretch;
  width: 1px;
  background: rgba(18, 22, 30, 0.14);
  margin: 2px 4px;
}

.lb-tab {
  border: 1.5px solid var(--line-2);
  background: transparent;
  border-radius: 999px;
  padding: 7px 16px;
  font: inherit;
  font-weight: 800;
  font-size: 13px;
  color: var(--muted);
  cursor: pointer;
  transition: all 0.18s;
}

.lb-tab.is-on {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--paper);
}

/* ------------------------------------------------------------- the podium */

/* Top three as a podium, laid out [2nd, 1st, 3rd] and bottom-aligned so the tallest bar is in the
   middle. The RANK order stays in the markup and the swap is done with `order`, so the list a
   screen reader walks is 1, 2, 3 — an <ol> whose DOM order was 2, 1, 3 would announce the runner
   up first and mean it. */
.lb-podium {
  list-style: none;
  /* 8 on top of the card's own 14px gap is the design's 22 above the podium. */
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 14px;
}

/* The design's 128px, kept as the CEILING rather than the width: three of them plus the gaps is
   412px, and a 375px phone has about 324px of card to spend. */
.lb-step {
  flex: 1 1 0;
  min-width: 0;
  max-width: 128px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  animation: dm-podium-up 0.4s var(--d, 0s) ease-out backwards;
}

.lb-step:nth-child(1) { order: 2; } /* winner, centre */
.lb-step:nth-child(2) { order: 1; }
.lb-step:nth-child(3) { order: 3; }

.lb-av {
  /* Same reason as .lb-bar below: 46px is the design's disc INSIDE its ring, measured on a page
     with no border-box reset. Under this file's reset the ring would eat 5px of the disc. */
  box-sizing: content-box;
  width: 46px;
  height: 46px;
  flex: none;
  border-radius: 50%;
  background: var(--av);
  border: 2.5px solid var(--medal);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--display);
  font-weight: 800;
  font-size: 19px;
  color: #ffffff;
}

.lb-step .lb-name {
  max-width: 100%;
  font-weight: 900;
  font-size: 13.5px;
}

.lb-step-pts {
  font-family: var(--display);
  font-weight: 800;
  font-size: 16px;
  color: var(--tier);
  /* The design's page had the browser's default leading here, not this file's 1.5. Left alone it
     was 4px tall, which happened to cancel the 5px the avatar was short — two wrongs holding the
     column's total right. Both are now right on their own. */
  line-height: 1.2;
}

.lb-bar {
  width: 100%;
  /* --h is the design's bar height ABOVE its 8px inset, measured on a page with no border-box
     reset. Under this file's reset the same number would swallow the padding and come out 8px
     short, so this one box keeps the design's model. */
  box-sizing: content-box;
  height: var(--h);
  background: linear-gradient(var(--medal), var(--medal-lo));
  border-radius: 10px 10px 0 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 8px;
  font-family: var(--display);
  font-weight: 800;
  font-size: 22px;
  color: rgba(255, 255, 255, 0.9);
}

/* --------------------------------------------------------- the rest of it */

/* While the next slice is in flight, the slice already on screen STAYS — it just goes quiet. The
   card is `margin:auto` centred, so emptying the body to show a loading line collapsed ~480px and
   threw the whole modal up the page and back on every press. */
.lb-body {
  transition: opacity 0.18s;
}

/* Inert IMMEDIATELY — what is under the pointer is about to be replaced, so a second press must
   not land on it — but dim only after a quarter second. The delay is on the way IN and not on the
   way out (the base rule above governs leaving), so a board that comes back quickly never visibly
   fades at all; trading one flicker for a shorter one would not have been a fix. A slow board
   still says plainly that it is being replaced. */
.lb-body.is-stale {
  opacity: 0.5;
  pointer-events: none;
  transition-delay: 0.25s;
}

.lb-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 7px;
}

/* Ranks 4 and down sit under a rule, because the podium above them is the same board and not a
   separate widget. Only when there IS a podium: a board of three has nothing to divide. */
.lb-podium + .lb-list {
  margin-top: 14px;
  border-top: 1px solid rgba(18, 22, 30, 0.1);
  padding-top: 14px;
}

/* Four fixed tracks: rank and points must not move as names change length, or the column reads
   as ragged noise instead of a ranking. minmax(0,1fr) lets a long name ellipsize rather than
   pushing the points off the row. */
.lb-row {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) auto 86px;
  align-items: center;
  gap: 10px;
  padding: 9px 14px;
  border: 1.5px solid rgba(18, 22, 30, 0.1);
  border-radius: 12px;
  /* White, not --paper: the design lifts a row off the cream card by making it brighter than the
     card, and --paper IS the card. */
  background: #ffffff;
  animation: dm-row-in 0.3s var(--d, 0s) ease-out backwards;
}

.lb-row.is-you {
  border-color: rgba(255, 106, 61, 0.5);
  background: rgba(255, 106, 61, 0.07);
}

.lb-rank {
  font-family: var(--display);
  font-weight: 800;
  font-size: 15px;
  color: var(--muted-3);
  text-align: left;
}

.lb-row.is-you .lb-rank {
  color: var(--accent);
}

.lb-name {
  font-weight: 900;
  font-size: 15px;
  color: var(--ink);
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Says WHICH row is yours in words, for the readers who cannot see that it is the orange one. */
.lb-you-tag {
  font-weight: 800;
  font-size: 11px;
  color: var(--accent);
}

.lb-wl {
  font-weight: 800;
  font-size: 12.5px;
  color: var(--muted-3);
}

/* A pill rather than a number: the tier wash is what makes the top of the board read hot and the
   tail read quiet at a glance, which is the whole point of the colour. */
.lb-pts {
  justify-self: end;
  font-family: var(--display);
  font-weight: 800;
  font-size: 14px;
  color: var(--tier);
  background: var(--tier-bg);
  border-radius: 999px;
  padding: 4px 12px;
  /* The pill is the tallest thing in the row, so its leading is what sets the row height. This
     file's body leading is 1.5 and the design's page had the browser default; left at 1.5 the pill
     came out 4px taller than drawn and took every row with it. */
  line-height: 1.2;
}

/* What a standing is worth on the portal, hung UNDER the score it belongs to — the catalogue's own
   shape (`renderBoardRows` in zapjam-board.js) drawn on this game's own board, so a row here reads
   the same as a row on every other game's. Both cells become a centred column: the score keeps the
   type it had, the zaps sit beneath it smaller and quieter, and the row simply grows by their
   height rather than the pill trying to hold two lines at one leading. */
.lb-pts,
.lb-step-pts {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
}

.board-zaps {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-family: var(--display);
  font-size: 10.5px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--muted-3);
  letter-spacing: 0.01em;
}

/* The coin is the ZapJam mark drawn as a MASK — the mark wears the citrus gradient, and an `<img>`
   cannot be recoloured, an external `<use>` cannot reach this document's gradient, and a script is a
   thing that can fail mid-render. Inline data, so nothing is fetched: this game loads nothing from
   anywhere but its own origin. `inline-block` rather than bare inline, because a bare inline span
   ignores both dimensions and paints nothing when it stands outside the flex row. */
.board-zap-coin {
  display: inline-block;
  vertical-align: -0.15em;
  width: 0.95em;
  height: 0.95em;
  flex: none;
  background: linear-gradient(135deg, #e0f050, #f4d344 42%, #f88b50);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 640'%3E%3Cpath d='M 515.4 142.5 A 264 264 0 1 1 440.9 85.3' fill='none' stroke='%23000' stroke-width='47' stroke-linecap='round'/%3E%3Cpath d='M -84 -120 L 156 -120 L -12 48 L 156 48 L 84 120 L -156 120 L 12 -48 L -156 -48 Z' fill='%23000' transform='translate(320 310)'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 640'%3E%3Cpath d='M 515.4 142.5 A 264 264 0 1 1 440.9 85.3' fill='none' stroke='%23000' stroke-width='47' stroke-linecap='round'/%3E%3Cpath d='M -84 -120 L 156 -120 L -12 48 L 156 48 L 84 120 L -156 120 L 12 -48 L -156 -48 Z' fill='%23000' transform='translate(320 310)'/%3E%3C/svg%3E");
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}

/* On the reader's own row the citrus sits on an orange wash; the row's own accent reads cleaner. */
.lb-row.is-you .board-zap-coin {
  background: var(--accent);
}

/* The word, for a screen reader that would otherwise be handed a bare number under a score. */
.board-zap-unit {
  position: fixed;
  width: 1px;
  height: 1px;
  clip: rect(0 0 0 0);
  overflow: hidden;
}

/* What those figures ARE. Nothing mints yet, so the board says so rather than letting a number
   under a score read as a balance (root `_doc.md`). */
.lb-zaps-note {
  margin: 10px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--muted-3);
  text-align: center;
}

.lb-you {
  margin: 12px 0 0;
  padding: 9px 12px;
  border: 2px dashed var(--accent);
  border-radius: 12px;
  text-align: center;
  font-weight: 800;
  color: var(--accent);
}

.lb-empty {
  margin: 0;
  padding: 18px 4px;
  text-align: center;
  color: var(--muted);
  font-size: 15px;
}

/* The topic picker sits under the chip row: a select rather than more chips because a topic is a
   full sentence and there are dozens. The design only reveals it once a category is chosen; ours
   is fed by real data, and hiding it under "All categories" would remove the only way to ask for
   one topic across all of them. */
.lb-filters {
  /* -4 against the card's 14px gap is the design's 10px under the chip row; nothing underneath,
     for the same reason .lb-chips carries nothing. */
  margin: -4px 0 0;
  display: flex;
  justify-content: center;
}

.lb-topic {
  width: 100%;
  max-width: 420px;
  min-width: 0;
  padding: 8px 12px;
  border: 1.5px solid var(--line-2);
  border-radius: 10px;
  background: #ffffff;
  font: inherit;
  font-weight: 800;
  font-size: 13px;
  color: var(--ink);
  cursor: pointer;
  text-overflow: ellipsis;
}

.row--lb-more {
  margin: 10px 0 0;
}

/* Sits directly under the "Published as…" line; tighter, because they are one thought. */
.published--topic {
  margin-top: 2px;
  font-size: 14px;
}

/* ================================================================= rigged cast
 *
 * The animation channels for rig.js. Everything here drives ONE transform on ONE layer; nothing
 * here draws. The custom properties are written per character by rig.js (periods, delays,
 * amplitudes) so that five characters on one stage never move in step — see TIMING there.
 *
 * Amplitudes are in the SVG's own user units, which are the SOURCE ARTWORK's pixels (~1450 tall).
 * A figure is drawn on stage at 265-400px, so a 10-unit rise is about 2 screen pixels. Numbers that
 * look far too small for a screen are correct here; numbers that look right for a screen would
 * throw the head off the neck.
 */

/* The head WANDERS: it rotates, it drifts left and right, and it rises — and the three channels
 * peak at different points in the cycle, so the motion never reverses through its own centre. A
 * head that only rotates reads as a metronome; a head whose rotation and drift are locked together
 * reads as a pendulum. Five stops at irregular intervals is what makes it look like someone
 * shifting their weight rather than a mechanism.
 *
 * The DRIFT is the part that reads as "looking around" on a flat cutout: a real head turning is a
 * yaw, which a 2D layer cannot do, and a small sideways translation is what sells it. Its peaks sit
 * at 0%/58% while the tilt peaks at 20%/79%, so the head is at its furthest sideways when it is
 * near level and most tilted when it is near centred — a wander over two axes rather than a slide
 * and tilt on one track. The first version had them on the same curve, which only scaled the
 * existing motion; QC measured the crown against a pure in-phase model and it matched exactly.
 *
 * Drift is kept small on the character with long hair — see `rise`/`drift` in rig.js — because the
 * hair crosses the head/body cut and travel shears it.
 */
@keyframes dm-rig-sway {
  0%   { transform: translate(calc(var(--drift) * -1),    0)                          rotate(calc(var(--tilt) * -0.2)); }
  20%  { transform: translate(calc(var(--drift) * -0.25), var(--rise))                rotate(calc(var(--tilt) * -1)); }
  41%  { transform: translate(calc(var(--drift) * 0.7),   calc(var(--rise) * 0.3))    rotate(calc(var(--tilt) * -0.15)); }
  58%  { transform: translate(var(--drift),               calc(var(--rise) * 0.75))   rotate(calc(var(--tilt) * 0.85)); }
  79%  { transform: translate(calc(var(--drift) * 0.05),  calc(var(--rise) * 0.25))   rotate(var(--tilt)); }
  100% { transform: translate(calc(var(--drift) * -1),    0)                          rotate(calc(var(--tilt) * -0.2)); }
}

/* Two blinks close together, then a long gap. A single evenly-spaced blink is the tell of a rigged
 * face; real blinking clusters. Each closed phase is ~1.4% of the period, which at a 5s period is
 * the ~70ms a blink actually holds, with a step in and out either side of it.
 *
 * Opacity, not a transform: the closed eyes are painted art (see rig.js), and painted art cannot be
 * slid into place — it is either showing or it is not. The 0.3% ramps are what keep the switch from
 * being a visible hard cut without ever cross-dissolving the two eyes into a double exposure. */
@keyframes dm-rig-blink {
  0%     { opacity: 0; }
  89.8%  { opacity: 1; }
  91.2%  { opacity: 0; }
  95.8%  { opacity: 1; }
  97.2%  { opacity: 0; }
  100%   { opacity: 0; }
}

/* The arm's gesture. Slower than the head and on its own period, because an arm that swings in time
 * with a head reads as a puppet on one string — and with two beats per cycle at different heights,
 * because a single slow sweep reads as swaying rather than as gesturing. */
@keyframes dm-rig-gesture {
  0%   { transform: rotate(calc(var(--swing) * -0.35)); }
  18%  { transform: rotate(calc(var(--swing) * 0.55)); }
  33%  { transform: rotate(calc(var(--swing) * 0.15)); }
  52%  { transform: rotate(var(--swing)); }
  68%  { transform: rotate(calc(var(--swing) * 0.3)); }
  84%  { transform: rotate(calc(var(--swing) * -0.6)); }
  100% { transform: rotate(calc(var(--swing) * -0.35)); }
}

/* The talk cycle: the resting mouth and ONE open mouth, alternating, with the mouth shut slightly
 * more often than not.
 *
 * The wide mouth is deliberately NOT in this loop. It is a full open-jaw shout — tongue and both
 * tooth arches — and shown even once per cycle it flashes, so a speaking character read as shouting
 * on every character. It is kept for `rattled`, where a shocked open mouth is the correct still
 * pose and nothing is flashing.
 *
 * Held opacity, never crossfaded. The 0.1% steps between each pair of stops make this a SWITCH
 * rather than a dissolve — two painted mouths fading through each other produce a smeared
 * double-exposure on a face that is sharp everywhere else. Real 2D lipsync snaps.
 */
@keyframes dm-rig-vis-open {
  0%, 17.9%     { opacity: 0; }
  18%, 38.9%    { opacity: 1; }
  39%, 58.9%    { opacity: 0; }
  59%, 74.9%    { opacity: 1; }
  75%, 100%     { opacity: 0; }
}

/* Brows lift a little on the stressed part of a line. Small — this is emphasis, not surprise. The
 * rotation is held at the expression's value so a talking character keeps whatever brow shape its
 * mood gave it. */
@keyframes dm-rig-brow-talk {
  0%, 100% { transform: translateY(var(--dy)) rotate(var(--rot)); }
  50%      { transform: translateY(calc(var(--dy) - 4px)) rotate(var(--rot)); }
}

.char--rigged .dm-head {
  animation-name: dm-rig-sway;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The forearm's own beat — shorter than the shoulder's and on its own delay, so the two joints
 * never reach their extremes together. An elbow that bends in time with the shoulder it hangs from
 * is one rigid arm with a crease in it. */
/* The forearm LIFTS as well as swings.
 *
 * A forearm hanging straight down and rotating about the elbow moves the hand almost purely
 * sideways — measured, the vertical travel was about one pixel at in-game size, so the hands slid
 * along the thighs and never came away from the body. It read as arms drifting, not as someone
 * making a point. Rotating far enough to raise the hand honestly would fold the arm up, and there
 * is no inside-of-elbow painted to show. A small lift paired with the bend buys the read for a few
 * pixels of travel that the sleeve smear behind the joint already covers. */
@keyframes dm-rig-forearm {
  0%   { transform: translateY(calc(var(--lift) * 0.15)) rotate(calc(var(--bend) * -0.5)); }
  26%  { transform: translateY(var(--lift)) rotate(calc(var(--bend) * 0.7)); }
  45%  { transform: translateY(calc(var(--lift) * 0.3)) rotate(calc(var(--bend) * -0.15)); }
  64%  { transform: translateY(calc(var(--lift) * 0.9)) rotate(var(--bend)); }
  85%  { transform: translateY(calc(var(--lift) * 0.05)) rotate(calc(var(--bend) * 0.2)); }
  100% { transform: translateY(calc(var(--lift) * 0.15)) rotate(calc(var(--bend) * -0.5)); }
}

.char--rigged .dm-arm,
.char--rigged .dm-forearm {
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

.char--rigged .dm-arm { animation-name: dm-rig-gesture; }
.char--rigged .dm-forearm { animation-name: dm-rig-forearm; }

/* `step-end`, not `linear`. Adjacent keyframe stops 0.3% apart still INTERPOLATE, and at a 5s
 * period that is one 60Hz frame of the closed eye drawn at ~66% opacity — the open iris, sclera and
 * catchlight all visible straight through the lid. It reads as a dead, milky eye and QC caught it
 * at 1x. Painted art has no meaningful half-state: it is showing or it is not. */
.char--rigged .dm-blink {
  opacity: 0;
  animation-name: dm-rig-blink;
  animation-timing-function: step-end;
  animation-iteration-count: infinite;
}

/* Both painted mouths are transparent at rest, so the character's own closed mouth shows and the
 * rig is pixel-identical to the source art when nothing is happening. */
.char--rigged .dm-vis {
  opacity: 0;
}

/* Two expressions rest on an open mouth rather than a closed one. */
.char--rigged.rest-open .dm-vis--open,
.char--rigged.rest-wide .dm-vis--wide {
  opacity: 1;
}

/* The per-character delay comes from the markup. Without it five characters open and close their
 * mouths on exactly the same frame, which is the most visible synchrony problem a crowd can have —
 * QC measured all five opening at the identical phases. */
.char--rigged.is-speaking .dm-vis--open {
  animation-duration: 0.62s;
  /* Same reason as the blink: a viseme is painted art, and interpolating between two of them draws
     both at once. */
  animation-timing-function: step-end;
  animation-iteration-count: infinite;
}

.char--rigged.is-speaking .dm-vis--open { animation-name: dm-rig-vis-open; }

/* Translate THEN rotate: the group is already positioned at the brow's inner end by the markup, so
 * the rotation happens about that point and the translation carries it. Swapping the order rotates
 * the brow about a point the translation has already moved. */
.char--rigged .dm-brow {
  transform: translateY(var(--dy)) rotate(var(--rot));
  transition: transform 0.25s ease-out;
}

.char--rigged.is-speaking .dm-brow {
  animation: dm-rig-brow-talk 0.54s ease-in-out infinite;
  transition: none;
}

/* A rigged character is five stacked bitmaps; without this the browser resamples them per frame at
 * whatever quality it feels like, and the head's edge crawls against the body's. */
.char--rigged image {
  image-rendering: auto;
}

/* The rig svg must NOT clip at its viewBox (the UA default for <svg> is `overflow: hidden`). The
 * viewBox margins (MX/MY in rig.js) are sized for the HEAD's travel only — but the gesture POSES
 * are wider than the artwork frame (raised hands overhang it by up to POSE_PAD, see rig.js), and
 * the default clip cut them off in a dead-straight vertical line at lectern height. Un-clipping is
 * free here: it changes no layout box, the figure paints BELOW the lectern image and the desk in
 * DOM order so an overhanging hand can only extend over the room art or behind the lectern, and
 * .set still clips everything at the stage edge. Growing the viewBox instead would change the
 * svg's aspect ratio, and `.figure svg { max-width: 100% }` would then SHRINK the widest
 * characters — a smaller cast traded for the same hands. On the svg root itself so the dev
 * harness (rig-harness.html, which loads this stylesheet) gets it too. */
.char--rigged {
  overflow: visible;
}

/* A PORTRAIT USED AS A LABEL RATHER THAN AS A PERFORMANCE — the name-plate on a fact-check block.
 * Four thumbnails blinking and swaying beside a list of claims is noise, and each one is six running
 * animations over thirteen layers.
 *
 * This is the same stop `prefers-reduced-motion` applies below, and it is placed here, after every
 * `.char--rigged` rule it has to beat, AND written with two classes rather than one. Either alone
 * would be luck: `.char--still .dm-vis` ties `.char--rigged.is-speaking .dm-vis--open` on
 * specificity and would be decided purely by which came last in the file — which is exactly how the
 * first version of `.char--bust` ended up as dead CSS that read like live CSS. */
.char--rigged.char--still .dm-head,
.char--rigged.char--still .dm-arm,
.char--rigged.char--still .dm-forearm,
.char--rigged.char--still .dm-blink,
.char--rigged.char--still .dm-vis,
.char--rigged.char--still .dm-brow,
.char--rigged.char--still .dm-rest,
.char--rigged.char--still .dm-pose {
  animation: none;
}

/* Closed eyes and open mouths hidden (or "no animation" means eyes shut and mouth open forever),
   and hands at the sides, which is the pose the artwork was drawn in — `.dm-rest` is pinned back to
   1 because the pose loop is what normally fades it out. */
.char--rigged.char--still .dm-blink,
.char--rigged.char--still .dm-vis,
.char--rigged.char--still .dm-pose {
  opacity: 0;
}

.char--rigged.char--still .dm-rest {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .char--rigged .dm-head,
  .char--rigged .dm-arm,
  .char--rigged .dm-forearm,
  .char--rigged .dm-blink,
  .char--rigged .dm-vis,
  .char--rigged .dm-brow {
    animation: none;
  }
  /* The closed eyes must stay hidden, or "no animation" means "eyes shut for the whole game". */
  .char--rigged .dm-blink { opacity: 0; }
}


/* ============================================================== gesture poses
 *
 * A speaking character cycles through drawn poses: hands at the sides, then an open palm, back to
 * the sides, a raised finger, back, both hands out. Each pose is a whole alternate body from the
 * neck down, so the switch is a CUT, never a blend — two arrangements of the same arms cross-faded
 * would show four arms.
 *
 * The rest body and the poses are exact complements: whenever a pose is up the resting body is down,
 * because the resting arms hang where a raised arm is not and would otherwise show beside it.
 *
 * The holds are uneven and the returns to rest are short. A gesture that comes back to neutral for
 * the same beat every time reads as a metronome; real speakers hold a hand up across a clause and
 * drop it briefly between them.
 */
@keyframes dm-rig-rest-body {
  0%, 21.9%    { opacity: 1; }
  22%, 39.9%   { opacity: 0; }
  40%, 51.9%   { opacity: 1; }
  52%, 69.9%   { opacity: 0; }
  70%, 79.9%   { opacity: 1; }
  80%, 95.9%   { opacity: 0; }
  96%, 100%    { opacity: 1; }
}

@keyframes dm-rig-pose-palm {
  0%, 21.9%    { opacity: 0; }
  22%, 39.9%   { opacity: 1; }
  40%, 100%    { opacity: 0; }
}

@keyframes dm-rig-pose-point {
  0%, 51.9%    { opacity: 0; }
  52%, 69.9%   { opacity: 1; }
  70%, 100%    { opacity: 0; }
}

@keyframes dm-rig-pose-both {
  0%, 79.9%    { opacity: 0; }
  80%, 95.9%   { opacity: 1; }
  96%, 100%    { opacity: 0; }
}

/* A pose only exists while the character is speaking. Standing at a lectern listening, they keep
   their hands at their sides. */
.char--rigged .dm-pose {
  opacity: 0;
}

.char--rigged.is-speaking.has-gestures .dm-rest,
.char--rigged.is-speaking.has-gestures .dm-pose {
  animation-timing-function: step-end;
  animation-iteration-count: infinite;
}

.char--rigged.is-speaking.has-gestures .dm-rest { animation-name: dm-rig-rest-body; }
.char--rigged.is-speaking.has-gestures .dm-pose--palm { animation-name: dm-rig-pose-palm; }
.char--rigged.is-speaking.has-gestures .dm-pose--point { animation-name: dm-rig-pose-point; }
.char--rigged.is-speaking.has-gestures .dm-pose--both { animation-name: dm-rig-pose-both; }

@media (prefers-reduced-motion: reduce) {
  .char--rigged .dm-rest,
  .char--rigged .dm-pose {
    animation: none;
  }
  /* Hands at the sides, which is the pose the artwork was drawn in. */
  .char--rigged .dm-rest { opacity: 1; }
  .char--rigged .dm-pose { opacity: 0; }
}

/* --------------------------------------------------------------------------
   Overlays: the pause cover and the How to Play card.

   Both sit ABOVE every screen rather than being one — `show()` swaps screens and would hide the
   debate under a help card, which is exactly what must not happen when the rules are opened mid
   turn. z-index 30 clears the top bar (12) and the card layer (8) and stays under the account
   menu (40), whose anchor is a top-bar control the overlay must not swallow. `margin: auto` on the
   card is the same rule .screen--card uses: it centres when it fits and scrolls when it does not,
   where align-items would cut the top off a tall card. */
.overlay {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: flex;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 16px clamp(10px, 4vw, 28px);
  background: rgba(12, 15, 20, 0.55);
  backdrop-filter: blur(3px);
}

.overlay .card {
  margin: auto;
}

/* Dark enough that neither the transcript nor the draft can be read through it: a pause is
   thinking time, not reading time. */
#pause-overlay {
  background: rgba(12, 15, 20, 0.93);
}

.card--pause {
  width: min(520px, 100%);
}

/* The rules card is a column with ONE scrolling region — the body — so Got it never leaves the
   screen on a landscape phone, however long the text (the Bladevolley sheet learned this). */
.card--help {
  width: min(640px, 100%);
  max-height: calc(100dvh - 32px);
  text-align: left;
}

.card--help h2 {
  text-align: center;
}

.help-body {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-right: 4px;
  font-size: 14.5px;
  line-height: 1.45;
  color: var(--ink-2);
}

.help-foot {
  flex: 0 0 auto;
}

.howto-section h3 {
  margin: 0 0 4px;
  font-family: var(--display);
  font-weight: 800;
  font-size: 15px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--bronze-lo);
}

.howto-section p {
  margin: 0 0 6px;
}

.howto-turns {
  margin: 0 0 8px;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 4px;
}

.howto-turns li {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 12px;
  padding: 6px 10px;
  border-radius: 10px;
  background: var(--accent-wash);
  border: 1px solid rgba(255, 106, 61, 0.22);
}

.howto-turns li span {
  color: var(--muted-2);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.howto-criteria {
  margin: 0 0 8px;
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 12px;
}

.howto-criteria dt {
  font-weight: 800;
  color: var(--ink);
}

.howto-criteria dd {
  margin: 0;
  color: var(--muted-2);
}

.howto-controls {
  margin: 0;
  padding-left: 18px;
  display: grid;
  gap: 4px;
}

/* THE CARD'S ONE PICTURE: a finished verdict, drawn by the verdict screen's own painters
   (verdict-view.js) and labelled. It is deliberately dressed as a specimen rather than as the real
   screen — inset on white, hairline-framed — because a real-looking scorecard on a help card is
   otherwise read as the player's own first result. */
.howto-verdict {
  margin: 0 0 8px;
  padding: 12px 14px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.6);
  border: 1px solid rgba(18, 22, 30, 0.12);
  display: grid;
  gap: 10px;
}

/* The specimen's own head cell carries a label where the live card's is empty, so the example says
   whose side it is showing without a caption doing it. */
.howto-verdict .score-head .label {
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1px;
  color: var(--muted);
}

.howto-points {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  padding-top: 8px;
  border-top: 1px solid rgba(18, 22, 30, 0.1);
}

.howto-points .label {
  font-family: var(--display);
  font-weight: 800;
  font-size: 11px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--muted);
}

.howto-points b {
  font-family: var(--display);
  font-size: 19px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.howto-points-sum {
  font-size: 13px;
  font-weight: 700;
  color: var(--muted-2);
  font-variant-numeric: tabular-nums;
}

.howto-verdict figcaption {
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--muted-2);
  text-align: left;
}

/* The example coaching keeps the verdict's own treatment and drops its heading's top margin, since
   the section's own heading is already above it. */
.coaching--example {
  margin: 0 0 6px;
}

/* The ? disc: the display face's own question mark, sized to read as the mute disc's emoji does. */
.help-glyph {
  font-family: var(--display);
  font-weight: 900;
  font-size: 19px;
  line-height: 1;
}

/* A paused ring wears the courtroom's gold: the number stays (it is what will resume), and the
   colour says why it is not moving. */
.timer-ring.is-paused .sweep {
  stroke: var(--gold);
}

.timer-ring.is-paused .value {
  color: var(--gold-lo);
}

@media (max-width: 620px) {
  .card--help {
    max-height: calc(100dvh - 24px);
  }

  .howto-criteria {
    grid-template-columns: 1fr;
    gap: 2px;
  }

  .howto-criteria dd {
    margin-bottom: 6px;
  }
}

/* --------------------------------------------------------------------------
   The phone's debate screen: composer first.

   Above 620px the debate screen is a pinned stage layout — HUD at the top, two speech columns
   flanking, the input card pinned to the bottom over the room. On a phone that put the text box
   under the transcript and behind the keyboard, and a player typing an answer could see neither
   the clock nor the words they were answering. Here the screen is a FLOW COLUMN ordered by whose
   turn it is: HUD (with the clock) → the composer → the opponent's last speech, collapsed on one
   line under it → the full transcript, scrolling. On the opponent's turn the speech comes first
   and the composer is not there. The base rules keep their absolute positioning (the desktop
   arithmetic that guarantees the conversation never lands on the characters is untouched); every
   override here is `position: static`, so nothing below is measured against --podium-h.

   The cast is hidden on this screen at phone width for the same reason the 470px height rule
   hides it: with the composer first, the transcript would otherwise paint over the debaters, and
   the painted room behind it is what keeps the screen the same place. */
@media (max-width: 620px) {
  .screen--debate {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 6px 8px 8px;
    overflow: hidden;
  }
  body[data-screen="debate"] .set {
    display: none;
  }
  .hud {
    position: static;
    transform: none;
    max-width: 100%;
    order: 0;
    flex: 0 0 auto;
  }
  .hud-card {
    width: 100%;
    justify-content: space-between;
  }
  .compose {
    position: static;
    transform: none;
    width: 100%;
    order: 1;
    flex: 0 0 auto;
  }
  /* Room to write: the whole point of the layout. Clamped so a short keyboard-up viewport still
     shows End turn under it, and never taller than a third of a tall phone. */
  .draft {
    height: clamp(88px, 26dvh, 240px);
    max-height: 30dvh;
  }
  /* Two classes, because the base `.last-speech { display: none }` below this block is later in
     the file and would win a single-class rule at equal specificity. */
  .screen--debate .last-speech {
    display: block;
    order: 2;
    flex: 0 0 auto;
  }
  .histories {
    position: static;
    order: 3;
    flex: 1 1 auto;
    min-height: 48px;
  }
  .waiting {
    position: static;
    order: 4;
    flex: 0 0 auto;
    margin: 0 auto;
  }
  /* The opponent's turn: their speech is the thing on screen, and there is nothing to answer yet. */
  body[data-turn="ai"] .histories {
    order: 1;
  }
  body[data-turn="ai"] .last-speech {
    display: none;
  }
}

/* The disclosure itself. A summary line styled as a quiet bubble; open, the full bubble the
   transcript also shows, scrolling inside so a long speech cannot push the transcript away. */
.last-speech {
  display: none;
  background: var(--paper);
  border: 1px solid rgba(18, 22, 30, 0.14);
  border-radius: 14px;
  box-shadow: var(--lift-shadow);
  overflow: hidden;
}

.last-speech summary {
  list-style: none;
  cursor: pointer;
  padding: 10px 12px 10px 30px;
  min-height: 44px;
  display: flex;
  align-items: center;
  position: relative;
  font-size: 13px;
  font-weight: 800;
  color: var(--ink-2);
  line-height: 1.3;
}

.last-speech summary::-webkit-details-marker {
  display: none;
}

/* A drawn chevron, turned when open — not the UA's triangle, which is a different picture on
   every platform. */
.last-speech summary::before {
  content: "";
  position: absolute;
  left: 12px;
  top: 50%;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  transform: translateY(-70%) rotate(45deg);
  transition: transform 0.15s ease;
}

.last-speech[open] summary::before {
  transform: translateY(-30%) rotate(-135deg);
}

.last-speech-body {
  max-height: 34dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 8px 8px;
}

.last-speech-body .bubble {
  box-shadow: none;
  width: auto;
  align-self: auto;
  animation: none;
}

/* Touch targets: a thumb needs 44px, and a 36px glass disc or a 38px pill is what a mouse was
   given. Affordance rules are POINTER questions (the note at the top of this file), so this is
   gated on the pointer rather than on width. The discs grow inside a bar whose height is
   authored (--topbar-h, 55px at phone width), so nothing under the bar moves. */
@media (pointer: coarse) {
  .topbar .ghost-btn {
    width: 44px;
    height: 44px;
  }
  .compose-row .big-btn,
  #btn-ready,
  .help-foot .big-btn,
  .card--pause .big-btn,
  .card--pause .ghost-btn,
  #deliberation-recovery .ghost-btn {
    min-height: 44px;
  }

  /* EVERY control on a card, not the handful somebody remembered. The rule above named the
     composer, the ready card, the pause cover and the recovery row and stopped there, so a sweep
     of every shipped control at 375x812 found the rest of them under the line: each screen's
     `← Back` at 50x19 — the only way out of six pickers — the board's two window tabs at 37, and
     the secondary buttons (Random claim, Random side, Watch an AI exhibition, Main menu,
     Challenge a friend) at 41-42.

     `← Back` takes the height through padding and gives it back with a negative margin, because it
     hangs at the top of a card whose own 14px gap sets the distance to the heading below it, and
     that heading is not a control: growing a box past a NON-interactive neighbour costs nothing,
     where growing it past an interactive one steals the neighbour's taps (see .trophy-btn). */
  .back-link {
    min-height: 44px;
    padding: 12px 6px;
    margin: -12px -6px;
  }

  .screen .ghost-btn,
  .lb-tab {
    min-height: 44px;
  }

  /* `.link-btn` is deliberately NOT in that list. It is a word inside a sentence — "vs Mei Point —
     change", "sign in to put this on the leaderboard" — and a 44px box around one word either
     reaches into the lines above and below it or pushes the sentence apart. An inline link in a
     block of text is the one target size rules exempt, and for this reason. */
}

/* --------------------------------------------------------------------------
   The judge's coaching: pull quotes above the bars.

   The teaching payoff, first: the line that did most for the player, the line that cost them most,
   and one thing to do next time — before the scorecard, because the scorecard says how much and
   these say why. Empty (and gone) for an exhibition and for a debate stored before the coaching
   existed. A quote the Worker could not find verbatim in the debater's own turns is drawn as the
   judge's reading, roman rather than italic and without quotation marks. */
.coaching {
  display: grid;
  gap: 8px;
  text-align: left;
}

.coaching:empty {
  display: none;
}

.coaching h3 {
  margin: 0;
  font-family: var(--display);
  font-size: 13px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 800;
}

.coaching-item {
  border-left: 3px solid var(--gold);
  padding: 2px 0 2px 12px;
}

.coaching-item--strongest { border-left-color: var(--pro-bar); }
.coaching-item--costliest { border-left-color: var(--con-bar); }
.coaching-item--next_time { border-left-color: var(--accent); }

.coaching-item h4 {
  margin: 0 0 2px;
  font-family: var(--display);
  font-size: 13px;
  font-weight: 800;
  color: var(--bronze-lo);
}

.coaching-line {
  margin: 0;
  font-weight: 700;
  font-size: calc(14.5px * var(--prose));
  line-height: 1.4;
  color: var(--ink-2);
}

.coaching-quote {
  font-style: italic;
}

@media (max-height: 700px) {
  .coaching {
    gap: 5px;
  }
  .coaching-line {
    font-size: calc(13.5px * var(--prose));
  }
}

/* Phone width: the whole block moves to the second mount, under the action row. Measured at
   390x664 on a canned verdict with two ordinary quotes: above the bars it put Rematch 123px past
   the fold, and "next time" alone still 64px past it, where the card had been 92px clear. The
   primary actions come before the detail (lessons.md), so the phone reads the coaching after
   Rematch, and the desktop keeps it above the bars where the why precedes the how much. */
.coaching--quotes {
  display: none;
}

@media (max-width: 620px) {
  .coaching--quotes {
    display: grid;
  }
  .coaching--quotes:empty {
    display: none;
  }
  .coaching:not(.coaching--quotes) {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   The verdict's arrival: the recovery row on the deliberation card, and the staggered reveal.

   The judge's answer streams from the Worker as progress (which phase she is in, a heartbeat
   while the model is silent) and then the verdict WHOLE, once it has been parsed and stored — so
   the reveal is paced here, on the client, from a verdict that cannot be contradicted by a retry.
   Rows tick in one criterion at a time, then the totals, then the badge; the gavel lands with the
   badge (main.js REVEAL_ROW_MS). Reduced motion collapses the stagger with every other duration. */
.deliberation-recovery {
  display: grid;
  gap: 10px;
}

#deliberation-recovery-note {
  color: var(--con-ink);
}

/* A row that is arriving holds its start frame until its turn; --i is written by scoreRowsHtml. */
.card.is-revealing .score-row {
  animation: dm-row-in 0.35s ease-out both;
  animation-delay: calc(var(--i, 0) * var(--reveal-row, 350ms));
}

.card.is-revealing .score-total,
.card.is-revealing .win-row {
  animation: dm-row-in 0.35s ease-out both;
  animation-delay: calc(var(--rows, 4) * var(--reveal-row, 350ms));
}

@keyframes dm-row-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

/* ---- You & friends, and a dare ----
   The friends block sits above the global rows on the board, painted with the board's own row
   template plus a Remove control on the right. */
.lb-friends {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 14px 0 10px;
  padding-bottom: 12px;
  border-bottom: 1px solid rgba(60, 40, 20, 0.16);
}
.lb-friends[hidden] { display: none !important; }
.lb-friends-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.lb-friends-heading {
  margin: 0;
  font-size: 0.82rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.75;
}
.lb-friends-btn { font-size: 0.82rem; padding: 6px 12px; }
.lb-friends-list { margin: 0; padding: 0; list-style: none; }
/* A fifth, auto-sized column for the Remove control; the board's own rows keep their four. */
.lb-friends-list .lb-row { grid-template-columns: 34px minmax(0, 1fr) auto 86px auto; }
.lb-row--unranked { opacity: 0.75; }
.lb-unranked { font-size: 0.8rem; font-style: italic; opacity: 0.85; }
.lb-remove {
  margin-left: auto;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.78rem;
  text-decoration: underline;
  color: inherit;
  cursor: pointer;
  opacity: 0.75;
}
.lb-remove:hover, .lb-remove:focus-visible { opacity: 1; }
.lb-friends-note { margin: 0; font-size: 0.85rem; opacity: 0.8; }

/* The dare card on the plaque: the blurb's paper, a headline in it. */
.dare-card {
  margin-top: 10px;
  padding: 10px 14px;
  border: 1px solid rgba(196, 136, 42, 0.55);
  border-radius: 10px;
  background: rgba(196, 136, 42, 0.1);
}
.dare-headline { display: block; margin-bottom: 3px; font-weight: 800; }
/* Armed: the next press removes. */
.lb-remove.confirming { opacity: 1; font-weight: 800; }
