/* Joe C. Fitzgerald's Conveyor — white, minimal, one accent. */

:root {
  --ink: #111;
  --muted: #666;
  --faint: #999;
  --line: #eaeaea;
  --surface: #fafafa;
  --accent: #0070f3;
  --accent-soft: rgba(0, 112, 243, 0.1);
  /* the sensor reads as its own signal, not as the UI accent */
  --sensor: #e00;
  --sensor-soft: rgba(238, 0, 0, 0.12);
  /* the clock running out — amber first, then the same red the sensor uses.
     Both are darkened enough to clear 4.5:1 on white at any weight. */
  --warn: #b45800;
  --warn-soft: rgba(180, 88, 0, 0.1);
  --urgent: #d40000;
  --urgent-soft: rgba(212, 0, 0, 0.1);
  /* the field an off call-to-action is waiting on — a hint, not an error, so
     it sits well under the sensor red the invalid rows use */
  --needs: rgba(212, 0, 0, 0.34);
  --needs-ring: rgba(212, 0, 0, 0.2);
  --needs-soft: rgba(212, 0, 0, 0.035);
  --radius: 10px;

  /* ---- the low-angle view --------------------------------------------
     One orthographic camera 13.5 degrees above the belt plane, with every
     package yawed on the belt so three of its faces read at once: lid,
     front, and the turned side. Yawing the box rather than the camera is
     what keeps the belt running dead horizontal on screen, so every
     position in app.js stays a pure translateX.

     The block below is ONE package, at 14 degrees: the default, and the one
     the instruction slides draw. On the belt each box is rendered at its own
     yaw and overrides all of it per `.car` — see app.js, deriveBox.

     Orthographic on purpose: the run is laid out on one constant slot
     pitch, and a perspective camera would make that pitch depend on where
     in the window a box happened to sit.

     A unit of belt DEPTH is worth sin(13.5) = 0.2334px of screen height,
     a unit of HEIGHT cos(13.5) = 0.9724px. Locked in design/belt-lab.html;
     re-dial it there and press "Lock in these values" to regenerate this
     block, the sprite and the two app.js pivots. */
  --box-sprite: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%20128.75%20101.82'%3E%3Cdefs%3E%3ClinearGradient%20id%3D'lid'%20x1%3D'0'%20y1%3D'0'%20x2%3D'.28'%20y2%3D'1'%3E%3Cstop%20offset%3D'0'%20stop-color%3D'%23ffe6a2'%2F%3E%3Cstop%20offset%3D'1'%20stop-color%3D'%23f4c552'%2F%3E%3C%2FlinearGradient%3E%3ClinearGradient%20id%3D'front'%20x1%3D'0'%20y1%3D'0'%20x2%3D'0'%20y2%3D'1'%3E%3Cstop%20offset%3D'0'%20stop-color%3D'%23eb9d25'%2F%3E%3Cstop%20offset%3D'1'%20stop-color%3D'%23d98511'%2F%3E%3C%2FlinearGradient%3E%3ClinearGradient%20id%3D'side'%20x1%3D'0'%20y1%3D'0'%20x2%3D'1'%20y2%3D'.4'%3E%3Cstop%20offset%3D'0'%20stop-color%3D'%23c67b14'%2F%3E%3Cstop%20offset%3D'1'%20stop-color%3D'%23ae670c'%2F%3E%3C%2FlinearGradient%3E%3ClinearGradient%20id%3D'tape'%20x1%3D'0'%20y1%3D'0'%20x2%3D'.4'%20y2%3D'1'%3E%3Cstop%20offset%3D'0'%20stop-color%3D'%23f8eec5'%2F%3E%3Cstop%20offset%3D'1'%20stop-color%3D'%23ebdb9d'%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cpolygon%20points%3D'0.00%2C95.55%20107.70%2C101.82%20128.75%2C82.11%20128.75%2C6.27%2021.05%2C0.00%200.00%2C19.71'%20fill%3D'none'%20stroke%3D'rgba(0%2C0%2C0%2C.10)'%20stroke-width%3D'1'%2F%3E%3Cpolygon%20points%3D'0.00%2C19.71%2021.05%2C0.00%20128.75%2C6.27%20107.70%2C25.98'%20fill%3D'url(%23lid)'%2F%3E%3Cpolygon%20points%3D'0.00%2C19.71%20107.70%2C25.98%20107.70%2C101.82%200.00%2C95.55'%20fill%3D'url(%23front)'%2F%3E%3Cpolygon%20points%3D'107.70%2C25.98%20128.75%2C6.27%20128.75%2C82.11%20107.70%2C101.82'%20fill%3D'url(%23side)'%2F%3E%3Cpolygon%20points%3D'42.54%2C22.18%2063.59%2C2.48%2086.21%2C3.79%2065.16%2C23.50'%20fill%3D'url(%23tape)'%2F%3E%3Cpolygon%20points%3D'42.54%2C22.18%2065.16%2C23.50%2065.16%2C53.84%2053.85%2C46.37%2042.54%2C52.52'%20fill%3D'%23e7d79b'%2F%3E%3Cpolygon%20points%3D'72.70%2C79.36%2098.55%2C80.87%2098.55%2C91.56%2072.70%2C90.06'%20fill%3D'%23211e1a'%20opacity%3D'.88'%2F%3E%3Cline%20x1%3D'0.00'%20y1%3D'19.71'%20x2%3D'107.70'%20y2%3D'25.98'%20stroke%3D'rgba(0%2C0%2C0%2C.17)'%20stroke-width%3D'1.1'%2F%3E%3Cline%20x1%3D'107.70'%20y1%3D'25.98'%20x2%3D'128.75'%20y2%3D'6.27'%20stroke%3D'rgba(0%2C0%2C0%2C.17)'%20stroke-width%3D'1.1'%2F%3E%3Cline%20x1%3D'107.70'%20y1%3D'25.98'%20x2%3D'107.70'%20y2%3D'101.82'%20stroke%3D'rgba(0%2C0%2C0%2C.17)'%20stroke-width%3D'1.1'%2F%3E%3C%2Fsvg%3E");
  --box-w: 128.75px;      /* the sprite is its silhouette's bounding box */
  --box-h: 101.82px;
  --contact: 12.99px;     /* how far its lowest corner sits below the belt's centre line */

  /* The number is stencilled INTO the front face's plane, which from this
     angle is a parallelogram: --face-* is that face's top-left corner in the
     sprite, the face in its own units, and the projection that maps those
     units to screen px. See .car .car-num. */
  --face-left: 0px;
  --face-top: 19.35%;
  --face-w: 111px;
  --face-h: 78px;
  --face-matrix: matrix(0.9703, 0.0565, 0, 0.9724, 0, 0);
  /* and where on that face it is painted, in the face's own units: low and to
     the left, which is the one place a two-digit print fits without crossing
     anything — below the tape flap (which hangs 31 units down over x 44→67) and
     left of the label (x 75→109). --num-x is where the print STARTS, so the
     widest number, 58, runs 6→57 of the face's 111. */
  --num-size: 40px;
  --num-x: 6px;
  --num-y: 14px;
  --num-lift: 0px;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --shadow-float: 0 8px 30px rgba(0, 0, 0, 0.08);
  --cta-bottom: 34px;  /* the bar's breathing room from the window edge */
  --cta-space: 136px;  /* what each screen reserves so content clears it */
}

* { box-sizing: border-box; }

[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: #fff;
  color: var(--ink);
  font-family: "Geist Sans", "Geist", -apple-system, "SF Pro Text", "Segoe UI", system-ui, sans-serif;
  font-size: 17px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

#app { min-height: 100vh; }

.screen {
  position: relative;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  padding-bottom: var(--cta-space); /* room for the fixed action bar */
}
.screen[hidden] { display: none; }

/* auto margins rather than align-items:center — they centre the same way but
   collapse instead of clipping when the content is taller than the window */
.screen-inner {
  width: 100%;
  margin: auto;
  padding: 48px 32px 8px;
  /* 'backwards', not 'both': a forwards fill leaves an identity transform
     behind, which would make this a containing block for the fixed CTA bar */
  animation: screen-in 420ms var(--ease) backwards;
}
.screen-inner.narrow { max-width: 640px; }
.screen-inner.wide { max-width: 1020px; }
.screen-inner.center { display: flex; flex-direction: column; align-items: center; }

/* ---------- the fixed call-to-action bar ---------- */

/* Every screen puts its action in the same spot: bottom centre, floating over
   the page, so the button never moves as the class steps through the round.
   The strip spans the full width, so it must not swallow clicks. */
.cta-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--cta-bottom);
  z-index: 25;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0;
  padding: 0 24px;
  pointer-events: none;
}
.cta-bar > * { pointer-events: auto; }
.cta-bar.stacked { flex-direction: column; gap: 14px; }
.cta-bar.stacked.tight { gap: 10px; }

/* a quiet aside under a CTA — says what the press will do, without competing
   with the button for weight */
.cta-note { font-size: 14px; font-weight: 400; color: var(--muted); margin: 0; text-align: center; }

@keyframes screen-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}

/* The entrance without the rise. Any box that *contains* a fixed .cta-bar must
   use this one: a transformed ancestor becomes the containing block for its
   position:fixed descendants, so for the length of a transform animation the
   bar anchors to that box — it lands mid-screen — and then snaps down to the
   viewport the instant the transform lifts. Opacity is safe (it makes a
   stacking context, not a containing block), so the bar still fades in with
   its screen; it just never moves. */
@keyframes screen-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* The stage is the one screen that nests its bars inside the content — Confirm
   bet, the transport, and the winner actions all live in .screen-inner — so it
   fades in rather than rising. */
#screen-stage > .screen-inner { animation-name: screen-fade; }

/* ---------- screen navigation: back, and the stage's options menu ---------- */

/* Both corners float over the screen rather than sitting in the flow, so
   adding them never shifts a layout that was composed without them. */
.nav-btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  color: var(--muted);
  background: none;
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 7px 13px;
  cursor: pointer;
  transition: color 150ms var(--ease), background 150ms var(--ease),
    border-color 150ms var(--ease);
}
.nav-btn:hover { color: var(--ink); background: var(--surface); border-color: var(--line); }
.nav-btn .nav-glyph { font-size: 16px; line-height: 1; }
.nav-btn .caret { font-size: 11px; }

.screen-back { position: absolute; top: 20px; left: 20px; z-index: 20; }

.screen-menu { position: absolute; top: 20px; right: 20px; z-index: 20; }
.screen-menu.open .menu-toggle { color: var(--ink); background: var(--surface); border-color: var(--line); }

.menu-list {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 244px;
  padding: 6px;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-float);
  animation: menu-in 160ms var(--ease) both;
}
@keyframes menu-in {
  from { opacity: 0; transform: translateY(-6px) scale(0.98); }
  to { opacity: 1; transform: none; }
}

.menu-item {
  appearance: none;
  display: block;
  width: 100%;
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  text-align: left;
  color: var(--ink);
  background: none;
  border: none;
  border-radius: 7px;
  padding: 9px 11px;
  cursor: pointer;
  transition: background 120ms var(--ease);
}
.menu-item:hover { background: var(--surface); }
.menu-note {
  display: block;
  font-size: 13px;
  font-weight: 400;
  color: var(--faint);
  margin-top: 2px;
}

