/* MiniPoker Arena -- one stylesheet, no build step, no framework.
 *
 * Two rules run through everything here.
 *
 * COLOUR.  Amber is P1's and teal is P2's; nothing else may use them.  That is
 * enforced by indirection rather than discipline: `--p1` and `--p2` are read in
 * exactly one place each, where they are assigned to a scoped `--seat`, and
 * every seat-coloured part reads `--seat`.  Bone is spent only on what the room
 * must read first -- card values, the pot, the equilibrium notch.  Red only ever
 * means a broken agent.  `tests/test_css_discipline.py` greps this file to keep
 * it that way.
 *
 * SCALE.  One layout, two viewing distances.  Anything the back row has to read
 * scales with the viewport through `clamp(min, vw, max)`; anything that is
 * reference material -- notation, axis ticks, the league table -- holds a fixed
 * px size.  Card typography is in `cqw` so a card is internally consistent at
 * any size.  Between 1280px and 1440px nothing moves: the stage compresses and
 * the panel narrows to its 272px floor, and that is all.
 *
 * Below 1280px the shell restacks into a single scrolling column -- see the
 * narrow-screen block at the end of this file.  A phone is not one of those two
 * viewing distances, and holding the desktop grid there would put a 272px side
 * panel next to a 100px stage.  The components are untouched by that block;
 * only the shell around them changes.  `tests/test_css_discipline.py` checks
 * that no media query can reach into the desktop range.
 */

:root {
  --stage:   #131210;
  --surface: #181613;
  --line:    #35302A;
  --bone:    #F2EDE1;
  --p1:      #E9A13C;
  --p2:      #4FBFB8;
  --heart:   #B8493C;
  --alert:   #E4564A;
  --muted:   #8B8478;

  /* Tones the prototype uses that the palette block never named. */
  --ink-2:   #B9B1A2;
  --faint:   #6E6961;
  --rule:    #262320;
  --dash:    #4A453C;
  --spade:   #171512;

  --font-display: 'Bricolage Grotesque', 'Avenir Next', 'Segoe UI', system-ui, sans-serif;
  --font-body:    'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --font-num:     'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Room reading: scales with the viewport. */
  --t-name:   clamp(17px, 1.55vw, 26px);
  --t-pot:    clamp(38px, 4.6vw, 76px);
  --t-bubble: clamp(13px, 1.25vw, 20px);
  --t-score:  clamp(20px, 2vw, 34px);
  --card-w:   clamp(58px, 6.2vw, 92px);

  /* Reference material: fixed, small, never scaled. */
  --t-label: 10px;
  --t-note:  11px;
  --t-mono:  12px;
}

* { box-sizing: border-box; }

/* A class that sets `display` outranks the UA rule for [hidden], and the result
 * is a layer that stays on screen with correct data behind it -- right numbers,
 * wrong picture, no error anywhere.  That cost a real bug once.  This rule ends
 * the whole class of them; `tests/test_css_discipline.py` checks it is here and
 * that every hideable component still declares its own [hidden] variant. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--stage);
  color: var(--ink-2);
  font-family: var(--font-body);
  overflow: hidden;
}

/* --- shell ------------------------------------------------------------- */

.app {
  height: 100vh;
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(272px, 23vw, 352px);
  grid-template-rows: auto minmax(0, 1fr) auto;
}

.topbar {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: clamp(10px, 1.4vw, 20px);
  padding: 0 clamp(12px, 1.6vw, 24px);
  height: clamp(40px, 4.4vh, 56px);
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
}

/* The top strip never reflows.  When it runs out of room it clips from the
 * right, because a bar that rewraps onto two lines is an element moving to a
 * different place -- which is the one thing the scale rule forbids. */
.topbar > * { white-space: nowrap; flex: none; }

.topbar__title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(13px, 1.05vw, 17px);
  color: var(--ink-2);
}

.topbar__sub {
  font-size: 9px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
}

.topbar__spacer { flex: 1 1 auto; }

.topbar__meta {
  display: flex;
  align-items: center;
  gap: clamp(8px, 1vw, 16px);
  font-family: var(--font-num);
  font-size: var(--t-note);
  color: var(--muted);
  min-width: 0;
  overflow: hidden;
}

.topbar__meta > span { white-space: nowrap; flex: none; }

.topbar__meta b { font-weight: 600; color: var(--ink-2); }

.topbar__sep { width: 1px; height: 14px; background: var(--line); }

/* The live build, stated on screen.  Not red -- the full build is the working
 * default, not a fault -- but solid enough that it is never missed, because
 * the one thing that goes wrong in ordinary use is sharing a link while it is
 * up.  The shareable build keeps the same slot and stays quiet.  Reference
 * material: fixed size, never scaled. */
.buildtag {
  font-family: var(--font-num);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: .16em;
  border-radius: 3px;
  padding: 2px 7px;
  color: var(--muted);
  border: 1px solid var(--line);
}

.buildtag--full {
  color: var(--stage);
  background: var(--ink-2);
  border-color: var(--ink-2);
}

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

.stage {
  position: relative;
  min-height: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: clamp(6px, 1vh, 14px);
  padding: clamp(8px, 1.4vh, 18px) clamp(12px, 1.6vw, 26px);
}

.seat {
  position: relative;
  display: grid;
  grid-template-columns: var(--card-w) minmax(0, 1fr) auto;
  gap: clamp(10px, 1.5vw, 22px);
  align-items: center;
}

/* The only two places --p1 and --p2 are read.  Everything seat-coloured
 * downstream reads --seat, which is what makes the colour rule checkable. */
.seat--p1 { --seat: var(--p1); }
.seat--p2 { --seat: var(--p2); }

.seat__slot { position: relative; }

.seat__body { display: grid; gap: clamp(5px, .8vh, 9px); min-width: 0; }

.seat__head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}

.seat__name {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--t-name);
  letter-spacing: -.01em;
  color: var(--seat);
}

.seat__kind {
  font-family: var(--font-num);
  font-size: var(--t-label);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}

.seat__side {
  font-family: var(--font-num);
  font-size: 9px;
  letter-spacing: .12em;
  color: var(--faint);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 1px 5px;
}

.seat__right { display: grid; gap: clamp(4px, .7vh, 8px); justify-items: end; }

.seat__scoreline { display: flex; align-items: baseline; gap: 6px; }

.seat__scorelabel {
  font-family: var(--font-num);
  font-size: var(--t-label);
  letter-spacing: .13em;
  color: var(--faint);
}

.seat__score {
  font-family: var(--font-num);
  font-weight: 700;
  font-size: var(--t-score);
  line-height: 1;
  color: var(--ink-2);
}

/* --- cards ------------------------------------------------------------- */

.card {
  container-type: size;
  width: 100%;
  aspect-ratio: 5 / 7;
  position: relative;
  user-select: none;
}

.card--spade { --card-ink: var(--spade); }
.card--heart { --card-ink: var(--heart); }

.card__face {
  position: absolute;
  inset: 0;
  border-radius: 7cqw;
  background: var(--bone);
  border: 1px solid #C9C2B2;
  overflow: hidden;
  box-shadow: 0 2cqw 7cqw rgba(0, 0, 0, .5);
  display: grid;
  place-items: center;
}

.card__value {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 62cqw;
  line-height: .82;
  letter-spacing: -.03em;
  color: var(--card-ink);
  margin-top: 2cqw;
}

.card__pip { position: absolute; width: 15cqw; height: 15cqw; }
.card__pip svg { width: 100%; height: 100%; display: block; }
.card__pip--tl { left: 7cqw; top: 5cqw; }
.card__pip--br { right: 7cqw; bottom: 5cqw; transform: rotate(180deg); }

.card__rule {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2.5cqw;
  background: var(--card-ink);
}

.card__back {
  position: absolute;
  inset: 0;
  border-radius: 7cqw;
  background: #1C1A16;
  border: 1px solid var(--line);
  overflow: hidden;
  box-shadow: 0 2cqw 6cqw rgba(0, 0, 0, .45);
}

.card__weave {
  position: absolute;
  inset: 0;
  opacity: .5;
  background-image:
    repeating-linear-gradient(45deg, var(--line) 0 1px, transparent 1px 7px),
    repeating-linear-gradient(-45deg, var(--line) 0 1px, transparent 1px 7px);
}

.card__inner {
  position: absolute;
  inset: 12cqw;
  border: 1px solid var(--line);
  border-radius: 3cqw;
}

/* A hole card is face up for the audience and unknown to the opponent.  The
 * veil is what says "the agent at the other seat cannot see this" -- it is the
 * information set, drawn.  It has to stay light: bone is the room's first
 * reading and a heavy veil turns it grey, which reads as a rendering fault
 * rather than as privacy. */