@media (max-width: 600px) {
  .screen-back { top: 12px; left: 12px; }
  .screen-menu { top: 12px; right: 12px; }
  .menu-list { min-width: 208px; }
}

@media (prefers-reduced-motion: reduce) {
  .menu-list { animation: none; }
}

/* ---------- type ---------- */

/* Only the design labs draw the old masthead now; the app's title screen is
   the poster below. Kept so the labs' "Shipped" baselines still render. */
.masthead { text-align: center; margin-bottom: 40px; }
.wordmark { font-size: 44px; font-weight: 600; letter-spacing: -0.03em; margin: 0; }
.tagline { color: var(--muted); margin: 8px 0 0; }

/* ---------- the title screen: the poster over the ambient belt ----------
   Locked from design/title-screen-studies.html, study B · Poster: the
   possessive as an eyebrow, the machine's name at display size, the CTAs in
   the lockup — and the conveyor running along the bottom edge of the screen,
   unattended (see startAmbient in app.js). Spacing locked from the lab's
   dials: 22vh top, 18px eyebrow gap, title at 100%, 6vh to the CTAs. */
.title-screen {
  flex-direction: column;
  align-items: center;
  padding-bottom: 0;              /* the belt owns the bottom edge */
}
.title-head {
  margin-top: 22vh;
  text-align: center;
  animation: screen-in 420ms var(--ease) backwards;
}
.title-eyebrow {
  display: block;
  font-size: clamp(13px, 1.5vw, 17px);
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--muted);
}
.title-display {
  margin: 18px 0 0;
  font-size: clamp(64px, 11.5vw, 158px);
  font-weight: 600;
  letter-spacing: -0.05em;
  line-height: 0.96;
}
/* the CTAs sit in the lockup, not in the fixed bar every other screen uses */
.title-screen .start-actions {
  position: static;
  margin-top: 6vh;
  padding: 0;
  animation: screen-in 420ms var(--ease) backwards;
}
/* the credit: the last thing in the lockup and the quietest thing on the
   screen — --faint, one step below the eyebrow, and it fades in behind the
   CTAs rather than arriving with them */
.title-credit {
  margin: 20px 0 0;
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--faint);
  text-align: center;
  animation: screen-fade 560ms var(--ease) backwards;
}

/* The machine as a BAND: full bleed out of both sides of the window, so the
   card's chrome comes off — no max-width, no rounding, no white of its own.
   Legless on purpose: with nothing standing in a room, the belt floats on the
   screen's bottom edge, so the window crops just under the frame. */
.title-hero {
  width: 100%;
  margin-top: auto;
  animation: screen-fade 560ms var(--ease) backwards;
}
.title-hero .machine { margin-top: 0; }
.title-hero .window {
  max-width: none;
  height: 296px;                  /* the frame's underside, then the edge */
  border-radius: 0;
  background: transparent;
}

.screen-title { font-size: 32px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 8px; }
.screen-title.small { font-size: 24px; margin-bottom: 20px; }
.panel-title { font-size: 18px; font-weight: 600; margin: 0 0 6px; }
.mini-title {
  display: block;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 0 8px;
}
.hint { color: var(--muted); font-size: 15px; margin: 0 0 16px; }
.lede { color: var(--muted); font-size: 20px; margin: 0 0 28px; }
.caption { color: var(--muted); font-size: 15px; text-align: center; margin: 10px 0 0; min-height: 1.5em; }

/* ---------- panels, buttons, inputs ---------- */

.panel {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 24px;
  margin-bottom: 20px;
}
.panel.soft { background: var(--surface); }

.btn {
  appearance: none;
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  color: var(--ink);
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 9px 18px;
  cursor: pointer;
  transition: background 150ms var(--ease), border-color 150ms var(--ease),
    color 150ms var(--ease), transform 150ms var(--ease), opacity 150ms var(--ease);
}
/* Hover is gated on :not(:disabled) rather than undone afterwards: a blanket
   .btn:disabled:hover would out-specify .btn.primary and strip a locked CTA
   back to white — white text on white, faded to nothing.
   [aria-disabled] rides along everywhere :disabled does: a CTA the round is
   not ready for wears the locked look but keeps the press, which is what
   raises the ring on the field in the way. See setBlocked in app.js. */