.card__veil {
  position: absolute;
  inset: 0;
  border-radius: 7cqw;
  background: rgba(19, 18, 16, .07);
  border: 1px solid rgba(139, 132, 120, .5);
  pointer-events: none;
}

/* Solid, not dashed.  The design sheet draws this slot dashed, but the same
 * sheet reserves dashing for "this chip will never be paid" -- and during the
 * phantom-clear beat the empty slot sits directly beside the phantom chips,
 * where a second dashed object costs the first one its meaning.  The slot has
 * words in it and does not need the cue. */
.card-slot--empty {
  width: 100%;
  aspect-ratio: 5 / 7;
  border: 1.5px solid var(--line);
  border-radius: 7px;
  display: grid;
  place-items: center;
  text-align: center;
  padding: 6px;
}

.card-slot--empty span {
  font-family: var(--font-num);
  font-size: 9px;
  line-height: 1.5;
  letter-spacing: .1em;
  color: #565149;
}

/* --- fold stamp -------------------------------------------------------- */

.stamp {
  position: absolute;
  inset: -6px;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.stamp span {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(15px, 1.5vw, 24px);
  letter-spacing: .06em;
  color: var(--seat);
  border: 2px solid var(--seat);
  border-radius: 4px;
  padding: 2px 8px;
  transform: rotate(-11deg);
  background: rgba(19, 18, 16, .78);
}

/* --- action bubble ----------------------------------------------------- */

.bubble {
  position: absolute;
  left: clamp(76px, 8.2vw, 124px);
  z-index: 5;
}

.seat--p1 .bubble { top: calc(-1 * clamp(11px, 1.5vh, 18px)); }
.seat--p2 .bubble { bottom: calc(-1 * clamp(11px, 1.5vh, 18px)); }

.bubble span {
  display: inline-block;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--t-bubble);
  letter-spacing: .04em;
  color: var(--stage);
  background: var(--seat);
  border-radius: 4px;
  padding: 2px 10px;
}

/* --- equity gauge ------------------------------------------------------ */

.gauge { width: clamp(96px, 11vw, 164px); }

.gauge__track {
  position: relative;
  height: clamp(9px, 1.2vh, 13px);
  border: 1px solid var(--line);
  border-radius: 2px;
  background: var(--stage);
}

.gauge__zero {
  position: absolute;
  left: 50%;
  top: -2px; bottom: -2px;
  width: 1px;
  background: var(--dash);
}

.gauge__true {
  position: absolute;
  top: 1px; bottom: 1px;
  width: 3px;
  background: var(--seat);
}

.gauge__belief {
  position: absolute;
  top: -3px; bottom: -3px;
  width: 9px;
  border: 1px solid var(--seat);
  border-radius: 2px;
}

.gauge__ticks {
  display: flex;
  justify-content: space-between;
  margin-top: 3px;
  font-family: var(--font-num);
  font-size: 9px;
  letter-spacing: .1em;
  color: var(--faint);
}

/* --- strategy strip ---------------------------------------------------- */

.strip { display: grid; gap: 4px; }

.strip__head {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-num);
  font-size: var(--t-label);
  letter-spacing: .12em;
  color: var(--faint);
}

.strip__rule { flex: 1; height: 1px; background: var(--rule); }

.strip__tabs { display: flex; gap: 4px; }

.strip__tab {
  font-family: var(--font-num);
  font-size: 9px;
  letter-spacing: .1em;
  color: var(--faint);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 1px 5px;
  background: none;
  cursor: pointer;
}

.strip__tab[aria-selected='true'] { color: var(--ink-2); border-color: var(--muted); }

.strip__bars {
  display: flex;
  gap: clamp(6px, .8vw, 12px);
  align-items: flex-end;
  max-width: 540px;
}

.strip__bar { flex: 1; min-width: 0; display: grid; gap: 3px; }

.strip__track {
  position: relative;
  height: clamp(16px, 2.1vh, 24px);
  border: 1px dashed var(--dash);
  border-radius: 3px;
}

/* The three marks are told apart by SHAPE, not by tone.  Two pale outlines a
 * few points apart in luminance are indistinguishable from the back of a room
 * and disappear entirely through a projector, so:
 *
 *   observed   solid horizontal fill, full height
 *   declared   thin outline, inset inside the track
 *   Nash       vertical tick that overhangs the track top and bottom
 *
 * An overhanging vertical rule cannot be confused with an enclosed rectangle
 * at any distance, which is the property that had to be bought. */
.strip__obs {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  background: var(--seat);
  border-radius: 2px;
}

/* Fewer than five observations is not a frequency, it is an anecdote. */
.strip__obs--thin { opacity: .38; }

.strip__decl {
  position: absolute;
  left: 0; top: 2px; bottom: 2px;
  border: 1px solid var(--ink-2);
  border-radius: 2px;
}

.strip__notch {
  position: absolute;
  top: -6px; bottom: -6px;
  width: 3px;
  background: var(--bone);
  border-radius: 1px;
}

/* A broken tick means the notch was weighted by the equilibrium's own self-play
 * reach rather than by the cards this match dealt, because fewer than ten hands
 * reached the node.  It answers a slightly different question from the bar next
 * to it and must not pass as directly comparable.  About half of all
 * card-level notches are in this state, so the distinction is routine. */
.strip__notch--eq {
  background: repeating-linear-gradient(
    to bottom, var(--bone) 0 3px, transparent 3px 6px);
  opacity: .75;
}

/* An action that is provably never worth playing.  Stage-1 bets never reach
 * the payout, so folding there trades a hand worth at least -1 for exactly -1;
 * the equilibrium plays it at 4e-09. */
.strip__bar--dominated .strip__track { opacity: .55; }
.strip__key--dominated { color: var(--muted); }
.strip__key--dominated sup { color: var(--muted); margin-left: 2px; }
/* Red only when an agent actually played the dominated action: an
 * advisory dagger is not a fault report, a bar under one is. */
.strip__key--broken sup { color: var(--alert); }

.strip__key {
  font-family: var(--font-num);
  font-size: var(--t-label);
  letter-spacing: .08em;
  color: var(--muted);
}

/* --- felt -------------------------------------------------------------- */

.felt {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 0;
}

.felt__stack {
  display: grid;
  justify-items: center;
  gap: clamp(6px, 1vh, 12px);
  width: 100%;
}

.felt__row {
  display: flex;
  align-items: center;
  gap: clamp(14px, 2.2vw, 38px);
}

.board { display: grid; justify-items: center; gap: 5px; }

.board__label,
.pot__label {
  font-family: var(--font-num);
  font-size: var(--t-label);
  letter-spacing: .16em;
  color: var(--faint);
}

.board__slot { width: var(--card-w); position: relative; }

.pot {
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: clamp(6px, 1vh, 12px) clamp(14px, 1.8vw, 28px);
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface);
}

.pot__value {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--t-pot);
  line-height: .95;
  color: var(--bone);
}

.pot__note {
  font-family: var(--font-num);
  font-size: var(--t-label);
  letter-spacing: .1em;
  color: var(--muted);
  min-height: 12px;
}

.pot__note--phantom { color: var(--ink-2); }

/* --- chips ------------------------------------------------------------- */

.chips {
  height: clamp(22px, 3vh, 34px);
  display: flex;
  align-items: center;
  gap: 6px;
}

.chips--p1 { --seat: var(--p1); }
.chips--p2 { --seat: var(--p2); }

.chip {
  width: clamp(16px, 1.7vw, 24px);
  height: clamp(16px, 1.7vw, 24px);
  border-radius: 50%;
  border: 1.5px solid var(--seat);
  background: color-mix(in srgb, var(--seat) 38%, transparent);
}

/* Dashed and half transparent means this chip will never be paid.  It is the
 * only dashed object on the stage, which is the whole point. */
.chip--phantom {
  border-style: dashed;
  background: none;
  opacity: .7;
}

/* --- caption and badges ------------------------------------------------ */

.caption {
  min-height: clamp(18px, 2.4vh, 26px);
  display: grid;
  place-items: center;
}

/* Badges bring their own ink; the caption rule must not repaint them. */
.caption span:not(.badge) {
  font-family: var(--font-num);
  font-weight: 600;
  font-size: clamp(11px, 1vw, 16px);
  letter-spacing: .14em;
  color: var(--ink-2);
  text-align: center;
}

.badge {
  font-family: var(--font-num);
  font-weight: 600;
  font-size: var(--t-note);
  letter-spacing: .14em;
  border-radius: 3px;
  padding: 2px 8px;
}

.badge--split { color: var(--stage); background: var(--ink-2); }
.badge--reset { color: var(--muted); border: 1px solid var(--line); }