.btn:hover:not(:disabled):not([aria-disabled="true"]) { border-color: #ccc; background: var(--surface); }
.btn:active:not(:disabled):not([aria-disabled="true"]) { transform: scale(0.98); }
.btn:disabled,
.btn[aria-disabled="true"] { opacity: 0.4; cursor: default; transform: none; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover:not(:disabled):not([aria-disabled="true"]) { background: #0060d1; border-color: #0060d1; }
.btn.big { font-size: 17px; padding: 13px 28px; border-radius: 9px; }

:is(button, input, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

input[type="text"], input[type="number"] {
  font: inherit;
  color: var(--ink);
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 8px 12px;
  transition: border-color 150ms var(--ease), background 300ms var(--ease),
    box-shadow 150ms var(--ease);
}
input[type="text"]:hover, input[type="number"]:hover { border-color: #ccc; }
input[type="text"]:focus, input[type="number"]:focus { border-color: var(--accent); outline: none; }
input:disabled { background: var(--surface); color: var(--muted); }

/* The answer to a press that could not go through. A greyed-out button only
   says "not yet"; this says WHERE — the empty field standing between the
   class and the press wears a faint red ring, drawn inside its own edge so
   nothing in the row moves when it comes and goes. Faint on purpose: nothing
   has gone wrong, something is just missing, and the loud red is spoken for
   by the invalid rows.
   It is never on by default. It arrives only when the CTA is actually
   pressed, and leaves the moment the field is filled — see markNeeded() and
   the flag/paint routines in app.js.
   The :hover and :focus pairs are carried because the base input rules
   out-specify a bare class and would otherwise paint the ring away under the
   pointer, or the moment the press moves the caret into the field. */
input.needs,
input.needs:hover,
input.needs:focus {
  border-color: var(--needs);
  box-shadow: inset 0 0 0 1px var(--needs-ring);
  background-color: var(--needs-soft);
}
/* Focus still has to read as focus, and the ring has taken the border — so
   focus is shown the way the rest of the app shows it on a keyboard, as the
   accent outline sitting outside the box. */
input.needs:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The one case with no field to ring: an empty roster. The button that makes
   one carries the mark instead, so the press still points somewhere. */
.btn.needs,
.btn.needs:hover:not(:disabled) {
  border-color: var(--needs);
  box-shadow: inset 0 0 0 1px var(--needs-ring);
  background-color: var(--needs-soft);
}
input[type="number"] { width: 90px; font-variant-numeric: tabular-nums; }

.field-row { display: flex; align-items: center; gap: 14px; margin: 14px 0; flex-wrap: wrap; }
.field-row label { font-weight: 500; }
.field-row .readout { margin: 0; }

/* ---------- screen 2: the setup bento ---------- */

.bento { display: grid; gap: 14px; margin-top: 24px; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.bento .tile-roster { grid-column: 1 / -1; }

.tile {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 20px;
  min-width: 0;
}
.tile.soft { background: var(--surface); }
.tile-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.tile-label {
  display: block;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
}
.count { font-size: 13px; color: var(--faint); font-variant-numeric: tabular-nums; }
.tile-foot { color: var(--muted); font-size: 14px; margin: 0 0 0 8px; }

/* the dial: a big, editable numeral flanked by steppers.
   .pdial is the practice room's version of the same object — the numeral
   without the steppers — so it rides these rules and overrides only its
   width and alignment. See "the practice room" below. */
.dial { display: flex; align-items: center; gap: 14px; margin-top: 6px; flex-wrap: wrap; }
.dial .dial-value,
.pdial .dial-value {
  font: inherit;
  font-size: 54px;
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  width: 2.6ch;
  padding: 2px 0;
  text-align: center;
  color: var(--ink);
  background: none;
  border: 1px solid transparent;
  border-radius: 10px;
  transition: border-color 150ms var(--ease), background 150ms var(--ease);
}
.dial .dial-value.time { font-size: 44px; width: 4.4ch; }
.dial .dial-value:hover,
.pdial .dial-value:hover { border-color: var(--line); background: var(--surface); }
.dial .dial-value:focus,
.pdial .dial-value:focus { border-color: var(--accent); background: #fff; outline: none; }
.dial .dial-value::-webkit-outer-spin-button,
.dial .dial-value::-webkit-inner-spin-button,
.pdial .dial-value::-webkit-outer-spin-button,
.pdial .dial-value::-webkit-inner-spin-button { appearance: none; margin: 0; }
.dial .dial-value[type="number"],
.pdial .dial-value[type="number"] { appearance: textfield; -moz-appearance: textfield; }

.step {
  font: inherit;
  font-size: 22px;
  line-height: 1;
  width: 42px;
  height: 42px;
  flex: none;
  background: #fff;
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  transition: border-color 150ms var(--ease), background 150ms var(--ease), transform 150ms var(--ease);
}
.step:hover { border-color: #ccc; background: var(--surface); }
.step:active { transform: scale(0.94); }
.step:disabled { opacity: 0.35; cursor: default; transform: none; }
.step:disabled:hover { border-color: var(--line); background: #fff; }

/* the range dial: two of the numerals sharing one row, each with its own
   steppers — everything a notch smaller so min–max fits where one number
   used to sit */
.dial.range { gap: 8px; }
.dial.range .dial-value { font-size: 44px; width: 2.4ch; }
.dial.range .step { width: 36px; height: 36px; font-size: 20px; }
.range-dash { font-size: 30px; font-weight: 600; color: var(--faint); padding: 0 2px; }

/* players as a wrapping mosaic rather than one tall column */
.player-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.player-chip {
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 7px 8px;
  min-width: 0;
  animation: screen-in 300ms var(--ease) both;
}
.player-chip .chip-main { display: flex; align-items: center; gap: 8px; }
.player-chip .icon-btn { width: 38px; height: 38px; font-size: 19px; border-radius: 9px; }
.player-chip input[type="text"] { flex: 1; min-width: 0; padding: 6px 9px; font-size: 15px; }
.player-chip .remove-btn { width: 28px; height: 28px; font-size: 14px; }
.player-chip.expanded { grid-column: 1 / -1; }
.player-chip.expanded .emoji-grid {
  border: none;
  border-top: 1px solid var(--line);
  background: transparent;
  margin-top: 8px;
  padding: 10px 2px 2px;
  grid-template-columns: repeat(12, 1fr);
}

.roster-foot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 14px; }
.roster-foot .hint { margin: 0; }

@media (max-width: 860px) {
  .bento { grid-template-columns: minmax(0, 1fr); }
  .player-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .player-grid { grid-template-columns: minmax(0, 1fr); }
  .player-chip.expanded .emoji-grid { grid-template-columns: repeat(8, 1fr); }
}

.chips { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 18px; }
.chip {
  appearance: none;
  font: inherit;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  border: 1px solid var(--line);
  background: #fff;
  color: var(--ink);
  border-radius: 999px;
  padding: 8px 18px;
  cursor: pointer;
  transition: border-color 150ms var(--ease), background 150ms var(--ease), color 150ms var(--ease);
}
.chip:hover { border-color: #ccc; }
.chip.selected { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); font-weight: 500; }
.chips.tight { gap: 7px; margin: 0 0 0 8px; max-width: 260px; }
.chips.tight .chip { padding: 6px 13px; font-size: 14px; }

.custom-time { display: flex; align-items: center; gap: 12px; }
.custom-time label { font-weight: 500; }
.custom-time input { width: 90px; text-align: center; font-variant-numeric: tabular-nums; }
.custom-time .readout { margin: 0; white-space: nowrap; }

/* ---------- screen 2b: the send-off ---------- */

.sendoff-card { text-align: center; padding: 56px 48px; box-shadow: var(--shadow-float); border: 1px solid var(--line); }
.sendoff-card .screen-title { margin: 0; text-wrap: balance; }

/* ---------- roster row shell (the teacher's bet row on the stage) ---------- */

.roster-row {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  margin-bottom: 10px;
  overflow: hidden;
  animation: screen-in 300ms var(--ease) both;
}
.roster-row-main { display: flex; align-items: center; gap: 10px; padding: 10px 12px; }
.roster-row.invalid { border-color: #e00; background: rgba(238, 0, 0, 0.04); }
.roster-row.invalid input[type="number"] { border-color: #e00; }
.icon-btn {
  font: inherit;
  font-size: 22px;
  line-height: 1;
  width: 46px;
  height: 46px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  cursor: pointer;
  transition: border-color 150ms var(--ease), background 150ms var(--ease);
}
.icon-btn:hover { border-color: #ccc; background: #fff; }
.roster-row input[type="text"] { flex: 1; min-width: 0; }
.roster-row input[type="number"] { width: 74px; text-align: center; flex: none; }
.remove-btn {
  font: inherit;
  font-size: 16px;
  color: var(--faint);
  background: none;
  border: none;
  border-radius: 8px;
  width: 34px;
  height: 34px;
  flex: none;
  cursor: pointer;
  transition: color 150ms var(--ease), background 150ms var(--ease);
}
.remove-btn:hover { color: var(--ink); background: var(--surface); }

.emoji-grid {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 4px;
  padding: 10px 12px 12px;
  border-top: 1px solid var(--line);
  background: var(--surface);
}
.emoji-grid button {
  font: inherit;
  font-size: 21px;
  line-height: 1;
  padding: 7px 0;
  background: none;
  border: 1px solid transparent;
  border-radius: 8px;
  cursor: pointer;
  transition: background 120ms var(--ease), border-color 120ms var(--ease), transform 120ms var(--ease);
}
.emoji-grid button:hover { background: #fff; border-color: var(--line); transform: scale(1.12); }
.emoji-grid button.selected { border-color: var(--accent); background: var(--accent-soft); }

/* ---------- screen 3: the timed table ---------- */

.countdown-wrap { display: flex; justify-content: center; margin-bottom: 28px; }
.countdown {
  font-size: 76px;
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  /* the padding is carried at every stage so the tinted pill the final
     stage falls back to under reduced motion cannot shift the layout */
  padding: 4px 20px;
  border-radius: 999px;
  transition: color 300ms var(--ease), background-color 300ms var(--ease);
}
/* Two steps, not a gradient: from across a classroom a smooth ramp reads as
   one colour, whereas a discrete change is something you notice happening. */
.countdown.warn { color: var(--warn); }
.countdown.final {
  color: var(--urgent);
  animation: countdown-pulse 1s var(--ease) infinite;
}
@keyframes countdown-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.035); }
}

.table-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
@media (max-width: 880px) { .table-grid { grid-template-columns: 1fr; } }

.preview-block { margin: 20px 0; }

.train-preview {
  position: relative;
  height: 84px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: #fff;
  overflow: hidden;
  cursor: grab;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
}
.train-preview.dragging { cursor: grabbing; }
.train-preview.readonly { cursor: default; }
.train-preview::before,
.train-preview::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 90px;
  z-index: 3;
  pointer-events: none;
}
.train-preview::before { left: 0; background: linear-gradient(to right, #fff 12%, rgba(255, 255, 255, 0)); }
.train-preview::after { right: 0; background: linear-gradient(to left, #fff 12%, rgba(255, 255, 255, 0)); }

.center-tick {
  position: absolute;
  left: 50%;
  top: 0;
  width: 0;
  z-index: 4;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid var(--accent);
  transform: translateX(-50%);
  pointer-events: none;
}

.preview-card {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 52px;
  height: 52px;
  margin: -26px 0 0 -26px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 19px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  pointer-events: none;
}
.preview-card.lead { border-color: var(--accent); background: #fff; color: var(--accent); font-weight: 600; }
.preview-card.pop-in { animation: pop-in 340ms var(--ease) backwards; }
@keyframes pop-in {
  from { opacity: 0; scale: 0.7; }
  to { opacity: 1; scale: 1; }
}

.bets-rows { display: flex; flex-direction: column; gap: 8px; }
.bet-row {
  display: flex;
  align-items: center;
  gap: 10px;
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  transition: border-color 200ms var(--ease), background 200ms var(--ease), opacity 400ms var(--ease);
}
.bet-row .bet-icon { font-size: 19px; flex: none; }
.bet-row .bet-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.bet-row input { width: 74px; text-align: center; }
.bet-row.invalid { border-color: #e00; background: rgba(238, 0, 0, 0.04); }
.bet-row.invalid input { border-color: #e00; }
.sealed-dash { width: 74px; text-align: center; color: var(--faint); animation: screen-in 400ms var(--ease) both; }

.seal-wrap { display: flex; justify-content: center; }

/* the seal micro-animation: inputs soften into their locked state */
.sealing .panel { transition: opacity 500ms var(--ease); }
.sealing input:disabled { background: var(--surface); border-color: transparent; }
.sealing .bet-row { border-color: transparent; background: var(--surface); }

/* ---------- screen 4: call-back ---------- */

.callback-card { text-align: center; padding: 56px 48px; box-shadow: var(--shadow-float); border: 1px solid var(--line); }
.callback-card .screen-title { margin: 0; text-wrap: balance; }

/* ---------- stage: the locked facts, Joe's bet, the board ---------- */

.stage-top { display: block; margin-bottom: 30px; }

/* where the countdown stood on the table screen */
.sealed-stamp {
  display: block;
  text-align: center;
  margin-bottom: 22px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--faint);
}

/* Both panels take the row's full height, so the bet — which has no heading
   over it — centres against the taller one instead of hugging its top. */
.stage-grid { align-items: stretch; }
/* Once the bet is placed its panel goes, and the facts take the middle rather
   than sitting on in their old half. */
.stage-grid.solo { grid-template-columns: minmax(0, 560px); justify-content: center; }

.locked-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.locked-num {
  font-size: 40px;
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.locked-unit { color: var(--muted); }

.panel-bet { display: flex; flex-direction: column; justify-content: center; }
/* Confirm: the bet panel clears the row before the facts cross into it. The
   facts card's own travel is scripted in app.js — it depends on measured
   positions, so it cannot be written here. */
.panel-bet.leaving { animation: bet-out 160ms var(--ease) forwards; pointer-events: none; }
@keyframes bet-out {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: translateY(-6px); }
}
.teacher-pick-row .roster-row { margin-bottom: 0; }
.teacher-pick-row .bet-name {
  flex: 1;
  min-width: 0;
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.teacher-row { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.teacher-row.invalid { box-shadow: none; }
.teacher-row .roster-row-main { padding: 12px 14px; }
.teacher-row .icon-btn { width: 44px; height: 44px; }
/* wide enough for the 1–58 placeholder, which clipped at the old 74px */
.teacher-row input[type="number"] { width: 88px; font-size: 19px; padding: 10px 12px; }
.stage-confirm { margin-top: 16px; }

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

.board-wrap > .mini-title { text-align: center; }
/* The board is not on screen while the bet is being placed — it arrives with
   the bets, easing down out of the facts card as it fades up. */
.board-wrap.arriving { animation: board-in 460ms var(--ease) backwards; }
@keyframes board-in {
  from { opacity: 0; transform: translateY(-18px); }
  to { opacity: 1; transform: none; }
}

/* A grid rather than a wrapping row: ten boxes must not break 9 + 1. The
   column count and the card size are set from n in renderCardGrid — at the
   58-box ceiling that is five full rows and a short one, not a scroll. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(var(--cols, 10), var(--cardw, 78px));
  gap: 10px;
  justify-content: center;
  margin: 8px 0 28px;
}
/* Card on top, badges underneath: the names never share the card's inside,
   so a landing badge cannot push the number off centre. */
.grid-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  width: var(--cardw, 78px);
}
.grid-cell .badges { width: 100%; }
.grid-card {
  position: relative;
  width: var(--cardw, 78px);
  min-height: var(--cardw, 78px);
  padding: 8px 4px 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  color: var(--ink);
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font: inherit;
  transition: border-color 150ms var(--ease), background 150ms var(--ease),
    transform 150ms var(--ease), box-shadow 150ms var(--ease);
}
.grid-card .grid-num {
  font-size: calc(var(--cardw, 78px) * 0.33);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

/* narrow windows: let the row length fall out of the width instead */
@media (max-width: 880px) {
  .card-grid { grid-template-columns: repeat(auto-fit, var(--cardw, 78px)); }
}

/* ---------- badges ---------- */

.badges { display: flex; flex-wrap: wrap; gap: 3px; justify-content: center; align-content: flex-start; }
.badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  max-width: 100%;
  font-size: 11px;
  font-weight: 500;
  line-height: 1;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 3px 7px 3px 4px;
}
.badge .badge-icon { font-size: 13px; line-height: 1; }
.badge .badge-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 64px; }
.badge.teacher { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.badge.compact { padding: 3px 4px; }
.badge.compact .badge-name { display: none; }
.badge.gray { filter: grayscale(1); opacity: 0.6; }
.badge.fly { animation: badge-fly 460ms var(--ease) backwards; }
@keyframes badge-fly {
  from { opacity: 0; transform: translateY(-22px) scale(0.5); }
  to { opacity: 1; transform: none; }
}

/* ---------- the machine window ---------- */

.machine { margin-top: 8px; }
.window {
  --pitch: 180px;

  /* ---- the conveyor, locked in design/belt-lab.html -------------------
     The belt plane's centre line and half its projected depth: a 232-deep
     belt seen 13.5 degrees above it, so the whole band is 54px of screen.
     Every part of the machine is placed off these two numbers. */
  --belt-mid: 240px;
  --belt-half: 27.08px;
  --rail-lip: 6.81px;      /* how far the near rail stands proud of the belt */
  --rail-face: 20px;       /* and the height of its visible plate */
  --far-rail-h: 3px;
  --under-h: 3px;          /* the frame's shadowed underside, below the rail */

  /* the rubber: slat pitch, the lit crest and the groove it falls into, how
     far the ridges stand proud of the frame, and the sheen at the ends */
  --slat-pitch: 15.5px;
  --slat-hi: 0.22;
  --slat-lo: 0.45;
  --slat-rise: 1.5px;
  --sheen: 0.04;

  /* the near lip: how deep the belt's front face shows above the rail, and
     the light on it. See .belt-caps — this is the whole near-edge cue. */
  --lip-h: 7px;
  --lip-crest: 0.26;

  /* the hatch, and how the belt's edges bend around it */
  --door-w: 145px;
  --door-inset: 6px;       /* belt strip framing the trapdoor, near and far */
  --door-swing: 55deg;
  --bend-d: 3px;           /* edge inset across the hatch */
  --bend-w: 18px;          /* and the run it eases back to the rail over */
  /* Where that bend starts on each side. Every part that carries the
     corrugation measures its notch from these two lines, so the far teeth and
     the near lip leave their rails at the same x. */
  --notch-l: calc(50% - var(--door-w) / 2 - var(--bend-w));
  --notch-r: calc(50% + var(--door-w) / 2 + var(--bend-w));

  --bolt-pitch: 44px;
  --bolt-r: 3.8px;
  --bolt-y: 10px;          /* half the rail plate: the bolt line */
  --leg-pitch: 144px;
  --leg-w: 15px;

  --shadow-h: 39px;        /* the package's contact patch on the belt */
  --shadow-a: 0.85;

  --belt-base: #202226;
  --belt-far-c: #3a3e44;
  --belt-mid-c: #1e2024;
  --belt-near-c: #2a2d32;
  --belt-lip-c: #101214;
  /* the three tones of the lit lip, top to bottom */
  --lip-hi: #3f454c;
  --lip-mid: #34393f;
  --lip-lo: #1a1d21;
  --steel-hi: #f2f4f6;
  --steel-mid: #d3d8dd;
  --steel-lo: #b0b6bd;
  --steel-edge: #8b939c;
  --steel-shade: #6f767f;
  --bolt-ring: #7d848d;

  position: relative;
  height: 364px;
  max-width: 680px;
  margin: 0 auto;
  overflow: hidden;
  border-radius: var(--radius);
  background: #fff;
}
/* The belt has no ends: it runs out of the window into white on both sides,
   which is also what lets packages appear and leave without a seam. */
.edge-fade {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 114px;
  z-index: 6;
  pointer-events: none;
}
.edge-fade.left { left: 0; background: linear-gradient(to right, #fff 3%, rgba(255, 255, 255, 0)); }
.edge-fade.right { right: 0; background: linear-gradient(to left, #fff 3%, rgba(255, 255, 255, 0)); }

/* Back to front: the void under the belt, the slatted surface, the trapdoor, the
   far rail — then the three pieces drawn OVER the packages. The NEAR rail, so a
   box's bottom sits behind it exactly as it would on a real line; and, below it,
   the frame's shadowed underside and the legs, so a package through the hatch
   goes down BEHIND the machine it fell into rather than across the front of it.
   Nothing at rest ever reaches those two — the belt line is a whole rail plate
   above them — so they only ever bite on a fall. The DOM order in index.html is
   the paint order, and it matters: see .belt-caps. */

.belt-legs {
  position: absolute;
  /* anchored to the centre and tiled, so legs stay symmetric about the
     trapdoor at any window width — and so the pair either side of the hatch
     straddles the fall line rather than standing in it */
  left: 50%;
  width: 1440px;
  margin-left: -720px;
  top: calc(var(--belt-mid) + var(--belt-half) - var(--rail-lip) + var(--rail-face) + var(--under-h));
  bottom: 0;
  z-index: 4;
  background-image: linear-gradient(90deg,
    rgba(255, 255, 255, 0) 0 calc(var(--leg-pitch) / 2 - var(--leg-w) / 2),
    var(--steel-hi) calc(var(--leg-pitch) / 2 - var(--leg-w) / 2) calc(var(--leg-pitch) / 2 - var(--leg-w) / 2 + 2px),
    var(--steel-mid) calc(var(--leg-pitch) / 2 - var(--leg-w) / 2 + 2px) calc(var(--leg-pitch) / 2 + var(--leg-w) / 6),
    var(--steel-lo) calc(var(--leg-pitch) / 2 + var(--leg-w) / 6) calc(var(--leg-pitch) / 2 + var(--leg-w) / 2 - 2px),
    var(--steel-edge) calc(var(--leg-pitch) / 2 + var(--leg-w) / 2 - 2px) calc(var(--leg-pitch) / 2 + var(--leg-w) / 2),
    rgba(255, 255, 255, 0) calc(var(--leg-pitch) / 2 + var(--leg-w) / 2));
  background-size: var(--leg-pitch) 100%;
}

/* Under the trapdoor: not the window's white, but somewhere to fall. The hole
   is cut INSIDE the belt band — a strip of travelling rubber frames it on the
   far and near sides — and the outer ring shadow is the rebate the plate sits
   in, which shows whether the door is shut or not. */
.belt-void {
  position: absolute;
  left: 50%;
  margin-left: calc(var(--door-w) / -2);
  width: var(--door-w);
  top: calc(var(--belt-mid) - var(--belt-half) + var(--door-inset));
  height: calc((var(--belt-half) - var(--door-inset)) * 2);
  z-index: 1;
  background: linear-gradient(180deg, #060708 0%, #0e1013 62%, #181b1f 100%);
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.45), inset 0 7px 12px rgba(0, 0, 0, 0.6);
}

/* Between the rail's bottom edge and the legs: the belt's return run and the
   frame's underside, in shadow — what gives the machine a thickness instead of
   a steel plate floating on white. Over the packages, like the rail above it: a
   falling box passes behind this band, which is what makes the hatch a hole
   through something rather than a hole in a surface. */
.belt-under {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--belt-mid) + var(--belt-half) - var(--rail-lip) + var(--rail-face));
  height: var(--under-h);
  z-index: 4;
  background: linear-gradient(180deg, #0a0b0d 0 2px, #1b1d21 2px 78%, #2e3136 100%);
}

/* One continuous run: the belt passes the trapdoor on both sides, so the
   surface never breaks — the door, and the hole under it, is a plate let into
   the middle of the band, framed by travelling rubber. Only the slats travel:
   --slat is set on .window by app.js, so the teeth and caps ride with them. */
.belt-track {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--belt-mid) - var(--belt-half));
  height: calc(var(--belt-half) * 2);
  z-index: 1;
  background-color: var(--belt-base);
  background-image:
    /* each pitch is a ridge with a lit leading edge, a flat rubber top and a
       groove it falls into: the groove's two walls take half the shadow each,
       which is what makes it read as a channel and not a line */
    repeating-linear-gradient(90deg,
      rgba(255, 255, 255, var(--slat-hi)) 0 1.5px,
      rgba(255, 255, 255, calc(var(--slat-hi) / 3)) 1.5px 3px,
      rgba(0, 0, 0, 0) 3px calc(var(--slat-pitch) - 6px),
      rgba(0, 0, 0, calc(var(--slat-lo) / 2)) calc(var(--slat-pitch) - 6px) calc(var(--slat-pitch) - 4.5px),
      rgba(0, 0, 0, var(--slat-lo)) calc(var(--slat-pitch) - 4.5px) calc(var(--slat-pitch) - 1.5px),
      rgba(0, 0, 0, calc(var(--slat-lo) / 2)) calc(var(--slat-pitch) - 1.5px) var(--slat-pitch)),
    /* the sheen: rubber catches the room's light where the run leaves the
       frame and goes matte toward the middle */
    linear-gradient(90deg, rgba(255, 255, 255, var(--sheen)), rgba(255, 255, 255, 0) 38%),
    linear-gradient(270deg, rgba(255, 255, 255, var(--sheen)), rgba(255, 255, 255, 0) 38%),
    /* across the belt's depth: the far edge in shadow, the near edge lit */
    linear-gradient(180deg, var(--belt-far-c) 0%, var(--belt-mid-c) 40%, var(--belt-near-c) 72%, var(--belt-lip-c) 100%);
  /* only the slats travel; the sheen and the depth shading stay put */
  background-position: var(--slat, 0px) 0, 0 0, 0 0, 0 0;
  background-repeat: repeat, no-repeat, no-repeat, no-repeat;
}

/* The ridge ends seen end-on: the belt is in FRONT of the far rail, so every
   ridge runs unbroken from the belt surface up past the rail and pokes
   --slat-rise above it — the rail's silver shows only through the groove gaps.
   Drawn bottom-flush with the belt's far edge in the belt's own colour, so the
   teeth grow out of the rubber instead of floating over the rail. Around the
   trapdoor the belt sinks behind the rail, and the teeth go WITH it: they
   curve down rather than fade out. See --hatch-notch. */
.belt-teeth {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--belt-mid) - var(--belt-half) - var(--far-rail-h) - var(--slat-rise));
  height: calc(var(--far-rail-h) + var(--slat-rise));
  z-index: 1;
  background-image: repeating-linear-gradient(90deg,
    var(--belt-far-c) 0 calc(var(--slat-pitch) - 6px),
    rgba(0, 0, 0, 0) calc(var(--slat-pitch) - 6px) var(--slat-pitch));
  background-position-x: var(--slat, 0px);
  /* Four mask layers, added together (union): two hard cuts that leave the run
     untouched out to --notch-l / --notch-r, and a quarter-ellipse at each notch
     — --bend-w wide, the strip's full height deep — whose arc IS the bend.
     Tangent to the rail where it starts, steepest at the hatch, where the teeth
     have gone the whole depth of the strip and the rail's silver runs clear.
     Ellipse, not a fade: the ridge ends stay opaque and hard-edged all the way
     down, so they read as corrugation dropping away instead of ink thinning
     out. The 98% stop is a hair of feathering to keep the arc from
     stair-stepping. */
  --hatch-notch:
    linear-gradient(90deg, #000 0 var(--notch-l), rgba(0, 0, 0, 0) var(--notch-l)),
    linear-gradient(90deg, rgba(0, 0, 0, 0) 0 var(--notch-r), #000 var(--notch-r)),
    radial-gradient(ellipse var(--bend-w) 100% at var(--notch-l) 100%,
      #000 0 98%, rgba(0, 0, 0, 0) 100%),
    radial-gradient(ellipse var(--bend-w) 100% at var(--notch-r) 100%,
      #000 0 98%, rgba(0, 0, 0, 0) 100%);
  -webkit-mask-image: var(--hatch-notch);
  mask-image: var(--hatch-notch);
}

/* The near edge's answer to the far teeth — and it works the opposite way
   round, because the near rail is drawn IN FRONT of the belt. Nothing the
   rubber does can occlude that plate, so there is no silhouette to be had and
   no shadow to cast onto it that isn't a smear on the cleanest surface in the
   scene. The only cue left is the light on the rubber itself.

   So: the lip is not black. It is the face of the belt that turns toward the
   room, so it catches the light — and the grooves that already run the whole
   depth of .belt-track come over the edge with it and cut that lit band into
   ridges for free. Nothing is drawn per-ridge here except the groove; the
   corrugation is the belt's own, seen end-on.

   Locked from design/near-ridge-studies-2.html, study G. Paint order: this
   goes down BEFORE the rail, so only --lip-h of it clears the plate's top. */
.belt-caps {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--belt-mid) + var(--belt-half) - var(--rail-lip) - var(--lip-h));
  height: calc(var(--rail-lip) + var(--lip-h));
  z-index: 3;
  pointer-events: none;
  /* The far teeth's notch, scaled to what actually shows here: only --lip-h
     clears the rail, so the arcs are that deep and anchored on the rail's top
     line. The lit band sinks along the bend until, at the hatch, it has ducked
     its whole depth behind the plate. Below the rail line it is covered by the
     plate anyway, so the arcs need reach no further. */
  --hatch-notch:
    linear-gradient(90deg, #000 0 var(--notch-l), rgba(0, 0, 0, 0) var(--notch-l)),
    linear-gradient(90deg, rgba(0, 0, 0, 0) 0 var(--notch-r), #000 var(--notch-r)),
    radial-gradient(ellipse var(--bend-w) var(--lip-h)
      at var(--notch-l) var(--lip-h), #000 0 98%, rgba(0, 0, 0, 0) 100%),
    radial-gradient(ellipse var(--bend-w) var(--lip-h)
      at var(--notch-r) var(--lip-h), #000 0 98%, rgba(0, 0, 0, 0) 100%);
  -webkit-mask-image: var(--hatch-notch);
  mask-image: var(--hatch-notch);
  background-image:
    /* the grooves, over the edge and down to the steel. Topmost, so they also
       cut the crest below into dashes rather than needing their own copy of
       the rhythm. */
    repeating-linear-gradient(90deg,
      rgba(0, 0, 0, 0) 0 calc(var(--slat-pitch) - 6px),
      var(--belt-lip-c) calc(var(--slat-pitch) - 6px) var(--slat-pitch)),
    /* the crest: the one row of pixels where the belt surface breaks over */
    linear-gradient(rgba(255, 255, 255, var(--lip-crest)), rgba(255, 255, 255, var(--lip-crest))),
    /* the lip itself — the belt's near colour rolling over into the lit
       chamfer, then its underside in shadow where it meets the plate */
    linear-gradient(180deg,
      var(--belt-near-c) 0 1px,
      var(--lip-hi) 1px calc(var(--lip-h) * 0.43),
      var(--lip-mid) calc(var(--lip-h) * 0.43) calc(var(--lip-h) * 0.71),
      var(--lip-lo) calc(var(--lip-h) * 0.71) calc(var(--lip-h) * 0.91),
      var(--belt-lip-c) calc(var(--lip-h) * 0.91));
  background-size: 100% 100%, 100% 1px, 100% 100%;
  background-repeat: repeat, repeat-x, no-repeat;
  background-position-x: var(--slat, 0px);
}

/* Around the trapdoor the belt bends: its edges pull away from both rails and
   wrap down around the hatch frame. Each notch is the shadowed gap that opens
   up — deepest across the door's span, easing back out to the rail over
   --bend-w with the rounded ends, and lit along its crest where the bent
   rubber still catches the room. */
.belt-bend {
  position: absolute;
  left: 50%;
  width: calc(var(--door-w) + var(--bend-w) * 2);
  margin-left: calc((var(--door-w) + var(--bend-w) * 2) / -2);
  height: var(--bend-d);
  z-index: 1;
}
.belt-bend.far {
  top: calc(var(--belt-mid) - var(--belt-half));
  border-radius: 0 0 var(--bend-w) var(--bend-w) / 0 0 var(--bend-d) var(--bend-d);
  background: linear-gradient(180deg, #08090b, #141619);
  box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.1);
}
.belt-bend.near {
  top: calc(var(--belt-mid) + var(--belt-half) - var(--bend-d));
  border-radius: var(--bend-w) var(--bend-w) 0 0 / var(--bend-d) var(--bend-d) 0 0;
  background: linear-gradient(180deg, #16181b, #060708);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}

/* The far rail stands up beyond the belt, so on screen it lands just above the
   belt's far edge rather than over any of it. */
.belt-far-rail {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--belt-mid) - var(--belt-half) - var(--far-rail-h));
  height: var(--far-rail-h);
  z-index: 1;
  background: linear-gradient(180deg,
    #fff 0 1.5px, var(--steel-mid) 1.5px 40%, var(--steel-lo) 40% 75%, var(--steel-edge) 100%);
}

/* The near rail: the only part of the machine drawn over the packages. */
.belt-rail {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--belt-mid) + var(--belt-half) - var(--rail-lip));
  height: var(--rail-face);
  z-index: 3;
  background-image:
    /* the bolt: a specular pip up and left of centre, over a domed body. One
       concentric bullseye reads as a flat eyelet — the offset pip is what makes
       it a head catching the same light as everything else. */
    radial-gradient(circle calc(var(--bolt-r) * 0.34)
        at calc(var(--bolt-pitch) / 2 - var(--bolt-r) * 0.26) calc(var(--bolt-y) - var(--bolt-r) * 0.26),
      rgba(255, 255, 255, 0.95) 0 55%, rgba(255, 255, 255, 0) 100%),
    /* darker than the plate it sits in, and shading outward, so the pip has
       something to be a highlight ON */
    radial-gradient(circle var(--bolt-r) at calc(var(--bolt-pitch) / 2) var(--bolt-y),
      var(--steel-lo) 0 50%,
      var(--bolt-ring) 50% 78%,
      var(--steel-shade) 78% 92%,
      rgba(0, 0, 0, 0.34) 92% 100%,
      rgba(0, 0, 0, 0) 100%),
    linear-gradient(180deg,
      #fff 0 2px,
      var(--steel-hi) 2px 8px,
      var(--steel-mid) 8px 54%,
      var(--steel-lo) 54% 82%,
      var(--steel-edge) 82% 94%,
      var(--steel-shade) 94% calc(100% - 1.5px),
      rgba(0, 0, 0, 0.34) calc(100% - 1.5px) 100%);
  background-size: var(--bolt-pitch) 100%, var(--bolt-pitch) 100%, 100% 100%;
  background-repeat: repeat-x, repeat-x, no-repeat;
}

/* A steel hatch let into the middle of the belt, deliberately not slatted: it
   belongs to the frame, it does not travel, and it has to read as a door.
   Gunmetal, a shade lighter than the rubber, sitting in the rebate the void's
   ring shadow draws around it. It hinges on its near edge — that's where the
   knuckles are — and falls away, so the hole opens from the far side first, the
   side this camera can actually see into. */
.door {
  position: absolute;
  left: 50%;
  margin-left: calc(var(--door-w) / -2);
  width: var(--door-w);
  top: calc(var(--belt-mid) - var(--belt-half) + var(--door-inset));
  height: calc((var(--belt-half) - var(--door-inset)) * 2);
  z-index: 1;
  background-image:
    /* four dome rivets holding the plate down, lit like the rail bolts */
    radial-gradient(circle 2.6px at 9px 8px,
      rgba(255, 255, 255, 0.55) 0 26%, #7d848c 26% 55%, #23262a 55% 90%, rgba(0, 0, 0, 0) 100%),
    radial-gradient(circle 2.6px at calc(100% - 9px) 8px,
      rgba(255, 255, 255, 0.55) 0 26%, #7d848c 26% 55%, #23262a 55% 90%, rgba(0, 0, 0, 0) 100%),
    radial-gradient(circle 2.6px at 9px calc(100% - 8px),
      rgba(255, 255, 255, 0.55) 0 26%, #7d848c 26% 55%, #23262a 55% 90%, rgba(0, 0, 0, 0) 100%),
    radial-gradient(circle 2.6px at calc(100% - 9px) calc(100% - 8px),
      rgba(255, 255, 255, 0.55) 0 26%, #7d848c 26% 55%, #23262a 55% 90%, rgba(0, 0, 0, 0) 100%),
    /* a whisper of brushed tread */
    repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.04) 0 1px, rgba(0, 0, 0, 0) 1px 7px),
    /* the plate: bevelled top, falling into shadow at the hinge edge —
       gunmetal pitched between the bright rails and the rubber, so it can never
       be mistaken for either the belt or the hole */
    linear-gradient(180deg, #a6adb4 0 1.5px, #6d737b 1.5px 40%, #585e66 40% 82%, #41464d 100%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), inset 0 0 0 1px rgba(0, 0, 0, 0.3);
  transform-origin: 50% 100%;
  transition: transform 260ms var(--ease), opacity 260ms var(--ease), filter 260ms var(--ease);
}
/* the hinge: a knuckle strip along the near edge the plate swings on */
.door::before {
  content: "";
  position: absolute;
  left: 5px;
  right: 5px;
  bottom: 0;
  height: 4px;
  background-image: repeating-linear-gradient(90deg,
    rgba(0, 0, 0, 0.55) 0 1px, #838a92 1px 3px, #4a5057 3px 9px,
    rgba(0, 0, 0, 0.55) 9px 10px, rgba(0, 0, 0, 0) 10px 22px);
}
/* the lifting slot, punched near the far edge so a hand could get in */
.door::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 22%;
  width: 24px;
  height: 5px;
  margin-left: -12px;
  border-radius: 3px;
  background: linear-gradient(180deg, #08090b, #1d2024);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.12);
}
.door.open { transform: perspective(560px) rotateX(var(--door-swing)); filter: brightness(0.42) saturate(0.6); }

.sensor {
  position: absolute;
  left: 50%;
  top: 29px;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  z-index: 5;
}
.sensor-stem {
  width: 2px;
  height: 26px;
  background: linear-gradient(180deg, #dfe3e8, #b7bdc5);
}

/* The sensor lamp reads as a FAULTY instrument, and the fault is the mechanism:
   it only half-notices most boxes, and every so often locks fully on — and a
   proper lock-on is what fires the trapdoor.

     .armed  dim red, flickering. A box is at the station, only partly sensed,
             and it survives precisely because the sensor is faulty. Entered
             directly: no ramp, no flash. It just becomes dim. HELD.
     .rise   the sensor closing in — plays only on the box it locks onto.
     .trip   full lock-on. This is what opens the door. Holds at full red while
             the box falls, so .out can pick up from it.
     .out    the box is gone and the station is empty, so there is nothing left
             to sense. Lands on the unpowered look and HOLDS.

   The bare element (no state class) is the same unpowered look, used before the
   belt starts and after the run ends.

   Rule order matters: rise, trip and out override the armed flicker by replacing
   the whole `animation` shorthand, so they must come after it. */
.sensor-light {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #e4e7ea;
  border: 1px solid #ccd1d6;
  transition: background 140ms var(--ease), box-shadow 140ms var(--ease), transform 140ms var(--ease);
}

/* Armed: dim red, flickering. Red at low intensity rather than a colourless
   lens — the page is already #fff, so a white lamp has nothing to read against
   and registers as nothing at all. */
.sensor-light.armed {
  background: rgba(238, 0, 0, 0.34);
  border-color: rgba(238, 0, 0, 0.5);
  box-shadow: 0 0 0 2px rgba(238, 0, 0, 0.07), 0 0 6px rgba(238, 0, 0, 0.3);
  animation: sensor-flicker 1.9s linear infinite;
}

/* The sensor closing in on the box that it will lock onto. Ease-in, so most of
   the brightening happens late — it appears to notice the box rather than
   anticipate it. Only the dropped box gets this. */
.sensor-light.rise {
  animation: sensor-rise var(--rise, 420ms) ease-in forwards;
}

/* Full lock-on — the flash that fires the trapdoor. Plays only on the box that
   drops, and holds at full red until the box is through the door. */
.sensor-light.trip {
  animation: sensor-trip var(--trip, 300ms) var(--ease) forwards;
}

/* No detection — this is the box the door takes. The glow that had been
   building goes out, and the lamp STAYS out: `forwards` holds the final frame
   and nothing re-arms it until the next step. An unpowered lamp is a resting
   state the box sits in, not a blink. */
.sensor-light.out {
  animation: sensor-out var(--out, 420ms) var(--ease) forwards;
}

/* The flicker varies the lamp's OUTPUT, never its opacity. Opacity was the bug
   behind "dark and dim most of the time": it fades the whole lamp toward
   whatever is behind it, and since the cycle only touches full strength at three
   instants, the lamp spent ~90% of its life dimmed. Here the bloom pulls in and
   the lens goes *lighter* as it weakens, so the lamp is always plainly lit.
   Shared with the illustrated lamps on the instruction slides. */
@keyframes sensor-flicker {
  0%, 100% {
    background-color: rgba(238, 0, 0, 0.34);
    box-shadow: 0 0 0 2px rgba(238, 0, 0, 0.07), 0 0 6px rgba(238, 0, 0, 0.3);
  }
  7% {
    background-color: rgba(238, 0, 0, 0.2);
    box-shadow: 0 0 0 1px rgba(238, 0, 0, 0.04), 0 0 3px rgba(238, 0, 0, 0.16);
  }
  10% {
    background-color: rgba(238, 0, 0, 0.33);
    box-shadow: 0 0 0 2px rgba(238, 0, 0, 0.07), 0 0 6px rgba(238, 0, 0, 0.29);
  }
  36% {
    background-color: rgba(238, 0, 0, 0.26);
    box-shadow: 0 0 0 2px rgba(238, 0, 0, 0.05), 0 0 4px rgba(238, 0, 0, 0.22);
  }
  39% {
    background-color: rgba(238, 0, 0, 0.36);
    box-shadow: 0 0 0 2px rgba(238, 0, 0, 0.08), 0 0 7px rgba(238, 0, 0, 0.32);
  }
  64% {
    background-color: rgba(238, 0, 0, 0.18);
    box-shadow: 0 0 0 1px rgba(238, 0, 0, 0.03), 0 0 3px rgba(238, 0, 0, 0.14);
  }
  68% {
    background-color: rgba(238, 0, 0, 0.32);
    box-shadow: 0 0 0 2px rgba(238, 0, 0, 0.07), 0 0 6px rgba(238, 0, 0, 0.28);
  }
  87% {
    background-color: rgba(238, 0, 0, 0.24);
    box-shadow: 0 0 0 2px rgba(238, 0, 0, 0.05), 0 0 4px rgba(238, 0, 0, 0.2);
  }
}

@keyframes sensor-rise {
  from {
    background-color: rgba(238, 0, 0, 0.34);
    border-color: rgba(238, 0, 0, 0.5);
    box-shadow: 0 0 0 2px rgba(238, 0, 0, 0.07), 0 0 6px rgba(238, 0, 0, 0.3);
    transform: scale(1);
  }
  to {
    background-color: var(--sensor);
    border-color: var(--sensor);
    box-shadow: 0 0 0 4px var(--sensor-soft), 0 0 15px rgba(238, 0, 0, 0.55);
    transform: scale(1.1);
  }
}

@keyframes sensor-trip {
  0% {
    background-color: var(--sensor);
    border-color: var(--sensor);
    box-shadow: 0 0 0 4px var(--sensor-soft), 0 0 15px rgba(238, 0, 0, 0.55);
    transform: scale(1.1);
  }
  /* A hot red core, not a white one — a white flash reads as a pale donut at
     12px and loses exactly the redness the buildup was for. */
  22% {
    background-color: #ff2d20;
    border-color: #ff6b60;
    box-shadow: 0 0 0 10px rgba(238, 0, 0, 0.32), 0 0 32px rgba(255, 45, 32, 0.95);
    transform: scale(1.45);
  }
  /* Holds locked-on at full red rather than settling back: the door is opening
     underneath, and sensor-out picks up from exactly these values once the box
     is gone. */
  100% {
    background-color: var(--sensor);
    border-color: var(--sensor);
    box-shadow: 0 0 0 4px var(--sensor-soft), 0 0 15px rgba(238, 0, 0, 0.55);
    transform: scale(1.1);
  }
}

/* Ends on exactly the unpowered values from `.sensor-light`, so the held final
   frame is indistinguishable from a dead lamp — which is the point. */
@keyframes sensor-out {
  0% {
    background-color: var(--sensor);
    border-color: var(--sensor);
    box-shadow: 0 0 0 4px var(--sensor-soft), 0 0 15px rgba(238, 0, 0, 0.55);
    transform: scale(1.1);
  }
  /* the bloom collapses first: the lamp visibly gives up on this box */
  55% {
    background-color: rgba(238, 0, 0, 0.06);
    border-color: rgba(238, 0, 0, 0.16);
    box-shadow: 0 0 0 0 rgba(238, 0, 0, 0), 0 0 0 rgba(238, 0, 0, 0);
    transform: scale(0.88);
  }
  100% {
    background-color: #e4e7ea;
    border-color: #ccd1d6;
    box-shadow: 0 0 0 0 rgba(238, 0, 0, 0), 0 0 0 rgba(238, 0, 0, 0);
    transform: scale(1);
  }
}

/* Reduced motion: no flicker and no ramp — the lamp still carries the verdict,
   it just states it instead of performing it. */
@media (prefers-reduced-motion: reduce) {
  .sensor-light.armed,
  .sensor-light.rise { animation: none; }
  /* still has to hold the dark resting state, just without the ramp */
  .sensor-light.out {
    animation: none;
    background: #e4e7ea;
    border-color: #ccd1d6;
    box-shadow: none;
  }
  .sensor-light.armed {
    background: rgba(238, 0, 0, 0.34);
    border-color: rgba(238, 0, 0, 0.5);
    box-shadow: 0 0 0 2px rgba(238, 0, 0, 0.07), 0 0 6px rgba(238, 0, 0, 0.3);
  }
  .sensor-light.trip {
    animation: none;
    background: var(--sensor);
    border-color: var(--sensor);
    box-shadow: 0 0 0 5px var(--sensor-soft), 0 0 14px rgba(238, 0, 0, 0.5);
  }
}

/* Packages ride between the belt surface and the near rail, so a box's bottom
   sits behind the rail exactly as it would on a real line.

   The mask is for the tail of the drop, and ONLY the tail. A box through the
   hatch has to be watchable the whole way down — it passes behind the rail, then
   out the far side of it into the shadow under the frame, tumbling between the
   legs — because the fall IS the elimination, and a package that vanished at the
   belt line read as deleted rather than dropped. So the fade starts well below
   the underside band, deep in the leg space, and takes it out just before the
   window's bottom edge would cut it off square. It cannot touch a package on the
   belt, or one still crossing the rail: the sprite's bottom edge is the belt
   line, the whole frame — lip, rail plate and underside — stands between that
   and where this begins. See paintFall() in app.js for the fall itself. */
.cars {
  position: absolute;
  inset: 0;
  z-index: 2;
  transition: opacity 160ms linear;
  --fall-line: calc(var(--belt-mid) + var(--belt-half) - var(--rail-lip)
    + var(--rail-face) + var(--under-h) + 30px);
  -webkit-mask-image: linear-gradient(180deg,
    #000 0 var(--fall-line), rgba(0, 0, 0, 0) calc(var(--fall-line) + 34px));
  mask-image: linear-gradient(180deg,
    #000 0 var(--fall-line), rgba(0, 0, 0, 0) calc(var(--fall-line) + 34px));
}

/* A package is a positioned anchor with no size of its own: the sprite, its
   contact shadow and its badge stack are placed against the belt line, so the
   box can rock and lift without dragging the shadow or the names with it.

   app.js sets this box's own --box-sprite, --box-w/h, --contact and --face-*
   here, so a package turned further than its neighbour is a taller sprite
   resting deeper below the belt's centre line, with the number lying in its own
   plane — and everything below reads those off the car rather than off :root.
   The belt line is derived rather than given: it is wherever this box's lowest
   corner lands. */
.car {
  position: absolute;
  left: 50%;
  top: 0;
  width: 0;
  height: 0;
  will-change: transform;
  --belt-y: calc(var(--belt-mid) + var(--contact) - 2px);
}
/* Nearly straight on, the contact patch is a shallow ellipse hugging the belt's
   centre line; the box hides all but its near crescent. */
.car-shadow {
  position: absolute;
  left: 50%;
  top: calc(var(--belt-mid) - var(--shadow-h) / 2);
  width: calc(var(--box-w) * 0.94);
  height: var(--shadow-h);
  margin-left: calc(var(--box-w) * -0.47);
  border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(0, 0, 0, var(--shadow-a)), rgba(0, 0, 0, 0) 66%);
  will-change: transform, opacity;
}

.car-box {
  position: absolute;
  left: 50%;
  top: calc(var(--belt-y) + 2px - var(--box-h));
  width: var(--box-w);
  height: var(--box-h);
  margin-left: calc(var(--box-w) / -2);
  background: var(--box-sprite) no-repeat center / 100% 100%;
  will-change: transform;
}
/* Stencilled on the front face — which from this angle is a parallelogram, so
   the number has to lie IN that plane rather than sit flat on the sprite. The
   element is sized in the face's own units and pinned to the face's top-left
   corner; --face-matrix is the projection that maps those units to screen px, so
   the number is painted on the cardboard. A child of the sprite, so it rides the
   rock exactly. */
.car .car-num {
  position: absolute;
  left: var(--face-left);
  top: var(--face-top);
  width: var(--face-w);
  height: var(--face-h);
  transform-origin: 0 0;
  /* the shift composes BEFORE the projection, so --num-x/--num-y move the
     number in the face's own units — along the cardboard, not the screen, which
     is what keeps it in the same printed spot however the box is turned */
  transform: var(--face-matrix) translate(var(--num-x), var(--num-y));
  display: flex;
  align-items: center;
  /* Set from the left, so --num-x is where the print STARTS and a second digit is
     added to the right of the first: box 9 and box 10 share a left edge, and the
     number grows one way rather than spreading from a centre. Centred, the count
     reaching two digits moved both digits outward at once, which walked the print
     off the right-hand seam — and the dial reaches 58, so most of a run is two
     digits wide. */
  justify-content: flex-start;
  padding-bottom: var(--num-lift);
  line-height: 1;
  font-size: var(--num-size);
  font-weight: 700;
  letter-spacing: -0.02em;
  /* tabular, so every two-digit number is exactly as wide as every other */
  font-variant-numeric: tabular-nums;
  color: #fff3d6;
  text-shadow: 0 1px 2px rgba(92, 46, 0, 0.5);
}

/* Bets sit above the box and never rotate — they are labels, not cargo. */
.car .badges {
  position: absolute;
  left: 50%;
  top: calc(var(--belt-y) + 2px - var(--box-h) - 48px);
  width: 176px;
  height: 44px;
  margin-left: -88px;
  align-content: flex-end;
  overflow: hidden;
  will-change: transform;
}
.car.winner .car-box {
  filter: drop-shadow(0 10px 26px rgba(0, 112, 243, 0.45));
}

/* The machine clears out for the win: the conveyor, the trapdoor and the sensor
   are all done working, and the callers move off the box and into the panel
   below it. Only the surviving package is left on stage — every part of the
   machine goes, or the box would be left sitting on half a conveyor. */
.window.celebrating .belt-legs,
.window.celebrating .belt-under,
.window.celebrating .belt-track,
.window.celebrating .belt-bend,
.window.celebrating .belt-void,
.window.celebrating .belt-far-rail,
.window.celebrating .belt-teeth,
.window.celebrating .belt-caps,
.window.celebrating .belt-rail,
.window.celebrating .door,
.window.celebrating .sensor {
  opacity: 0;
  pointer-events: none;
  transition: opacity 420ms var(--ease);
}
@media (prefers-reduced-motion: reduce) {
  .window.celebrating .belt-legs,
  .window.celebrating .belt-under,
  .window.celebrating .belt-track,
  .window.celebrating .belt-bend,
  .window.celebrating .belt-void,
  .window.celebrating .belt-far-rail,
  .window.celebrating .belt-teeth,
  .window.celebrating .belt-caps,
  .window.celebrating .belt-rail,
  .window.celebrating .door,
  .window.celebrating .sensor {
    transition: none;
  }
}

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

/* ---------- run controls, status, fallen strip ---------- */

.run-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  flex-wrap: wrap;
}

/* Run and the rate are one object — press play, the belt rolls at this rate —
   so they read as a video player's transport rather than as two more buttons
   competing with Step. No overflow:hidden on the group: it would clip the
   focus ring off the segment inside it. */
.transport {
  display: inline-flex;
  align-items: stretch;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 9px;
}
.transport-btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  color: var(--ink);
  background: #fff;
  border: 0;
  padding: 11px 16px;
  cursor: pointer;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.transport-btn:first-child { border-radius: 8px 0 0 8px; }
.transport-btn:last-child { border-radius: 0 8px 8px 0; }
.transport-btn + .transport-btn { border-left: 1px solid var(--line); }
.transport-btn:hover { background: var(--surface); }
.transport-btn .glyph { font-size: 12px; line-height: 1; }
/* playing is a state, not a relabelling — visible from the back of a room */
.transport-btn.run[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); }
/* the rate cycles on tap: a 130px slider was a precision drag to make while
   standing in front of a class, and could land somewhere unreadable */
.transport-btn.rate { min-width: 74px; color: var(--muted); }
.speed-readout { font-variant-numeric: tabular-nums; color: var(--ink); }

.fallen {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
  align-items: flex-start;
  min-height: 56px;
  max-width: 820px;
  margin: 10px auto 0;
  padding: 10px 12px;
  border-top: 1px solid var(--line);
}
/* Chips exist from the first step of the run — the whole pile is laid out
   hidden so the strip holds its final height and the machine stays put (see
   buildFallenPile in app.js). A drop reveals the chip it already owns. */
.fallen-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  width: 52px;
  padding: 6px 3px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  color: var(--faint);
  visibility: hidden;
}
.fallen-card.landed {
  visibility: visible;
  animation: fallen-in 380ms var(--ease) both;
}
.fallen-card .fallen-num { font-size: 18px; font-weight: 500; font-variant-numeric: tabular-nums; }
@keyframes fallen-in {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: none; }
}

/* ---------- winner ---------- */

/* fade, not rise: this panel wraps the fixed winner actions, and a transform
   here would drag the bar up with it (see screen-fade). */
.winner-panel { text-align: center; margin-top: 26px; animation: screen-fade 500ms var(--ease) backwards; }
.winner-actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
/* Rerun only exists here, at the end, for a class that wants to watch the
   whole line again. Its note takes the full row so it sits under all three. */
.rerun { display: inline-flex; align-items: center; gap: 8px; }
.rerun .glyph { font-size: 16px; line-height: 1; }
.rerun-note { width: 100%; }

/* The names that called the winning box do not move at the win: they stay in
   the holder they have ridden in since the reveal, and grow instead. Moving
   them into a list under the machine would break the one continuity the round
   has been building — THAT box, with THOSE names on it, is the one still up.
   See design/win-screen-studies.html, study E.

   Three things have to give, and all three are here:
     the holder was a 176 x 44 clip box sized for a badge or two on a moving
     belt, so it widens and stops clipping;
     the badges were built .compact once three or more people were on a box,
     so the crowding rule is relaxed to 6 now that they are the payoff;
     the sprite grows 1.4x from its base at the win while the holder is placed
     off an UNSCALED box — at 48px it would clear the new lid by 8px, so the
     offset is re-derived from the same scale the box is given. */
.car.winner .badges {
  top: calc(var(--belt-y) + 2px - var(--box-h) * 1.4 - 10px - 100px);
  width: 520px;
  height: 100px;
  margin-left: -260px;
  gap: 8px;
  overflow: visible;
  transition: top 700ms var(--ease);
}
/* with the names, not under the box: a caption below the package would be
   pointing back up past it */
.car.winner .badges .mini-title { width: 100%; margin: 0; text-align: center; }
.car.winner .badge {
  font-size: 15px;
  line-height: 1.2;
  gap: 6px;
  padding: 7px 14px 7px 8px;
  box-shadow: var(--shadow-float);
}
.car.winner .badge .badge-icon { font-size: 18px; }
.car.winner .badge .badge-name { max-width: none; }
.car.winner .badge.teacher { box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow-float); }

/* ---------- instructions slideshow ---------- */

.start-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

/* This screen's bar is the tallest in the app on its last slide — dots, nav
   row, and the invitation above them — so it reserves more than the shared
   --cta-space. Reserved on every slide, not just the one that needs it: the
   deck must not shift under the class between two presses of Next. */
#screen-instructions { padding-bottom: calc(var(--cta-space) + 60px); }

.instr-slides { width: 100%; max-width: 680px; }

/* The real conveyor at reduced scale: the .window inside is the run screen's
   machine markup verbatim, so every part inherits the machine CSS. app.js
   scales it to the container width and sets the container height to match. */
.instr-machine {
  position: relative;
  width: 100%;
  margin-bottom: 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: #fff;
  overflow: hidden;
}
.instr-machine .window { width: 680px; transform-origin: 0 0; }
.instr-slide { text-align: center; animation: screen-in 300ms var(--ease) both; }
.instr-slide .panel-title { margin-bottom: 4px; }
.instr-slide .hint { margin-bottom: 0; min-height: 3em; }
/* The instruction that matters, lifted out of the muted sentence around it: a
   class reads these slides off a projector, and the bolded run is what has to
   carry from the back of the room. Full ink, not a heavier grey. */
.instr-slide .hint strong { color: var(--ink); font-weight: 600; }
.instr-slide .btn { margin-top: 14px; }

.instr-viz {
  position: relative;
  height: 150px;
  margin-bottom: 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: #fff;
  overflow: hidden;
}
.instr-viz::before, .instr-viz::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 56px;
  z-index: 3;
  pointer-events: none;
}
.instr-viz::before { left: 0; background: linear-gradient(to right, #fff 10%, rgba(255, 255, 255, 0)); }
.instr-viz::after { right: 0; background: linear-gradient(to left, #fff 10%, rgba(255, 255, 255, 0)); }

/* The same package sprite the machine uses, so the illustration and the real
   thing are the same object. Sized on the sprite's own 128.75 × 101.82 ratio —
   any other box shape here would stretch the render — and bottom-aligned to the
   mini belt line at 100px, because the sprite's bottom edge IS that line. */
.viz-card {
  position: absolute;
  top: 53px;
  left: 50%;
  width: 60px;
  height: 47px;
  margin-left: -30px;
  background: var(--box-sprite) no-repeat center / 100% 100%;
  z-index: 2;
}
/* The machine's stencil, at the card's scale: .car .car-num's face-plane
   projection with the sprite's shrink (60 / 128.75) composed in front, so the
   number lands on the same printed spot on the cardboard as on the belt. */
.viz-card .viz-num {
  position: absolute;
  left: var(--face-left);
  top: var(--face-top);
  width: var(--face-w);
  height: var(--face-h);
  transform-origin: 0 0;
  transform: scale(0.466) var(--face-matrix) translate(var(--num-x), var(--num-y));
  display: flex;
  align-items: center;
  justify-content: flex-start;
  line-height: 1;
  font-size: var(--num-size);
  font-weight: 700;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: #fff3d6;
  text-shadow: 0 1px 2px rgba(92, 46, 0, 0.5);
}

/* The game's own Step call-to-action, centred under an interactive machine. */
.instr-cta { display: flex; justify-content: center; margin: -2px 0 18px; }
.instr-cta .btn { margin: 0; min-width: 130px; }

/* The run screen's fallen strip, reused under the auto-run machine. */
.instr-fallen {
  width: 100%;
  max-width: 560px;
  margin: -6px auto 18px;
  min-height: 50px;
  padding-top: 8px;
}
/* A discarded box keeps its caller, so the pile reads as who is out and not
   just which numbers went — which is the whole point of showing names ride the
   line. The run screen's chip is a fixed 52 sized for a number alone, so here
   it takes the width the name needs. */
.instr-fallen .fallen-card { width: auto; min-width: 52px; padding: 6px 7px; }

/* The mini run's win is the glow alone — the sprite is not scaled up the way
   the real celebration scales it, so the badge holder keeps its belt-riding
   placement and size instead of taking the winner screen's. */
.instr-machine .car.winner .badges {
  top: calc(var(--belt-y) + 2px - var(--box-h) - 48px);
  width: 176px;
  height: 44px;
  margin-left: -88px;
  gap: 3px;
  overflow: hidden;
}
.instr-machine .car.winner .badge {
  font-size: 11px;
  line-height: 1;
  gap: 3px;
  padding: 3px 7px 3px 4px;
  box-shadow: none;
}
.instr-machine .car.winner .badge .badge-icon { font-size: 13px; }
.instr-machine .car.winner .badge.teacher { box-shadow: 0 0 0 2px var(--accent-soft); }

/* You against the well-practiced teacher: the game's own badges, larger. */
.instr-vs {
  height: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 12px;
  padding: 52px 24px;
}
.instr-vs .badge { font-size: 15px; gap: 7px; padding: 8px 16px 8px 11px; }
.instr-vs .badge .badge-icon { font-size: 21px; }
.instr-vs .badge .badge-name { max-width: none; }
.vs-mark {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 8px;
}

/* The setup screen's dials, shown as a still life. */
.instr-dials {
  height: auto;
  display: flex;
  align-items: stretch;
  justify-content: center;
  flex-wrap: wrap;
  gap: 14px;
  padding: 24px;
}
.instr-dials .tile { text-align: center; }
.instr-dials .step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.instr-dials .dial { justify-content: center; }
.instr-dials .dial-value { display: inline-block; }

/* The table screen's two conveyor decisions, live but read-only: the box count
   above the starting-position strip, in the order the table screen asks them.
   Auto margins, or the 560 sits against the left edge of the 680 slide while
   the title and hint under it are centred — the strip, its caption and the
   sentence they belong to all have to share one centre line. */
.instr-line { width: 100%; max-width: 560px; margin: 0 auto 18px; }

/* The "# of boxes" field as a dial, at slide scale rather than the setup
   screen's 54px numeral, and inert: createInstrLine drives the value. */
.instr-count {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  margin-bottom: 14px;
}
.instr-count .dial { justify-content: center; margin-top: 0; gap: 12px; }
.instr-count .dial-value {
  display: inline-block;
  font-size: 34px;
  line-height: 1.2;
  border-color: transparent;
}
.instr-count .step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  font-size: 18px;
  pointer-events: none;
}
/* the count landing on a new value, the same beat the strip repopulates on */
.instr-count .dial-value.bump { animation: count-bump 380ms var(--ease); }
@keyframes count-bump {
  0% { transform: scale(1); color: var(--ink); }
  40% { transform: scale(1.18); color: var(--accent); }
  100% { transform: scale(1); color: var(--ink); }
}

/* Scenes: the bets, split over two slides. "Place your bets" loops the two
   player chips landing on their picks; "The teacher bets blind" holds the
   sealed dashes and stamp still while the teacher's chip lands. Base styles
   are the settled tableau, which is what reduced motion shows. */
.viz-bets .viz-card { top: 58px; }
.viz-bets .bcard-1 { margin-left: -148px; }
.viz-bets .bcard-2 { margin-left: -72px; }
.viz-bets .bcard-3 { margin-left: 4px; }
.viz-bets .bcard-4 { margin-left: 80px; }

.viz-bet {
  position: absolute;
  top: 38px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 500;
  line-height: 1;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 4px 9px 4px 6px;
  z-index: 3;
  transform: translateX(-50%);
}
.viz-bet .viz-bet-icon { font-size: 13px; line-height: 1; }
.viz-bet.on-1 { left: calc(50% - 118px); }
.viz-bet.on-2 { left: calc(50% - 42px); }
.viz-bet.on-3 { left: calc(50% + 34px); }
.viz-bet.player-a { opacity: 1; animation: viz-bet-a 4.5s var(--ease) infinite; }
.viz-bet.player-b { opacity: 1; animation: viz-bet-b 4.5s var(--ease) infinite; }
.viz-bet.dash {
  color: var(--faint);
  font-size: 12px;
  padding: 5px 8px;
}
.viz-bet.teacher {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);
  animation: viz-bet-teacher 5.5s var(--ease) infinite;
}
.viz-stamp {
  position: absolute;
  left: 50%;
  top: 112px;
  transform: translateX(-50%);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  background: var(--accent-soft);
  border-radius: 999px;
  padding: 3px 10px;
}
@keyframes viz-bet-a {
  0%, 8% { opacity: 0; transform: translate(-50%, -18px) scale(0.6); }
  13%, 90% { opacity: 1; transform: translate(-50%, 0); }
  96%, 100% { opacity: 0; transform: translate(-50%, 0); }
}
@keyframes viz-bet-b {
  0%, 22% { opacity: 0; transform: translate(-50%, -18px) scale(0.6); }
  27%, 90% { opacity: 1; transform: translate(-50%, 0); }
  96%, 100% { opacity: 0; transform: translate(-50%, 0); }
}
@keyframes viz-bet-teacher {
  0%, 24% { opacity: 0; transform: translate(-50%, -20px) scale(0.5); }
  31%, 92% { opacity: 1; transform: translate(-50%, 0); }
  98%, 100% { opacity: 0; transform: translate(-50%, 0); }
}

/* The invitation that closes the deck: the words, then an arrow aimed at Start
   game. It sits at the top of the bar's stack, so it is the bar's own gap that
   separates the arrow's point from the button — a fixed distance at every
   window height. Left in the slide's flow it landed wherever the centred deck
   happened to end, which on a tall window is halfway up the page from the
   button it is pointing at. */
.instr-invite {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  animation: invite-in 460ms var(--ease) 240ms backwards;
}
.invite-text {
  margin: 0;
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
}
/* The nod: a slow fall and recovery, the arrow fading as it reaches for the
   button and coming back up at full ink. Two seconds a cycle — a pointer that
   twitches competes with the machine still running above it. */
.invite-pointer {
  width: 26px;
  height: 40px;
  fill: none;
  stroke: var(--accent);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  animation: invite-nod 2s var(--ease) 700ms infinite;
}
@keyframes invite-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
@keyframes invite-nod {
  0%, 100% { transform: translateY(0); opacity: 1; }
  45% { transform: translateY(9px); opacity: 0.45; }
  80% { transform: translateY(0); opacity: 1; }
}

.instr-dots { display: flex; gap: 8px; justify-content: center; margin: 0; }
.instr-dot {
  appearance: none;
  width: 9px;
  height: 9px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid #ccc;
  background: #fff;
  cursor: pointer;
  transition: background 150ms var(--ease), border-color 150ms var(--ease), transform 150ms var(--ease);
}
.instr-dot:hover { border-color: var(--faint); }
.instr-dot.selected { background: var(--accent); border-color: var(--accent); transform: scale(1.2); }

.instr-nav { display: flex; gap: 10px; justify-content: center; }
.instr-nav .btn { min-width: 90px; }

/* ---------- the practice room ----------
   The teacher's back door off the title screen: the two facts a round hands
   you, a miniature of the real machine, and a log of what came out. It is the
   one screen in the app with no class in front of it, so it is the one screen
   that may put a scrolling table on the page — everything else is composed for
   a projector at the back of a room. */

/* the entry, in the corner every other screen gives to Back. The title's other
   furniture fades up; a button that simply appeared would be the one thing on
   screen that arrived instead of resolving. */
.practice-entry { animation: screen-fade 560ms var(--ease) backwards; }

/* No fixed .cta-bar on this screen — the transport sits under the machine it
   drives — so the room every other screen reserves for one comes back. */
.practice-screen { padding-bottom: 44px; }
/* the title carries the room on its own — the dials say what they want, and the
   verdict under the belt says what came back */
.practice-head { margin-bottom: 22px; }

/* The log is a column of its own so it can scroll without taking the dials or
   the machine with it — the whole point of the left-hand rail. */
.practice-grid {
  display: grid;
  grid-template-columns: 300px minmax(0, 1fr);
  gap: 20px;
  align-items: start;
}

.practice-log {
  position: sticky;
  top: 24px;
  display: flex;
  flex-direction: column;
  padding: 18px;
  margin-bottom: 0;
}
.practice-log .tile-head { margin-bottom: 10px; }

/* The scroller is the table's body, not the panel: the heading row and the
   Clear button stay put while a long session runs past underneath them. */
.log-scroll {
  min-height: 128px;
  max-height: min(52vh, 420px);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* the gutter is padded rather than margined so a focus ring on a row is not
     clipped by the scroll box */
  margin: 0 -6px;
  padding: 0 6px;
}

.log-table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
.log-table th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: #fff;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  text-align: right;
  padding: 0 6px 8px;
  border-bottom: 1px solid var(--line);
}
.log-table td {
  font-size: 16px;
  text-align: right;
  padding: 9px 6px;
  border-bottom: 1px solid var(--line);
}
.log-table tr:last-child td { border-bottom: 0; }
/* the drill number is an index, not a quantity: left, and quiet */
.log-table .col-i { text-align: left; color: var(--faint); font-size: 13px; width: 2.6em; }
/* the one number the drill was for */
.log-table .col-last { font-weight: 600; }
.log-table td.col-last { color: var(--accent); }

/* Striking one drill off. Red, because it destroys a record and nothing else in
   the log does — but held back at rest: a column of twenty full-strength ✕s
   would be the loudest thing in a panel whose subject is four small numbers.
   The pointer brings it up to full. */
.log-table .col-del { width: 26px; padding-left: 0; padding-right: 0; text-align: center; }
.log-del {
  appearance: none;
  font: inherit;
  font-size: 12px;
  line-height: 1;
  width: 22px;
  height: 22px;
  padding: 0;
  color: var(--urgent);
  opacity: 0.62;
  background: none;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: opacity 120ms var(--ease), background 120ms var(--ease);
}
.log-del:hover { opacity: 1; background: var(--urgent-soft); }
.log-del:focus-visible { opacity: 1; }
/* the row lands at the top of the log, so the newest drill is the one you are
   already looking at when the belt stops */
.log-row.fresh { animation: log-in 380ms var(--ease) both; }
@keyframes log-in {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: none; }
}

.log-empty { color: var(--faint); font-size: 14px; margin: 18px 6px; text-wrap: balance; }
.log-foot { display: flex; justify-content: flex-end; margin-top: 12px; }
.log-foot .nav-btn { font-size: 13px; padding: 5px 10px; }
.log-foot .nav-btn:disabled { opacity: 0.4; cursor: default; background: none; border-color: transparent; color: var(--faint); }

/* the two facts, as the setup screen's dial without its steppers */
.practice-dials { display: flex; align-items: stretch; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.pdial {
  flex: 1 1 200px;
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 12px 14px 10px;
}
.pdial .tile-label { margin-bottom: 4px; }
.pdial .dial-value {
  display: block;
  width: 100%;
  font-size: 40px;
  text-align: left;
  padding: 0 8px;
  margin-left: -8px;
}
/* the die, beside the dials rather than under them: rolling a line is a way of
   setting the two fields, not a third thing to decide */
.practice-roll { align-self: flex-end; margin-bottom: 4px; }
.practice-roll .nav-glyph { font-size: 15px; }

.practice-machine { margin-bottom: 0; }

/* The pile, compact: at 58 boxes the class's 52px chips would be five rows of
   debris under a screen whose subject is one number. Chips are laid out hidden
   from the first step (see the run's buildFallenPile), so the strip claims its
   height up front and the transport below it never moves — which is also why
   the rule the class's strip hangs its chips from comes off here. Empty, it
   would be a line ruled under the machine for no reason, and it is empty at the
   start of every single drill.

   The height cap is set by app.js (sizeStrip), not here: it has to be an exact
   number of chip rows or the row above the fold gets sliced in half, and a chip
   is only as tall as its numeral's font makes it. The gap below is the one
   number the two share — change it in both. */
.practice-fallen {
  max-width: none;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  gap: 6px;
  margin-top: 14px;
  padding: 0;
  border-top: 0;
}
.practice-fallen .fallen-card { width: 40px; padding: 4px 2px; border-radius: 7px; }
.practice-fallen .fallen-card .fallen-num { font-size: 14px; }

.practice-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  flex-wrap: wrap;
  margin-top: 20px;
}
.practice-controls .btn .glyph { font-size: 15px; line-height: 1; margin-right: 8px; }
/* The answer, stated in type under the machine. The number is painted on the
   cargo too, but a numeral on a photograph of a box is not a result — this is
   the line the drill exists to produce, so it is written down. It holds its
   height empty so the controls never move when it fills. */
.practice-verdict { width: 100%; min-height: 1.5em; margin: 0; }
.practice-verdict strong { color: var(--ink); font-weight: 600; }

@media (max-width: 980px) {
  /* the machine leads and the log follows it: on a narrow window the record of
     the round should not be the first thing between you and the round */
  .practice-grid { grid-template-columns: minmax(0, 1fr); }
  .practice-stage { order: 1; }
  .practice-log { order: 2; position: static; }
  .log-scroll { max-height: 40vh; }
}

/* ---------- reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .log-row.fresh,
  .practice-entry { animation: none; }
  .screen-inner,
  .roster-row,
  .preview-card.pop-in,
  .panel-bet.leaving,
  .board-wrap.arriving,
  .badge.fly,
  .fallen-card.landed,
  .winner-panel { animation: none; }
  .car.winner .badges { transition: none; }
  /* instructions scenes fall back to their base styles: a settled tableau.
     The mini conveyors stay usable — app.js collapses their timings. */
  .instr-slide,
  .viz-bet,
  .instr-count .dial-value.bump,
  .instr-invite { animation: none; }
  /* the arrow still has to point, so it keeps its ink and loses only the nod */
  .invite-pointer { animation: none; }
  /* without the pulse, red alone is all that separates the last stretch from
     the warning one — so the numerals take a tinted pill instead. */
  .countdown.final { animation: none; background: var(--urgent-soft); }
  .btn:active { transform: none; }
  .car { transition: none; }
  .door { transition: opacity 160ms linear; }
  .door.open { transform: none; opacity: 0; }
}