.badge--rank {
  font-family: var(--font-num);
  font-size: 9px;
  letter-spacing: .12em;
  color: var(--stage);
  background: var(--ink-2);
  border-radius: 3px;
  padding: 1px 6px;
}

/* --- void overlay: the only red on the screen -------------------------- */

/* `display: grid` outranks the UA rule for [hidden], so say it explicitly --
 * otherwise the overlay veils the whole felt on every non-void hand. */
.void[hidden] { display: none; }

.void {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgba(19, 18, 16, .86);
  border: 1px solid #4A2622;
  border-radius: 8px;
}

.void__inner {
  display: grid;
  justify-items: center;
  gap: 8px;
  text-align: center;
  padding: 14px;
}

.void__tag {
  font-family: var(--font-num);
  font-weight: 700;
  font-size: var(--t-note);
  letter-spacing: .2em;
  color: var(--stage);
  background: var(--alert);
  border-radius: 3px;
  padding: 3px 10px;
}

.void__title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(18px, 1.8vw, 30px);
  color: var(--ink-2);
}

.void__reason {
  font-family: var(--font-num);
  font-size: var(--t-mono);
  letter-spacing: .06em;
  color: var(--ink-2);
}

.void__detail {
  font-family: var(--font-body);
  font-size: var(--t-note);
  color: var(--muted);
  max-width: 42ch;
}

/* --- side panel -------------------------------------------------------- */

.panel {
  border-left: 1px solid var(--rule);
  background: var(--surface);
  display: grid;
  grid-template-rows: auto auto auto minmax(0, 1fr);
  min-height: 0;
  overflow: hidden;
}

.panel section { padding: clamp(8px, 1.2vh, 14px) clamp(10px, 1vw, 16px); min-height: 0; }
.panel section + section { border-top: 1px solid var(--rule); }

.panel__title {
  font-family: var(--font-num);
  font-size: var(--t-label);
  letter-spacing: .18em;
  color: var(--faint);
  display: block;
  margin-bottom: 6px;
}

.league { display: grid; gap: 2px; }

.league__row {
  display: grid;
  grid-template-columns: 16px minmax(0, 1fr) 40px 38px 26px 46px;
  gap: 6px;
  align-items: center;
  font-family: var(--font-num);
  font-size: var(--t-note);
  color: var(--muted);
  padding: 2px 4px;
  border-radius: 3px;
}

.league__row span:nth-child(2) {
  font-family: var(--font-body);
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.league__row span:nth-child(n+3) { text-align: right; }

.league__head { color: var(--faint); font-size: 9px; letter-spacing: .1em; }

.league__row { cursor: pointer; }
.league__row--live { background: var(--rule); }

/* One agent's run across its pairings, from the three-integer summaries in
 * index.js.  Reference material: fixed 46x12, never scaled. */
.spark { display: block; width: 46px; height: 12px; }
.spark__svg { display: block; width: 46px; height: 12px; overflow: visible; }
.spark__line { fill: none; stroke: var(--muted); stroke-width: 1; }
.spark__zero { stroke: var(--rule); stroke-width: 1; }
.league__row--live .spark__line { stroke: var(--ink-2); }
.league__row--p1 span:nth-child(2) { color: var(--p1); }
.league__row--p2 span:nth-child(2) { color: var(--p2); }

/* A row that crashed is not a competitor that scored badly. */
.league__row--alert { border-left: 2px solid var(--alert); }
.league__row--alert span:nth-child(4) { color: var(--alert); }

.picker {
  width: 100%;
  font-family: var(--font-num);
  font-size: var(--t-note);
  color: var(--ink-2);
  background: var(--stage);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 4px 6px;
}

.picker__broken { color: var(--alert); }

.curve {
  position: relative;
  height: clamp(70px, 12vh, 120px);
  border: 1px solid var(--line);
  border-radius: 4px;
  overflow: hidden;
  cursor: crosshair;
}

.curve__svg { display: block; width: 100%; height: 100%; }
.curve__line { fill: none; stroke: var(--ink-2); stroke-width: 1.2; }
.curve__zero { stroke: var(--rule); stroke-width: 1; }
/* Where a new pairing began.  Agent state is not reset between pairings in the
 * instructor's format, so a learning agent's drift shows up as a kink here. */
.curve__break { stroke: var(--dash); stroke-width: 1; stroke-dasharray: 2 3; }
.curve__now { fill: var(--ink-2); }

.curve__note {
  position: absolute;
  right: 6px; top: 4px;
  font-family: var(--font-num);
  font-size: 9px;
  letter-spacing: .1em;
  color: var(--faint);
}

.filters { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 5px; }

.chipbtn {
  font-family: var(--font-num);
  font-size: 9px;
  letter-spacing: .08em;
  color: var(--muted);
  background: none;
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 1px 5px;
  cursor: pointer;
}

.chipbtn[aria-pressed='true'] { color: var(--stage); background: var(--ink-2); }
.chipbtn:disabled { opacity: .35; cursor: default; }

.gauge__legend { letter-spacing: .06em; }

/* Only the list scrolls.  When the whole section did, the filter buttons rode
 * away with it the moment the log jumped to a hand past the fold -- controls
 * that leave the screen when you use them. */
.log {
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr);
  overflow: hidden;
  min-height: 0;
}

/* Positioned so that a row's offsetTop is measured against this box: the
 * centring maths in renderPanel reads it, and against a static parent it
 * silently measures from the page instead. */
.log__list {
  display: grid;
  gap: 1px;
  align-content: start;
  overflow: auto;
  position: relative;
  min-height: 0;
}

.log__row {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr) 34px;
  gap: 6px;
  font-family: var(--font-num);
  font-size: var(--t-note);
  color: var(--muted);
  padding: 2px 4px;
  border-radius: 3px;
  white-space: nowrap;
}

.log__row b { font-weight: 400; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; }
.log__row i { font-style: normal; text-align: right; }
.log__row--void b { color: var(--alert); }
.log__row--live { background: var(--rule); }

/* --- transport --------------------------------------------------------- */

.devbar {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px clamp(12px, 1.6vw, 24px);
  border-top: 1px solid var(--rule);
  background: var(--surface);
  font-family: var(--font-num);
  font-size: var(--t-note);
  color: var(--faint);
  white-space: nowrap;
  overflow: hidden;
}

.devbar[hidden] { display: none; }

.devbar button {
  font-family: var(--font-num);
  font-size: var(--t-note);
  color: var(--muted);
  background: none;
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 3px 9px;
  cursor: pointer;
}

.devbar button[aria-pressed='true'] { color: var(--stage); background: var(--ink-2); }

.devbar__gap { flex: 1; }

.devbar__scrub {
  flex: 0 0 clamp(120px, 16vw, 260px);
  height: 4px;
  border: 1px solid var(--line);
  border-radius: 2px;
  position: relative;
  overflow: hidden;
}

.devbar__scrub i { position: absolute; left: 0; top: 0; bottom: 0; background: var(--muted); }

/* --- motion ------------------------------------------------------------
 * Every animation is a CSS entrance played when the renderer mounts the frame.
 * There is no requestAnimationFrame loop anywhere: the player advances a frame
 * index and the stylesheet does the rest, which is what keeps single-stepping,
 * rewinding and speed changes free. */

@keyframes dealIn {
  from { opacity: 0; transform: translateY(-16px) rotate(-4deg) scale(.9); }
  to   { opacity: 1; transform: none; }
}

@keyframes bubbleIn {
  from { opacity: 0; transform: translateY(6px) scale(.94); }
  to   { opacity: 1; transform: none; }
}

@keyframes stampIn {
  0%   { opacity: 0; transform: rotate(-11deg) scale(1.9); }
  60%  { opacity: 1; transform: rotate(-11deg) scale(.97); }
  100% { opacity: 1; transform: rotate(-11deg) scale(1); }
}

@keyframes captionIn {
  from { opacity: 0; letter-spacing: .3em; }
  to   { opacity: 1; letter-spacing: .14em; }
}

@keyframes slideIn {
  from { opacity: 0; transform: translateY(var(--from, 22px)) scale(.7); }
  to   { opacity: 1; transform: none; }
}

/* The signature beat: chips drift up, blur out, and never arrive anywhere. */
@keyframes smokeOut {
  0%   { opacity: .7; transform: translate(0, 0) scale(1); filter: blur(0); }
  35%  { opacity: .55; transform: translate(var(--dx, 0), -14px) scale(1.08); filter: blur(2px); }
  100% { opacity: 0; transform: translate(calc(var(--dx, 0px) * 2.6), -58px) scale(1.5); filter: blur(9px); }
}

@keyframes payTo {
  from { transform: none; opacity: 1; }
  to   { transform: translateY(var(--to)) scale(.85); opacity: .15; }
}

@keyframes shrinkInPlace {
  from { transform: none; opacity: 1; }
  to   { transform: scale(.45); opacity: .3; }
}

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

.card--deal { animation: dealIn .3s ease-out both; }
.board__slot .card--deal { animation-duration: .34s; }
.bubble { animation: bubbleIn .22s ease-out both; }
.stamp span { animation: stampIn .35s cubic-bezier(.2, 1.5, .5, 1) both; }
.caption span { animation: captionIn .26s ease-out both; }
.badge--rank { animation: tagIn .3s ease-out both; }
.chip--enter { animation: slideIn .3s ease-out both; }
.chip--clearing { animation: smokeOut 1.4s ease-in both; }
.chips--paying .chip { animation: payTo .7s ease-in both; }
.chips--splitting .chip { animation: shrinkInPlace .6s ease-out both; }

/* Two beats of a check, in the seat's own colour. */
@keyframes knock {
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  20%      { box-shadow: -10px 0 0 -4px var(--seat); }
  55%      { box-shadow: 0 0 0 0 transparent; }
  75%      { box-shadow: -10px 0 0 -4px var(--seat); }
}

.seat--knock .seat__name { animation: knock .5s ease-out both; }

.seat--folded .seat__body,
.seat--folded .seat__right { opacity: .38; transition: opacity .3s ease; }
.seat--lost .seat__body { opacity: .55; transition: opacity .3s ease; }

.pot--pulse .pot__value { animation: tagIn .3s ease-out both; }

/* Keep the rhythm, drop the movement.  Phase durations live in JavaScript and
 * are untouched, so a hand still takes the same five seconds -- it simply
 * arrives at each state instantly instead of travelling there. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
}

/* Test hook: `#freeze=MS` stops every animation mid-flight so a screenshot can
 * be taken at a known offset into a phase.  Nothing sets this in normal use. */
.frozen *, .frozen *::before, .frozen *::after { animation-play-state: paused !important; }

/* --- narrow screens ----------------------------------------------------
 *
 * The desktop rule stands: between 1280px and 1440px nothing moves, only sizes
 * change, and `tests/test_css_discipline.py` still checks that by comparing 24
 * DOM paths at both widths.  This block fires only *below* the 1280px floor,
 * where holding that rule would mean holding a 272px side panel next to a
 * 100px stage -- which is not one layout at two distances, it is one layout
 * broken.
 *
 * What changes is the shell, not the components.  The page becomes a single
 * scrolling column: seat, felt, seat, then the panel underneath.  Every part
 * keeps its own internals, its colours and its type roles; the seat's three
 * columns become three rows because at 390px they were overlapping each other.
 */
@media (max-width: 879px) {

  /* The stage is taller than the viewport now, so the page scrolls. */
  html, body { overflow: auto; }

  .app {
    height: auto;
    min-height: 100vh;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto auto;
  }

  /* The one place the no-reflow rule is dropped on purpose: clipping the top
   * strip from the right at this width would hide all of it. */
  .topbar {
    flex-wrap: wrap;
    height: auto;
    padding: 8px clamp(10px, 3vw, 16px);
    row-gap: 4px;
  }

  .topbar__spacer { display: none; }

  .topbar__meta {
    flex-basis: 100%;
    overflow-x: auto;
    gap: 10px;
    padding-bottom: 2px;
  }

  .stage {
    grid-template-rows: auto auto auto;
    gap: 14px;
    padding: 14px clamp(10px, 3vw, 18px);
  }

  /* Card beside the name and score; strip and gauge on their own rows. */
  .seat {
    grid-template-columns: var(--card-w) minmax(0, 1fr);
    grid-template-areas: 'card head' 'strip strip' 'gauge gauge';
    gap: 10px 12px;
    align-items: start;
  }

  .seat__slot { grid-area: card; }
  .seat__body { grid-area: head; gap: 0; }
  .seat__right { grid-area: gauge; justify-items: stretch; }

  /* The strip is the widest part of a seat and the one worth the full width. */
  .seat__body .strip {
    grid-area: strip;
    margin-top: 4px;
  }

  .seat__body {
    display: contents;
  }

  .seat__head { align-items: center; gap: 6px 8px; }

  .seat__right {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
  }

  .gauge { width: min(200px, 55%); }
  .strip__bars { max-width: none; }

  /* P2's bubble hangs below its seat and P1's above; at this width they would
   * land on top of the neighbouring row, so both sit inside their own. */
  .bubble { position: static; }
  .seat--p1 .bubble, .seat--p2 .bubble { top: auto; bottom: auto; }
  .seat--p1 .bubble { grid-area: head; justify-self: end; align-self: center; }
  .seat--p2 .bubble { grid-area: head; justify-self: end; align-self: center; }

  .felt { min-height: 0; }
  .felt__row { gap: 18px; }
  .board__slot, .board { width: var(--card-w); }

  /* Panel: no longer a column beside the stage, a stack under it. */
  .panel {
    border-left: 0;
    border-top: 1px solid var(--rule);
    grid-template-rows: auto auto auto auto;
    overflow: visible;
  }

  .panel section { padding: 12px clamp(10px, 3vw, 18px); }

  /* Without a fixed-height parent the log needs its own bound, or it prints
   * every hand and the transport ends up a screen and a half down. */
  .log { display: grid; grid-template-rows: auto auto auto; overflow: visible; }
  .log__list { max-height: 44vh; overflow: auto; }

  .curve { height: 96px; }

  /* Transport stays reachable without scrolling back to it. */
  .devbar {
    position: sticky;
    bottom: 0;
    flex-wrap: wrap;
    row-gap: 6px;
    z-index: 20;
  }

  .devbar__scrub { flex: 1 1 100%; order: 9; }
  .devbar__gap { display: none; }

  /* These reserve room so the felt does not jump when a chip appears.  On a
   * phone that reservation is a third of the visible stage, and the jump is
   * cheaper than the emptiness. */
  .chips { height: 22px; }
  .caption { min-height: 20px; }
  .felt__stack { gap: 8px; }
}

/* Phones proper: the league table loses its two thinnest columns rather than
 * letting the agent name collapse to three characters. */
@media (max-width: 479px) {
  .league__row { grid-template-columns: 14px minmax(0, 1fr) 42px 40px; }
  .league__row span:nth-child(5),
  .league__row span:nth-child(6) { display: none; }
  .felt__row { flex-direction: column; gap: 12px; }
  .felt { min-height: 0; }
}

/* --- failure ----------------------------------------------------------- */

.fault {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: var(--stage);
  padding: 32px;
  z-index: 50;
}

.fault__inner {
  max-width: 62ch;
  display: grid;
  gap: 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 22px 26px;
  background: var(--surface);
}

.fault h1 { font-family: var(--font-display); font-size: 20px; color: var(--ink-2); margin: 0; }
.fault p { margin: 0; font-size: 13px; line-height: 1.6; }
.fault code { font-family: var(--font-num); font-size: var(--t-mono); color: var(--ink-2); }

/* --- gallery ----------------------------------------------------------- */

.sheet {
  height: auto;
  overflow: auto;
  padding: 28px clamp(16px, 3vw, 48px) 80px;
  display: grid;
  gap: 26px;
}

body.gallery { overflow: auto; }

.sheet__h {
  font-family: var(--font-num);
  font-size: var(--t-label);
  letter-spacing: .2em;
  color: var(--faint);
  border-bottom: 1px solid var(--rule);
  padding-bottom: 6px;
}

.sheet__grid {
  display: flex;
  flex-wrap: wrap;
  gap: 22px;
  align-items: flex-start;
}

.spec {
  display: grid;
  gap: 7px;
  justify-items: start;
  padding: 12px;
  border: 1px solid var(--rule);
  border-radius: 5px;
  background: var(--surface);
}

/* Seat scoping for the sheet.  The gallery must not borrow `.seat` itself --
 * that carries the seat's three-column grid, which is a layout, not a colour. */
.spec--p1 { --seat: var(--p1); }
.spec--p2 { --seat: var(--p2); }

.spec__label {
  font-family: var(--font-num);
  font-size: 9px;
  letter-spacing: .12em;
  color: var(--faint);
  max-width: 34ch;
  line-height: 1.5;
}

.spec__stage {
  position: relative;
  min-width: 120px;
  display: grid;
  gap: 6px;
  justify-items: start;
}

.swatch { display: grid; gap: 4px; }
.swatch__chip { width: 92px; height: 44px; border-radius: 4px; border: 1px solid var(--rule); }
