/* ===========================================================
   Squelch — felt table meets modern sports app.
   Dark by default. One hot accent (ember) reserved for danger + CTA.
   =========================================================== */
:root {
  color-scheme: dark;
  --felt-900: #0b120f;
  --felt-800: #0e1512;
  --felt-700: #12201a;
  --felt-600: #172a21;
  --felt-500: #1e3529;
  --line: rgba(226, 232, 220, 0.11);
  --line-strong: rgba(226, 232, 220, 0.22);
  --bone: #f2ece0;
  --bone-dim: #cfc8b9;
  --ink: #0c110e;
  --text: #e7e9e2;
  --text-dim: #9aa79c;
  --text-faint: #6d7c71;
  --ember: #e0533d;
  --ember-hot: #ff6a4d;
  --amber: #e5a33c;
  --mint: #6fcf9b;
  --r-sm: 8px;
  --r-md: 14px;
  --r-lg: 20px;
  --r-xl: 28px;
  --sp: 4px;
  /* theme-overridable fallbacks — themes.css redefines these per [data-theme] */
  --font-ui: 'Satoshi', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-display: 'Archivo', 'Satoshi', system-ui, sans-serif;
  --display-weight: 800;
  --display-track: -0.02em;
  --die-1: #fffdf6;
  --die-2: #f4efe2;
  --die-3: #ded6c4;
  --pip: #14110c;
  --die-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
  --cta-ink: #1a0b07;
  --cta-shadow: #9d3323;
  --surface: radial-gradient(120% 80% at 50% -10%, #1c3529 0%, #132119 45%, #0b120f 100%);
  --grain-img: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.32'/%3E%3C/svg%3E");
  --grain-opacity: 0.5;
  --grain-blend: overlay;
  --alarm: #ff5c45;
  --squelch-bg: radial-gradient(circle at 50% 45%, rgba(224, 83, 61, 0.5), rgba(120, 20, 10, 0.72));
  --stamp-ink: #ffeee9;
  /* the Twos. Deliberately NOT --alarm: a squelch is a disaster and this is a
     needle, so the taunt gets its own deeper crimson. */
  --peril-edge: #e0304a;
  /* the taunt in the log strip: --peril-edge is too dark to read there */
  --peril-log: #ff8fa3;
  --peril-ink: #ffe3e8;
  --grace-cloud: #fbf5e6;      /* cream, not white: it has warm light on it */
  --grace-rim: #f3d79a;
  --grace-ray: #ffd98a;
  --grace-ink: #4a3a1e;        /* warm brown on cream — dark text on a light cloud */
  --stamp-line: #ffeee9;
  /* the Chicken lives in the warning/amber range so it never reads as a squelch.
     Deliberately theme-independent: the bird has to stay legible and charming on
     all seventeen tables, light and dark alike, sitting on its own warm wash. */
  /* lighter and warmer than the squelch wash: ribbing, not catastrophe */
  --chicken-bg: radial-gradient(circle at 50% 44%, rgba(255, 201, 96, 0.42), rgba(58, 38, 6, 0.68));
  --chicken-ink: #ffd166;
  --chicken-line: #ffd166;
  --chicken-body: #fff5db;
  --chicken-shade: #f2dcaa;
  --chicken-comb: #e64f2c;
  --chicken-beak: #f6a623;
  --chicken-outline: #2b1c07;
  --shadow-lift: 0 10px 30px rgba(0, 0, 0, 0.45);
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  /* the turn log strip at the floor of the game screen — the chat drawer stops
     above it, so nothing double-stacks down there */
  --logbar-h: 28px;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

body {
  background: var(--felt-800);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: 'tnum' 1, 'lnum' 1;
  position: fixed;
  inset: 0;
}

.num {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: 'tnum' 1, 'lnum' 1;
  letter-spacing: var(--display-track);
}

/* felt surface: layered radial light + fibre grain */
.felt {
  position: fixed;
  inset: 0;
  z-index: 0;
  background: var(--surface);
  background-color: var(--felt-900);
  transition: background 0.4s ease;
}
.felt::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: var(--grain-opacity);
  background-image: var(--grain-img);
  mix-blend-mode: var(--grain-blend);
  pointer-events: none;
}
/* theme cross-fade */
body.theme-swap .screen > *, body.theme-swap .felt { animation: theme-fade 0.42s ease; }
@keyframes theme-fade { from { opacity: 0.15; } to { opacity: 1; } }

/* ───────── screens ───────── */
.screen {
  position: fixed;
  inset: 0;
  z-index: 1;
  display: none;
  flex-direction: column;
  padding-top: var(--safe-t);
  padding-bottom: var(--safe-b);
}
.screen.is-active { display: flex; }

.scroller {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 16px 16px 28px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
/* A scrolling flex column must never squeeze its children. Without this the
   House Rules toggles (min-height: 48px) were shrunk to exactly 48px once the
   form overflowed, and their helper text spilled out of the row and printed on
   top of the next field's label — "JUMP-OFF POINT WARNING AT" sat across
   "…reaches the amount below". */
.scroller > * { flex-shrink: 0; }
.toggle { flex: none; }

/* ───────── topbar ───────── */
.topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
  background: rgba(9, 15, 12, 0.75);
  backdrop-filter: blur(8px);
}
.topbar h2 {
  flex: 1;
  margin: 0;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: 0.01em;
}
.topbar-note {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.icon-btn {
  width: 44px;
  height: 44px;
  flex: 0 0 44px;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.03);
  color: var(--text);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s ease, transform 0.1s ease;
}
.icon-btn:active { transform: scale(0.94); background: rgba(255, 255, 255, 0.08); }

/* ───────── buttons ───────── */
.btn {
  appearance: none;
  border: 1px solid var(--line-strong);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 16px;
  font-weight: 700;
  min-height: 52px;
  padding: 12px 18px;
  border-radius: var(--r-md);
  cursor: pointer;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  transition: transform 0.09s ease, background 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
}
.btn em {
  font-style: normal;
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  text-transform: none;
}
.btn:active { transform: translateY(1px) scale(0.99); }
.btn:disabled { opacity: 0.72; cursor: not-allowed; filter: saturate(0.45); }
.btn--xl { min-height: 74px; font-size: 20px; letter-spacing: 0.01em; }
.btn--wide { flex: 1; }
.btn--primary {
  /* solid fill with a sheen rather than a two-tone gradient: the label sits on
     one predictable tone in every theme, so contrast never dips at a stop */
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0) 46%, rgba(0, 0, 0, 0.09)), var(--ember);
  border-color: rgba(0, 0, 0, 0.35);
  color: var(--cta-ink);
  box-shadow: 0 6px 0 -2px var(--cta-shadow), var(--shadow-lift);
}
.btn--primary em { color: rgba(26, 11, 7, 0.66); }
.btn--ghost { background: rgba(255, 255, 255, 0.05); border-color: var(--line-strong); }
.btn--quiet {
  min-height: 48px;
  font-size: 14px;
  font-weight: 600;
  background: none;
  border-color: var(--line);
  color: var(--text-dim);
}
.btn--danger { color: var(--ember); border-color: rgba(224, 83, 61, 0.4); }

/* ───────── home ───────── */
.screen--home { justify-content: space-between; }
.home-wrap {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 18px;
  padding: 22px 20px calc(20px + var(--safe-b));
  overflow-y: auto;
}
.brand { text-align: center; padding-top: 6vh; }
.logo { width: 86px; height: 86px; color: var(--bone); }
.brand-word {
  margin: 12px 0 2px;
  /* the exclamation belongs to the H: cancel the tracking so it sits tight */
  padding-left: 0.06em;
  font-family: var(--font-display);
  font-size: clamp(44px, 14vw, 72px);
  line-height: 0.9;
  font-weight: 900;
  letter-spacing: 0.06em;
  color: var(--bone);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.5);
}
.brand-bang {
  margin-left: -0.075em;
  letter-spacing: 0;
  color: var(--ember);
}
.brand-sub {
  margin: 10px auto 0;
  max-width: 23ch;
  font-size: clamp(12.5px, 3.5vw, 14.5px);
  line-height: 1.4;
  letter-spacing: 0.035em;
  text-wrap: balance;
  color: var(--text-faint);
}
/* forced break keeps the tagline as two balanced lines on a phone */
@media (max-width: 460px) {
  .brand-sub { max-width: none; }
  .brand-sub__br { display: block; height: 0; overflow: hidden; }
}
/* on a wide screen it has room to say it in one breath */
@media (min-width: 700px) {
  .brand-sub { max-width: 52ch; font-size: 15.5px; }
}
.home-actions { display: flex; flex-direction: column; gap: 12px; }
.home-row { display: flex; gap: 12px; }
.home-row .btn { flex: 1; }
.home-foot {
  margin: 0;
  text-align: center;
  font-size: 12px;
  color: var(--text-faint);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  flex-wrap: wrap;
}
.home-foot b { color: var(--bone-dim); }
/* Build number under the settings pills, on its own line so it never competes
   with them for space on a narrow phone. --text-dim, NOT --text-faint: the pills
   above measured 2.9:1 on faint, under the 4.5:1 floor, and this is a number the
   player is meant to actually read and report back.

   It was 10.5px of that dim grey and nobody could find it when it mattered, so it
   is now a real tap target with the figure itself at full --text contrast — the
   whole point is that a player can read it out, and then tap it to get off a
   stale build. */
.home-ver {
  display: block;
  margin: 14px auto 0;
  padding: 9px 16px;
  min-height: 44px;              /* a genuine thumb target, not a label */
  background: none;
  border: 1px solid var(--line);
  border-radius: 999px;
  text-align: center;
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-dim);
  cursor: pointer;
}
.home-ver [data-home-version] {
  /* the figure itself is the thing being read aloud, so it gets the contrast */
  color: var(--text);
  font-weight: 700;
  letter-spacing: 0.06em;
}
.home-ver em {
  display: block;
  margin-top: 2px;
  font-size: 10px;
  font-style: normal;
  letter-spacing: 0.1em;
  color: var(--text-dim);
}
.home-ver:active {
  border-color: var(--text-dim);
}
.home-ver.is-updating {
  opacity: 0.6;
  pointer-events: none;
}
.sound-toggle {
  background: none;
  border: 1px solid var(--line);
  border-radius: 999px;
  /* the settings pills (SOUND, VIBRATION, GRAPHICS) are 10.5px caps on the near
     black home screen: on --text-faint they measured 2.9:1, under the 4.5:1
     floor. --text-dim takes all three to ~7:1 and they still read as quiet
     secondary controls. */
  color: var(--text-dim);
  font: inherit;
  font-size: 10.5px;
  letter-spacing: 0.12em;
  padding: 6px 12px;
  min-height: 32px;
  cursor: pointer;
}

/* ───────── setup ───────── */
.hint { margin: 0; font-size: 13px; color: var(--text-dim); }
.err { margin: 0; font-size: 13.5px; color: var(--ember-hot); font-weight: 600; }
.player-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.player-row {
  display: flex;
  align-items: center;
  gap: 10px;
  background: rgba(255, 255, 255, 0.035);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 8px 10px;
}
.token {
  width: 40px;
  height: 40px;
  flex: 0 0 40px;
  border-radius: 50%;
  border: 2px solid rgba(0, 0, 0, 0.4);
  box-shadow: inset 0 -3px 6px rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.4);
  cursor: pointer;
  display: grid;
  place-items: center;
  font-weight: 900;
  font-size: 15px;
  color: rgba(0, 0, 0, 0.62);
}
.player-row input {
  flex: 1;
  min-width: 0;
  background: none;
  border: none;
  color: var(--text);
  font: 600 16px var(--font-ui);
  padding: 10px 4px;
  min-height: 44px;
}
.player-row input:focus { outline: none; }
.player-row .rm {
  width: 40px;
  height: 40px;
  border-radius: 10px;
  background: none;
  border: 1px solid var(--line);
  color: var(--text-faint);
  font-size: 18px;
  cursor: pointer;
}
.rules-peek {
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-md);
  padding: 12px 14px;
  font-size: 13px;
  color: var(--text-dim);
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.rules-peek b { color: var(--bone-dim); }

.dock {
  display: flex;
  gap: 10px;
  padding: 12px 16px calc(12px + var(--safe-b));
  border-top: 1px solid var(--line);
  background: rgba(9, 15, 12, 0.82);
  backdrop-filter: blur(8px);
}
.dock--inline { border: none; background: none; padding: 4px 0 0; }

/* ───────── cards / fields ───────── */
.card {
  background: rgba(255, 255, 255, 0.035);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.card h3 { margin: 0; font-size: 15px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--bone-dim); }
.field { display: flex; flex-direction: column; gap: 6px; }
.field > span { font-size: 11.5px; letter-spacing: 0.13em; text-transform: uppercase; color: var(--text-faint); }
.field input {
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  color: var(--text);
  font: 600 17px var(--font-ui);
  padding: 12px;
  min-height: 48px;
}
.field input:focus { outline: 2px solid rgba(229, 163, 60, 0.6); outline-offset: 1px; }
.code-input {
  text-transform: uppercase;
  letter-spacing: 0.5em;
  font-size: 26px !important;
  font-weight: 900 !important;
  text-align: center;
}

.code-badge {
  /* The badge is the one thing an invited player is read over the phone, so it
     carries the same fixed ink pair as the overlay chrome rather than the
     theme's grey: measured at 2.2:1 over the beach room before this, against a
     0.25 black wash that pale rooms simply showed through. */
  --ui-scrim: rgba(6, 9, 8, 0.88);
  --ui-ink-muted: #d9e0e6;
  text-align: center;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  padding: 16px;
  background: var(--ui-scrim);
}
.code-badge span,
.code-badge em {
  display: block;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ui-ink-muted);
  font-style: normal;
}
.code-badge strong {
  display: block;
  font-family: var(--font-display);
  font-size: 52px;
  line-height: 1.1;
  font-weight: 900;
  letter-spacing: 0.16em;
  color: var(--amber);
}
/* ───────── invite by link ─────────
   The link is an ADDITION to the code above it, never a replacement: the badge
   keeps the 4 letters at 52px for reading aloud on a phone call. The button and
   the fallback link text carry their own scrim plate and the fixed --ui-ink
   pair, so they hold their contrast over the palest room the same way the
   in-game overlay chrome does (see THE OVERLAY CONTRAST FLOOR below). */
.invite {
  --ui-scrim: rgba(6, 9, 8, 0.88);
  --ui-ink: #f4f7f9;
  --ui-ink-muted: #d9e0e6;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.btn--invite {
  background: linear-gradient(0deg, rgba(229, 163, 60, 0.18), rgba(229, 163, 60, 0.18)), var(--ui-scrim);
  border-color: rgba(255, 255, 255, 0.28);
  color: var(--ui-ink);
}
.btn--invite[hidden] { display: none; }
.invite .hint { color: var(--ui-ink-muted); margin: 0; }
.invite-link {
  margin: 0;
  padding: 10px 12px;
  border-radius: var(--r-md);
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: var(--ui-scrim);
}
.invite-link span {
  display: block;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ui-ink-muted);
  margin-bottom: 4px;
}
.invite-link code {
  display: block;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 14px;
  line-height: 1.45;
  word-break: break-all;
  color: var(--ui-ink);
  -webkit-user-select: all;
  user-select: all;
}
.invited-note {
  margin: 0 0 4px;
  padding: 10px 12px;
  border-radius: var(--r-md);
  border: 1px solid rgba(229, 163, 60, 0.42);
  background: rgba(229, 163, 60, 0.12);
  color: var(--text);
  font-size: 13.5px;
}
.invited-note b {
  font-family: var(--font-display);
  letter-spacing: 0.14em;
  color: var(--amber);
}
/* Arriving on an invite link: Join is the one lit thing on the screen. A slow
   ember pulse on the shadow, not a bounce — it should draw the eye, not nag. */
.btn--invited {
  animation: invited-pulse 1.9s ease-in-out infinite;
}
@keyframes invited-pulse {
  0%, 100% { box-shadow: 0 6px 0 -2px var(--cta-shadow), var(--shadow-lift), 0 0 0 0 rgba(229, 163, 60, 0.55); }
  50% { box-shadow: 0 6px 0 -2px var(--cta-shadow), var(--shadow-lift), 0 0 0 9px rgba(229, 163, 60, 0); }
}
/* The Create-table card steps back while Join is the job at hand. */
.card--muted { opacity: 0.5; }
.card--muted .btn:disabled { box-shadow: none; }

.lobby-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.lobby-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  font-weight: 600;
  background: rgba(255, 255, 255, 0.03);
}
.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--mint); flex: 0 0 9px; }
.dot.off { background: var(--text-faint); }
.tag {
  margin-left: auto;
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* ───────── house rules form ───────── */
.rule-group { display: flex; flex-direction: column; gap: 2px; }
.rule-group > h3 {
  margin: 10px 0 8px;
  font-size: 11.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--amber);
}
.rule-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: rgba(255, 255, 255, 0.028);
  margin-bottom: 8px;
}
.rule-row label { flex: 1; font-size: 14.5px; font-weight: 600; }
.rule-row small { display: block; font-weight: 500; font-size: 12px; color: var(--text-faint); margin-top: 2px; }
.rule-row input[type='number'] {
  width: 104px;
  background: rgba(0, 0, 0, 0.34);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  color: var(--bone);
  font: 800 17px var(--font-ui);
  text-align: right;
  padding: 10px;
  min-height: 46px;
}
.switch {
  position: relative;
  width: 54px;
  height: 32px;
  flex: 0 0 54px;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  background: rgba(0, 0, 0, 0.4);
  cursor: pointer;
}
.switch::after {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--text-faint);
  transition: transform 0.16s ease, background 0.16s ease;
}
.switch[aria-checked='true'] { background: rgba(111, 207, 155, 0.22); border-color: rgba(111, 207, 155, 0.5); }
.switch[aria-checked='true']::after { transform: translateX(22px); background: var(--mint); }

/* ───────── how to play ───────── */
.howto h3 {
  margin: 14px 0 6px;
  font-size: 12px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--amber);
}
.howto p { margin: 0 0 8px; font-size: 14.5px; color: var(--text); }
.howto ul { margin: 0 0 10px; padding-left: 18px; font-size: 14.5px; }
.howto li { margin-bottom: 5px; }
.howto .off { color: var(--text-faint); text-decoration: line-through; }
.combo-table { width: 100%; border-collapse: collapse; font-size: 14.5px; }
.combo-table td { padding: 9px 4px; border-bottom: 1px solid var(--line); }
.combo-table td:last-child { text-align: right; font-weight: 800; color: var(--bone); }
.combo-table tr.is-off td { color: var(--text-faint); }

/* ═══════════════════ GAME ═══════════════════ */
.screen--game { overflow: hidden; }
.topbar--game { padding: 8px 10px; }
.turn-of { flex: 1; display: flex; align-items: center; gap: 8px; min-width: 0; }
.turn-of b { font-size: 17px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.turn-of em {
  font-style: normal;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-faint);
  white-space: nowrap;
}
.turn-token { width: 22px; height: 22px; border-radius: 50%; flex: 0 0 22px; box-shadow: inset 0 -2px 4px rgba(0, 0, 0, 0.4); }

/* .banner / .banner--final lived here and styled the FINAL ROUND row. Both are
   gone with it: the round is announced in the header (#turn-sub) and the number
   being chased is the live countdown in the pill (.tally-need above), neither of
   which takes a row of its own. Nothing else in the app used .banner. */

/* the header says which round it is, in the amber the banner used to own. Keyed
   off the id because the element is a bare <em id="turn-sub"> with no class. */
#turn-sub.is-final { color: var(--amber); font-weight: 800; letter-spacing: 0.1em; }

/* scoreboard rail */
/* The chrome lives on the WRAPPER, not the scrolling strip. The strip itself is
   masked at whichever edge still has seats past it (see .rail--more-*), and a
   mask would eat the divider line and the shadow along with the chips if the two
   shared an element. Splitting them also keeps the mask off the themed
   backgrounds, which differ between the flat table and the 3D one. */
.rail-wrap {
  flex: 0 0 auto;
  position: relative;
  z-index: 2;   /* the rail sits above the felt */
  border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0));
  box-shadow: 0 8px 18px -10px rgba(0, 0, 0, 0.75);
}
.rail {
  display: flex;
  gap: 8px;
  padding: 8px 10px;
  overflow-x: auto;
  scrollbar-width: none;
  /* Kept `auto` on purpose: the rail is rebuilt every repaint and its offset
     restored instantly, so a CSS-wide smooth behaviour would animate that
     restore too. The one glide we DO want is asked for explicitly in JS. */
  scroll-behavior: auto;
}
.rail::-webkit-scrollbar { display: none; }
/* EIGHT PLAYERS ON A PHONE. At 390px the strip holds about three cards, so a
   full table leaves five seats past the edge with nothing to say so. The edge
   with more to show softens instead of ending on a hard cut. */
.rail--more-left {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 28px);
  mask-image: linear-gradient(90deg, transparent 0, #000 28px);
}
.rail--more-right {
  -webkit-mask-image: linear-gradient(270deg, transparent 0, #000 28px);
  mask-image: linear-gradient(270deg, transparent 0, #000 28px);
}
.rail--more-left.rail--more-right {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
}
.chip {
  flex: 0 0 auto;
  min-width: 92px;
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 7px 10px;
  background: rgba(255, 255, 255, 0.03);
  display: flex;
  flex-direction: column;
  gap: 1px;
  position: relative;
}
.chip-name {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 700;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chip-name i { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 8px; }
/* the name now shares its row with the place badge, so it is the part that
   gives: it shrinks and ellipses rather than pushing the badge off the chip. */
.chip-who { min-width: 0; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip-score { font-size: 19px; font-weight: 900; color: var(--bone); }
.chip-flag { font-size: 9.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-faint); }
.chip.is-active {
  border-color: rgba(229, 163, 60, 0.75);
  background: rgba(229, 163, 60, 0.1);
  box-shadow: 0 0 0 1px rgba(229, 163, 60, 0.35);
}
.chip.is-active .chip-name { color: var(--amber); }
.chip.is-off .chip-score { color: var(--text-faint); }
.chip.is-out { opacity: 0.45; }
/* Jump-off Point: the marker rides on the crosser's card for the rest of the game.
   Tiny, one line, and clipped rather than allowed to widen the chip — eight
   players on a 390px phone still scroll cleanly. */
.chip.is-jumpoff { border-color: rgba(255, 92, 69, 0.6); }

/* THE PLACE BADGE. Sits on the name row, after the name, and is deliberately
   quiet: the score under it is the headline, the place is the context. Fixed
   min-width so 1ST and 8TH leave the name ending in the same spot down the
   rail. */
.chip-rank {
  margin-left: auto;
  flex: 0 0 auto;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.1em;
  color: var(--text-faint);
  background: rgba(255, 255, 255, 0.06);
  border-radius: 5px;
  padding: 1px 4px;
  min-width: 22px;
  text-align: center;
}
.chip.is-active .chip-rank { color: var(--amber); background: rgba(229, 163, 60, 0.18); }
.chip-rank.is-first { color: var(--bone); background: rgba(229, 163, 60, 0.32); }

/* the standings sheet */
.standings-list { list-style: none; margin: 4px 0 10px; padding: 0; display: grid; gap: 5px; }
.standings-row {
  /* min-width:0 is load-bearing: a grid item will not shrink below its content's
     min-content width by default, so at 320px the row pushed past the card and
     clipped the totals rather than ellipsing the name. */
  display: flex; align-items: center; gap: 8px; min-width: 0;
  padding: 8px 9px; border-radius: 9px;
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid transparent;
}
.standings-row.is-you { border-color: rgba(229, 163, 60, 0.55); background: rgba(229, 163, 60, 0.12); }
.standings-row.is-jumpoff { box-shadow: inset 2px 0 0 var(--alarm); }
.standings-place {
  flex: 0 0 27px; font-size: 10.5px; font-weight: 900; letter-spacing: 0.04em;
  color: var(--text-faint);
}
.standings-row.is-lead .standings-place { color: var(--amber); }
.standings-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 8px; }
.standings-name {
  flex: 1 1 auto; min-width: 0; font-weight: 700; font-size: 14px; color: var(--bone);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.standings-tag {
  flex: 0 0 auto; font-size: 8.5px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--alarm); border: 1px solid rgba(255, 92, 69, 0.5);
  border-radius: 4px; padding: 1px 4px;
}
.standings-total { flex: 0 0 auto; font-size: 16px; font-weight: 900; color: var(--bone); }
.standings-row.is-off .standings-total { color: var(--text-faint); }
.standings-icon { width: 18px; height: 18px; fill: currentColor; }

/* the vibration check readout */
/* The felt/nothing answer and the pasteable report. Both sit inside the
   vibration sheet and are hidden until they have something to say. */
.vibe-felt {
  display: grid; gap: 7px; margin: 4px 0 2px;
  padding: 9px 10px; border-radius: 10px;
  background: rgba(255, 255, 255, 0.035);
  border: 1px solid rgba(255, 255, 255, 0.07);
}
.vibe-felt__ask {
  font-size: 12px; font-weight: 700; letter-spacing: 0.02em;
  color: var(--text-dim);
}
.vibe-felt__row { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; min-width: 0; }
.vibe-felt__row .btn { min-width: 0; }
.vibe-out {
  width: 100%; box-sizing: border-box; min-width: 0;
  margin: 2px 0 6px; padding: 9px 10px;
  border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(0, 0, 0, 0.35); color: var(--bone);
  font: 400 11.5px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
  /* pre-wrap, not pre: the user agent string is one very long line and a
     sideways-scrolling textarea is unreadable and unselectable on a phone. */
  resize: none; white-space: pre-wrap; word-break: break-word; overflow-y: auto;
}
/* This sheet is the tallest in the app -- nine facts, three tests, the felt
   answer, the report and two buttons -- so it scrolls rather than growing past
   the top of the screen and hiding its own heading. */
#vibecheck-sheet .sheet-card {
  max-height: 88vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.vibe-facts { margin: 2px 0 10px; display: grid; grid-template-columns: 1fr auto; gap: 4px 10px; font-size: 12px; }
.vibe-facts dt { color: var(--text-faint); }
.vibe-facts dd { margin: 0; color: var(--bone); font-weight: 700; text-align: right; }
.vibe-facts dd.is-bad { color: var(--alarm); }
.vibe-facts dd.is-good { color: var(--mint, #7fd6a2); }

/* THE LEADER CALL — the whole screen, briefly. Pointer-events stay off so a
   dismissing overlay can never eat a tap meant for the dock underneath. */
.leader-fx {
  position: fixed; inset: 0; z-index: 62;
  display: grid; place-items: center;
  background: radial-gradient(circle at 50% 42%, rgba(224, 83, 61, 0.28), rgba(8, 9, 12, 0.88) 62%);
  pointer-events: none;
  animation: leader-bg-in 0.24s ease both;
}
.leader-fx.is-out { animation: leader-bg-out 0.3s ease both; }
.leader-fx__inner {
  display: grid; justify-items: center; gap: 6px;
  padding: 0 22px; text-align: center; max-width: 100%;
  animation: leader-pop 0.42s cubic-bezier(0.2, 1.5, 0.4, 1) both;
}
.leader-klaxon { width: 46px; height: 35px; margin-bottom: 2px; }
.leader-fx__name {
  font-size: clamp(30px, 11vw, 52px); font-weight: 900; line-height: 1.02;
  color: var(--bone); letter-spacing: -0.01em;
  max-width: 100%; overflow-wrap: break-word; hyphens: none;
}
.leader-fx__line {
  font-size: clamp(13px, 4.4vw, 19px); font-weight: 800;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--alarm);
}
.leader-fx__score { font-size: clamp(19px, 6vw, 27px); font-weight: 900; color: var(--amber); }
@keyframes leader-bg-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes leader-bg-out { to { opacity: 0; } }
@keyframes leader-pop {
  from { opacity: 0; transform: scale(0.86) translateY(10px); }
  to { opacity: 1; transform: none; }
}
.chip-jumpoff {
  margin-top: 3px;
  align-self: flex-start;
  max-width: 100%;
  font-size: 8.6px;
  font-weight: 900;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: clip;
  color: var(--stamp-ink, #fff);
  background: var(--alarm);
  border-radius: 999px;
  padding: 2px 6px 1px;
}

/* EARNED NICKNAMES: the titles the table awards as the game goes on.
   One badge per line, never truncated and never wrapped mid-name — the chip is
   allowed to grow to the widest title it holds ("Mother Clucker",
   "Squelchologist") and the rail scrolls, exactly as it already does for eight
   players. Nothing here can push into the score, the flag line or the JUMP-OFF
   marker: it is its own block at the bottom of the card. */
.chip-titles {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  margin-top: 4px;
}
.chip-title {
  max-width: none;
  font-size: 8.6px;
  line-height: 1.25;
  font-weight: 900;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--amber);
  background: rgba(229, 163, 60, 0.16);
  border: 1px solid rgba(229, 163, 60, 0.45);
  border-radius: 999px;
  padding: 2px 6px 1px;
}
.chip.is-active .chip-title { color: var(--bone); background: rgba(229, 163, 60, 0.3); border-color: rgba(229, 163, 60, 0.7); }

.game-main { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.game-board {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 6px;
  padding: 6px 12px 0;
}

.tally { display: flex; align-items: center; justify-content: center; gap: 14px; padding: 2px 0 0; position: relative; z-index: 2; }
.tally-sep { width: 1px; height: 34px; background: var(--line-strong); }
.tally-turn, .tally-bank, .tally-need { display: flex; flex-direction: column; align-items: center; }
.tally span { font-size: 10px; letter-spacing: 0.18em; color: var(--text-dim); }
.tally-turn b {
  font-size: 44px;
  line-height: 1;
  font-weight: 900;
  color: var(--amber);
  opacity: 1;
  /* a dark rim under the numeral: on dark felt it adds punch, on the light
     themes it keeps the amber from dissolving into the surface */
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.55), 0 2px 6px rgba(0, 0, 0, 0.35);
}
.tally-bank b {
  font-size: 30px;
  line-height: 1.15;
  font-weight: 800;
  color: var(--bone);
  opacity: 1;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
}
/* The final-round countdown, third cell of the pill. Sized like BANKED, coloured
   like an alarm while the player is still chasing the number, and switched to the
   winning green the moment the countdown reaches zero and becomes a lead. */
.tally-need b {
  font-size: 30px;
  line-height: 1.15;
  font-weight: 900;
  /* --alarm, not --peril-edge: this number sits on dark felt and the deeper red
     loses too much contrast there. Bright, and never mistaken for a score. */
  color: var(--alarm);
  opacity: 1;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
  /* tabular figures so a counting-down number does not jitter its own width */
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}
.tally-need span { color: var(--alarm); font-weight: 800; }
.tally-need.is-ahead b, .tally-need.is-ahead span { color: var(--mint); }

/* ── the three-cell squeeze ──
   Only in the final round, when the countdown joins TURN and BANKED. Measured
   worst case (TURN 12,000 · BANKED 9,000 · AHEAD +10,550) came to 462px, which
   overhangs a 390px phone by 36px each side. The figures scale with the viewport
   so the widest sum still lands inside the screen on a 320px handset, and the
   max-width is a hard backstop so the pill can never leave the glass. */
.tally.has-need { gap: 10px; max-width: calc(100vw - 20px); }
.tally.has-need .tally-sep { height: 30px; }
.tally.has-need .tally-turn b { font-size: clamp(27px, 8.4vw, 44px); }
.tally.has-need .tally-bank b,
.tally.has-need .tally-need b { font-size: clamp(19px, 5.9vw, 30px); }
.tally-turn b.bump { animation: bump 0.32s cubic-bezier(0.3, 1.5, 0.5, 1); }
@keyframes bump { 40% { transform: scale(1.22); } }

/* ───────── the table has depth: shared camera over the play surface ─────────
   One perspective on the dice zone, preserved down through .dice to each .die,
   so every cube is seen by the same camera and off-centre dice show their sides.
   Only transform/opacity ever animate in here. */
.dice-zone {
  flex: 1;
  min-height: 0;
  display: grid;
  place-items: center;
  position: relative;
  perspective: var(--persp, 1500px);
  perspective-origin: 50% 40%;
  transform-style: preserve-3d;
}
/* the felt receding away from the camera — the theme's own surface, tilted */
.dice-zone::before {
  content: '';
  position: absolute;
  left: -20%;
  right: -20%;
  top: -4%;
  bottom: -16%;
  transform-origin: 50% 100%;
  transform: rotateX(48deg) translateZ(-140px) scale(1.2);
  /* light-led: the plane adds a highlight and only the faintest floor shadow, so
     a theme's felt keeps its own colour and brightness instead of being
     multiplied down into mud */
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.02) 42%, rgba(0, 0, 0, 0.06) 84%, rgba(0, 0, 0, 0.12)),
    var(--surface);
  background-color: var(--felt-800);
  border-radius: 44% 44% 20% 20% / 26% 26% 9% 9%;
  opacity: 0.5;
  pointer-events: none;
  /* feathered, so the horizon never reads as a hard arc drawn on the felt */
  -webkit-mask-image: radial-gradient(78% 86% at 50% 52%, #000 32%, rgba(0, 0, 0, 0.35) 68%, transparent 100%);
  mask-image: radial-gradient(78% 86% at 50% 52%, #000 32%, rgba(0, 0, 0, 0.35) 68%, transparent 100%);
}
/* directional key light from above plus a soft vignette, matching the dice */
.dice-zone::after {
  content: '';
  position: absolute;
  inset: -8% -12%;
  transform: translateZ(-42px);
  /* a whisper of a vignette — enough to seat the dice in space, nowhere near
     enough to darken the theme */
  background:
    radial-gradient(54% 40% at 50% 10%, rgba(255, 255, 255, 0.09), transparent 76%),
    radial-gradient(128% 94% at 50% 44%, transparent 52%, rgba(0, 0, 0, 0.14) 100%);
  pointer-events: none;
}
.dice {
  display: grid;
  grid-template-columns: repeat(3, auto);
  /* roomier than the dice need, so per-die jitter never lets two dice touch */
  gap: 22px;
  justify-content: center;
  transform-style: preserve-3d;
}
.dice-empty { position: absolute; font-size: 13px; color: var(--text-faint); pointer-events: none; }

/* ───────── a die is a real cube ─────────
   Six faces on the surface of a preserve-3d cube, opposites summing to 7.
   The button itself only ever translates, so the hit area stays put and stays
   aligned with the face you can see; the cube inside does the rotating. */
.die {
  --d: clamp(52px, 19vw, 76px);
  --half: calc(var(--d) / 2);
  --lift: 0px;
  width: var(--d);
  height: var(--d);
  padding: 0;
  border: none;
  background: none;
  position: relative;
  cursor: pointer;
  transform-style: preserve-3d;
  /* per-die rest jitter, set from JS on every throw; fallbacks keep tray and
     preview dice square */
  transform: translate3d(var(--jx, 0px), var(--jy, 0px), var(--lift));
  transition: transform 0.18s cubic-bezier(0.3, 1.4, 0.5, 1), opacity 0.2s ease;
}
.die-cube {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  /* rotateZ first = the die lies on the table turned a little; the rest of the
     rotation is solved from the die's value (js/dice3d.js) so the rolled number
     always ends up face-on to the camera. */
  transform: rotateZ(var(--jr, 0deg))
    rotateX(calc(var(--restX, 0deg) + var(--tiltX, 0deg)))
    rotateY(calc(var(--restY, 0deg) + var(--tiltY, 0deg)));
  /* No background on the cube itself: the cube element is a flat plane through
     the middle of the die, and at the resting rotations for 2/3/4/5 it turns
     edge-on to the camera and paints a bright band across the number. The faces
     alone form the solid. */
}
.die-face {
  position: absolute;
  /* exactly on the cube's surface — oversizing the faces makes neighbours
     intersect and z-fight, which shows as bright triangles across the number */
  inset: 0;
  /* tight radius: adjacent faces of a CSS cube meet along their edges, so a large
     radius opens notches at the cube's corners that read as torn geometry */
  border-radius: calc(var(--d) * 0.055);
  padding: calc(var(--d) * 0.13);
  display: grid;
  grid-template: repeat(3, 1fr) / repeat(3, 1fr);
  /* tight range: die-1 dominates, die-3 only kisses the very bottom edge, so the
     set reads as one bone-white material instead of a spread of greys */
  background: linear-gradient(172deg, var(--die-1) 0%, var(--die-1) 46%, var(--die-2) 84%, var(--die-3) 100%);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.5),
    inset 0 -1.5px 2px rgba(0, 0, 0, 0.13);
  backface-visibility: hidden;
}
/* the six faces: 1 front, 6 back, 2 right, 5 left, 3 top, 4 bottom */
.die-face--1 { transform: translateZ(var(--half)); }
.die-face--6 { transform: rotateY(180deg) translateZ(var(--half)); }
.die-face--2 { transform: rotateY(90deg) translateZ(var(--half)); }
.die-face--5 { transform: rotateY(-90deg) translateZ(var(--half)); }
.die-face--3 { transform: rotateX(90deg) translateZ(var(--half)); }
.die-face--4 { transform: rotateX(-90deg) translateZ(var(--half)); }
/* One light source for every die, and it lives on the faces themselves.
   The resting rotation is the inverse of the face placement, so whichever value
   landed, its face arrives at the camera with identity orientation — its local
   top IS world top. That means the value's face can be lit from directly above
   and be exactly right for all six values, while the five faces behind it sit in
   shadow. Because the die knows its value (`data-face`), this is static CSS: no
   JS per frame, and the pale hairline of a neighbouring face at the cube's edge
   reads as a shaded edge instead of a torn-paper flange.
   (An earlier version used a camera-facing highlight plate. It did not rotate
   with the cube, so it stuck out past the tilted silhouette. Deleted.) */
.die-face::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  /* the faces you are not looking at: in shadow */
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.42));
}
/* the face the camera sees: lit from above, and always the brightest surface */
.die[data-face='1'] .die-face--1::before,
.die[data-face='2'] .die-face--2::before,
.die[data-face='3'] .die-face--3::before,
.die[data-face='4'] .die-face--4::before,
.die[data-face='5'] .die-face--5::before,
.die[data-face='6'] .die-face--6::before {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.04) 40%, rgba(0, 0, 0, 0.02) 74%, rgba(0, 0, 0, 0.07));
}
/* pips sunk into the face rather than printed on it */
.die .pip {
  width: 76%;
  height: 76%;
  border-radius: 50%;
  background: var(--pip);
  place-self: center;
  position: relative;
  /* hard-edged and high contrast — the number is the whole game. The depth cue
     is one tight 1px inner highlight and a 1px rim of face colour below, not a
     blur that eats the pip's edge. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.14),
    0 1px 0 rgba(255, 255, 255, 0.55);
}
/* contact shadow on the felt, offset and squashed to match how the die landed */
.die-cast {
  position: absolute;
  left: 5%;
  right: 5%;
  bottom: calc(var(--d) * -0.13);
  height: 42%;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(50% 56% at 50% 46%, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0.34) 48%, transparent 76%);
  transform: translateZ(-3px)
    translate(calc(var(--jx, 0px) * 0.4), calc(var(--jy, 0px) * 0.18))
    rotate(calc(var(--jr, 0deg) * 0.55))
    scale(var(--castX, 1), var(--castY, 1));
  transition: transform 0.18s ease, opacity 0.18s ease;
}
/* non-scoring dice recede and desaturate instead of being filtered */
/* Non-scoring dice recede and go translucent so the felt shows through — the
   same read as the 2D version, and it avoids `filter`, which would flatten the
   3D context. Opacity goes on the faces, never on .die. */
.die.is-dead { --lift: -14px; cursor: default; }
.die.is-dead .die-face { opacity: 0.55; }
.die.is-dead .die-face::before { opacity: 0.5; }
.die.is-dead .pip { box-shadow: none; }
.die.is-dead .die-cast { opacity: 0.3; }
/* set-aside dice lift toward the camera, gilded, with a bigger softer shadow */
.die.is-sel { --lift: 30px; }
.die.is-sel .die-cube {
  transform: rotateZ(calc(var(--jr, 0deg) * 0.4))
    rotateX(calc(var(--restX, 0deg) + var(--tiltX, 0deg) * 0.4))
    rotateY(calc(var(--restY, 0deg) + var(--tiltY, 0deg) * 0.4));
}
.die.is-sel .die-face {
  box-shadow:
    inset 0 0 0 2.5px var(--amber),
    inset 0 0 0 3.5px rgba(0, 0, 0, 0.12),
    inset 0 -1.5px 2px rgba(0, 0, 0, 0.13);
}
.die.is-sel .die-cast {
  opacity: 0.75;
  transform: translateZ(-3px) translate(calc(var(--jx, 0px) * 0.4), 6px) scale(1.3, 0.8);
}
.die.is-locked { cursor: default; }
/* ── the set-aside beat: this die has gone to the scoring tray ──
   It leaves the TABLE, and the dice nobody picked keep their exact places: the
   node stays in the grid (and, on the 3D table, in the overlay's index order),
   so nothing shifts sideways while the beat plays. `visibility` rather than
   `opacity`, which would flatten the preserve-3d cube. */
.die.is-aside {
  visibility: hidden;
  pointer-events: none;
  cursor: default;
}
/* the faces just added to the tray arrive rather than blink into existence */
.held-tray .die.is-fresh { animation: setaside-land 220ms cubic-bezier(0.25, 1.3, 0.5, 1) both; }
@keyframes setaside-land {
  from { transform: translate3d(0, -14px, 0) scale(1.14); opacity: 0; }
  to { transform: none; opacity: 1; }
}
/* Every throw is its own: entry vector, spin axis mix, turn count, direction,
   duration, stagger and rest jitter all come from JS per die (throwFx), but the
   final rotation is solved from the value so it lands showing the right face. */
.die.rolling {
  animation: throw-in var(--dur, 0.7s) cubic-bezier(0.2, 0.86, 0.28, 1) var(--delay, 0s) both;
  will-change: transform;
}
.die.rolling .die-cube {
  animation: tumble3d var(--dur, 0.7s) cubic-bezier(0.24, 0.8, 0.3, 1) var(--delay, 0s) both;
  will-change: transform;
}
@keyframes throw-in {
  0% {
    transform: translate3d(var(--tx0, 0px), var(--ty0, -80px), var(--tz0, 200px)) scale(var(--sc0, 0.8));
    opacity: 0.72;
  }
  56% {
    transform: translate3d(calc(var(--jx, 0px) * 1.3), calc(var(--jy, 0px) + var(--bounce, 8px)), 4px) scale(1.02);
    opacity: 1;
  }
  76% {
    transform: translate3d(calc(var(--jx, 0px) * 0.9), calc(var(--jy, 0px) - var(--bounce, 8px) * 0.5), 16px) scale(0.995);
  }
  90% {
    transform: translate3d(var(--jx, 0px), calc(var(--jy, 0px) + 1.5px), 3px) scale(1.004);
  }
  100% { transform: translate3d(var(--jx, 0px), var(--jy, 0px), var(--lift)) scale(1); }
}
@keyframes tumble3d {
  0% {
    transform: rotateZ(calc(var(--jr, 0deg) + var(--sz, -360deg)))
      rotateX(calc(var(--restX, 0deg) + var(--tiltX, 0deg) + var(--sx, -720deg)))
      rotateY(calc(var(--restY, 0deg) + var(--tiltY, 0deg) + var(--sy, 1080deg)));
  }
  56% {
    transform: rotateZ(calc(var(--jr, 0deg) + var(--sz, -360deg) * 0.34))
      rotateX(calc(var(--restX, 0deg) + var(--tiltX, 0deg) + var(--sx, -720deg) * 0.3 + var(--wob, 12deg)))
      rotateY(calc(var(--restY, 0deg) + var(--tiltY, 0deg) + var(--sy, 1080deg) * 0.28));
  }
  80% {
    transform: rotateZ(calc(var(--jr, 0deg) + var(--sz, -360deg) * 0.06))
      rotateX(calc(var(--restX, 0deg) + var(--tiltX, 0deg) + var(--sx, -720deg) * 0.05 - var(--wob, 12deg) * 0.6))
      rotateY(calc(var(--restY, 0deg) + var(--tiltY, 0deg) + var(--sy, 1080deg) * 0.04));
  }
  93% {
    transform: rotateZ(var(--jr, 0deg))
      rotateX(calc(var(--restX, 0deg) + var(--tiltX, 0deg) + var(--wob, 12deg) * 0.14))
      rotateY(calc(var(--restY, 0deg) + var(--tiltY, 0deg)));
  }
  100% {
    transform: rotateZ(var(--jr, 0deg))
      rotateX(calc(var(--restX, 0deg) + var(--tiltX, 0deg)))
      rotateY(calc(var(--restY, 0deg) + var(--tiltY, 0deg)));
  }
}
/* Mid-tumble every face is on show and none of them is "the" face, so they all
   get the same gentle top-light and the cube's form comes from its edges. */
.die.rolling .die-face::before,
.die.rolling .die-face--1::before,
.die.rolling .die-face--2::before,
.die.rolling .die-face--3::before,
.die.rolling .die-face--4::before,
.die.rolling .die-face--5::before,
.die.rolling .die-face--6::before {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.22), rgba(0, 0, 0, 0.06) 60%, rgba(0, 0, 0, 0.16));
}

.die.nope { animation: nope 0.3s ease; }
@keyframes nope {
  25% { transform: translate3d(calc(var(--jx, 0px) - 6px), var(--jy, 0px), var(--lift)); }
  75% { transform: translate3d(calc(var(--jx, 0px) + 6px), var(--jy, 0px), var(--lift)); }
}

.held { display: flex; align-items: center; gap: 10px; min-height: 46px; padding: 2px 0 6px; }
.held-label { font-size: 9.5px; letter-spacing: 0.18em; color: var(--text-faint); writing-mode: horizontal-tb; }
.held-tray {
  flex: 1;
  display: flex;
  gap: 6px;
  align-items: center;
  min-height: 44px;
  padding: 5px 8px;
  border-radius: 12px;
  border: 1px dashed var(--line-strong);
  background: rgba(0, 0, 0, 0.22);
  overflow-x: auto;
  scrollbar-width: none;
}
.held-tray::-webkit-scrollbar { display: none; }
.held-tray .die { --d: 30px; cursor: default; }
.held-tray .die-cast { height: 26%; bottom: -2px; opacity: 0.7; }
.held-tray .die .pip { background: var(--pip); }
.held-empty { font-size: 12px; color: var(--text-faint); }

/* ── SET-ASIDE GROUPS ──────────────────────────────────────────────────────
   The tray shows WHAT WAS PULLED AT ONCE, not a loose row of dice. Anything
   taken as a set — three of a kind, or the six-die straight — is ringed and
   labelled, so a player scanning their own tray can see that three 5s went in
   together rather than as three separate picks. A solo 1 or 5 is one die and
   gets no ring: a box drawn round a single die would claim a set that is not
   there. The ring is a hairline in the accent, not a filled panel, so a tray
   with several sets in it does not turn into a row of competing boxes. */
.held-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 0 0 auto;
}
.held-group-dice {
  display: flex;
  gap: 4px;
}
.held-group--set {
  padding: 3px 5px 4px;
  border: 1px solid var(--ember);
  border-radius: 9px;
  /* the faintest wash so the ring reads as one object, not four edges */
  background: color-mix(in srgb, var(--ember) 9%, transparent);
}
.held-group--set .held-group-dice { gap: 3px; }
.held-group-tag {
  font-size: 9px;
  line-height: 1;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ember);
  white-space: nowrap;
  /* never widen the tray: a long label ("Straight 1-6") is allowed to sit
     narrower than its dice rather than stretch the ring */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* A pick that has not been committed yet can be sent back, as the unit it
   went in as. Only ever set on the fresh group, so a committed set is inert. */
.held-group.is-undoable { cursor: pointer; }
.held-group.is-undoable .die { cursor: pointer; }
@media (hover: hover) {
  .held-group.is-undoable:hover { opacity: 0.82; }
}
.held-group.is-undoable:focus-visible { outline: 2px solid var(--ember); outline-offset: 2px; }

/* ─────────────────────────────────────────────────────────────
   THE TURN LOG IS A STRIP AT THE FLOOR OF THE SCREEN.
   It used to be a panel between the board and the dock that expanded INLINE,
   so opening it squeezed the table and, on desktop, it held a 320px column of
   its own. Now it is a ~28px strip below the dock — the last thing on screen —
   minimized always, and expanding lifts the list OVER the table as an overlay
   so the board keeps every pixel it was given.
   ───────────────────────────────────────────────────────────── */
.log-pane {
  flex: 0 0 auto;
  position: relative;
  z-index: 6;               /* under the chat drawer (20) and the sheets */
  border-top: 1px solid var(--line);
  background: rgba(0, 0, 0, 0.42);
  padding-bottom: var(--safe-b);   /* clears the home indicator */
}
/* the WHOLE strip is the button: a 12px glyph is not a tap target, and the
   narrow breakpoint used to hide the glyph outright */
.log-strip {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  height: var(--logbar-h);
  padding: 0 12px;
  border: none;
  background: none;
  color: var(--text-faint);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-align: left;
  cursor: pointer;
}
.log-strip-label { flex: 0 0 auto; }
/* the one most recent line, on one line, in its own colour */
.log-latest {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--text-dim);
}
.log-caret { flex: 0 0 auto; font-size: 11px; letter-spacing: 0; }
/* Expanded, the strip carries only its label and the ▼: the newest line is
   already the first row of the open list, and showing it in both places at once
   reads like a bug. */
.log-pane.is-open .log-latest { display: none; }
/* Expanded: an overlay anchored to the strip, never a reflow. Full width, edge
   to edge with the strip, square at the bottom so it reads as one attached
   panel in the bottom region rather than a pill floating over the dice, and it
   grows UPWARD over the table. --log-lift (set on open) is the distance from the
   strip up to the bottom cluster's ceiling — the top of the SET ASIDE tray — so
   the tray, the readout and ROLL / “I'll take” / PASS DICE are never covered. */
.log {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(100% + var(--log-lift, 0px));
  display: none;
  list-style: none;
  margin: 0;
  padding: 6px 14px 10px;
  /* ~46vh ceiling, and never taller than the room above the bottom cluster:
     --log-max (measured on open) keeps the TURN / BANKED tally fully readable
     instead of slicing it in half. */
  max-height: min(46vh, var(--log-max, 46vh));
  overflow-y: auto;
  overscroll-behavior: contain;
  /* a row cut in half by the cap should read as “there is more”, not as clipped
     text, so the last 10px fade out — which is the bottom padding once the list
     is scrolled all the way down */
  -webkit-mask-image: linear-gradient(to top, transparent 0, #000 10px);
  mask-image: linear-gradient(to top, transparent 0, #000 10px);
  /* fully opaque: a bright element behind it (the TURN / BANKED tally) ghosted
     through even at 0.95 and read as a rendering fault */
  background: rgb(11, 14, 13);
  border-top: 1px solid var(--line-strong);
  border-bottom: 1px solid var(--line-strong);
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -12px 34px rgba(0, 0, 0, 0.55);
}
.log-pane.is-open .log { display: block; }
.log .log-empty {
  font-size: 12px;
  color: var(--text-faint);
  border-bottom: none;
  padding: 2px 0;
}
.log li {
  font-size: 12.5px;
  color: var(--text-dim);
  padding: 3px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.045);
  display: flex;
  gap: 7px;
}
.log li::before { content: '·'; color: var(--text-faint); }
.log li.k-bank, .log-latest.k-bank { color: var(--mint); }
.log li.k-squelch, .log-latest.k-squelch { color: var(--alarm); font-weight: 700; }
.log li.k-chicken, .log-latest.k-chicken { color: var(--chicken-beak); font-weight: 700; }
.log li.k-reject, .log-latest.k-reject { color: var(--amber); }
.log li.k-final, .log li.k-win,
.log-latest.k-final, .log-latest.k-win { color: var(--amber); font-weight: 700; }
/* a newly earned nickname: announced once, in the same amber as the other
   table-wide moments, so it reads as an event and not as a roll */
.log li.k-title, .log-latest.k-title { color: var(--amber); font-weight: 700; }
/* The Twos drama had its moment on screen but read as plain grey once it reached
   the log, the only place the drama is still legible after the cloud drifts off.
   NOT --peril-edge: that deep red measures about 4.1:1 on this dark strip, under
   the 4.5:1 floor for text this size. --peril-ink is the taunt's own lettering,
   which is nearly white and would read as an ordinary line, so the log gets its
   own tint -- bright enough to pass comfortably, and far enough from --alarm that
   a taunt is never mistaken for a squelch. Only these two: `corner` and `jumpoff`
   are deliberately left at the default rather than handed a colour already spoken
   for by banking and squelching. */
.log li.k-peril, .log-latest.k-peril { color: var(--peril-log); font-weight: 700; }
.log li.k-grace, .log-latest.k-grace { color: var(--grace-rim); font-weight: 700; }
.log-empty { color: var(--text-faint); font-size: 12px; }

/* dock */
.dock--game {
  flex-direction: column;
  gap: 8px;
  /* the log strip is the floor now, so the safe-area inset lives on the strip
     and not here — the dock's own bottom padding stays a flat 10px */
  padding: 8px 12px 10px;
  /* real elevation over the table surface */
  box-shadow: 0 -14px 28px -14px rgba(0, 0, 0, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.07);
}
.readout {
  margin: 0;
  text-align: center;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text-dim);
  min-height: 20px;
}
.readout b { color: var(--amber); font-weight: 900; }
.readout.warn { color: var(--ember-hot); }
/* “Take All Scoring” — the helper row above the two big actions. Deliberately
   quieter than ROLL and “I'll take”: an outlined amber row, shorter than them
   but still a 48px tap target, so it never competes with the two decisions that
   matter. Disabled it loses the amber entirely and goes dashed and faint, which
   reads as off at a glance rather than as merely dim. */
.dock-assist { display: flex; }
.btn--assist {
  flex: 1;
  min-height: 48px;
  padding: 6px 12px;
  font-size: 14px;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  background: color-mix(in srgb, var(--amber) 12%, transparent);
  border-color: color-mix(in srgb, var(--amber) 55%, transparent);
  color: var(--amber);
}
.btn--assist:not(:disabled):active { transform: translateY(1px); }
.btn--assist:disabled {
  background: none;
  border-style: dashed;
  border-color: var(--line);
  color: var(--text-faint);
  box-shadow: none;
  opacity: 1;
}
.dock-btns { display: flex; gap: 10px; }
.dock-btns .btn { flex: 1; min-height: 62px; font-size: 20px; font-weight: 900; letter-spacing: 0.06em; }
.btn--bank {
  background: linear-gradient(180deg, rgba(111, 207, 155, 0.26), rgba(111, 207, 155, 0.15));
  border-color: rgba(111, 207, 155, 0.7);
  color: var(--mint);
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
}
/* “I'll take 1,250” — the words you actually say, with the live turn score in
   the same breath. One label, never two figures. It must hold on the narrowest
   phone with a five-digit score (“I'll take 10,450”): the button is half the dock,
   so the type is sized in vw with a floor that stays legible, the whole label is
   nowrap, and the figure carries tabular figures so it cannot jitter a pixel as
   dice are set aside. */
.dock-btns .btn--bank {
  /* one row, not the stacked column the base .btn uses: the words and the
     figure are one spoken phrase and must never be split */
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.3em;
  white-space: nowrap;
  padding-left: 6px;
  padding-right: 6px;
  letter-spacing: 0.01em;
  font-size: clamp(15px, 4.55vw, 20px);
}
.bank-take { font-weight: 800; }
.bank-figure {
  font-weight: 900;
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: 'tnum' 1, 'lnum' 1;
  letter-spacing: 0;
}
/* nothing set aside yet: the copy says so, and the button reads plainly off */
.dock-btns .btn--bank.btn--bank-zero { font-size: clamp(12.5px, 3.4vw, 15px); letter-spacing: 0.04em; }
.dock-btns .btn--bank:disabled {
  background: rgba(255, 255, 255, 0.04);
  border-color: var(--line);
  color: var(--text-faint);
  text-shadow: none;
  opacity: 1;
}
/* HELD BACK BY THE OPENING HOUSE RULE, not merely inert. The button carries the
   condition as its label ("Need 1,000 to open"), and this makes it look like a
   rule rather than a button that failed: a dashed edge nothing else in the dock
   uses, and the amber the app already spends on house-rule wording. */
.dock-btns .btn--bank.btn--bank-locked:disabled {
  border-style: dashed;
  border-color: rgba(229, 163, 60, 0.5);
  color: var(--amber);
  background: rgba(229, 163, 60, 0.09);
}
/* the primary action reads unmistakably live: full-saturation fill, dark ink and
   a lifted edge. Disabled buttons lose the lift and the saturation, so the two
   states are never confusable. */
.dock-btns .btn--roll:not(:disabled) { box-shadow: 0 5px 0 -1px var(--cta-shadow), 0 10px 20px -8px rgba(0, 0, 0, 0.7); }
.dock-btns .btn:disabled { box-shadow: none; transform: translateY(2px); }
.btn--roll {
  /* solid fill with a sheen rather than a two-tone gradient: the label sits on
     one predictable tone in every theme, so contrast never dips at a stop */
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0) 46%, rgba(0, 0, 0, 0.09)), var(--ember);
  color: var(--cta-ink);
  border-color: rgba(0, 0, 0, 0.35);
  box-shadow: 0 5px 0 -1px var(--cta-shadow), var(--shadow-lift);
}

/* overlays */
.pass-overlay {
  position: absolute;
  inset: 0;
  z-index: 30;
  background: rgba(6, 10, 8, 0.94);
  display: grid;
  place-items: center;
  padding: 24px;
  animation: fade 0.2s ease;
}
.pass-card { text-align: center; max-width: 320px; width: 100%; }
.pass-token {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  display: block;
  margin: 0 auto 16px;
  box-shadow: inset 0 -4px 8px rgba(0, 0, 0, 0.4);
}
.pass-card p { margin: 0; font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--text-faint); }
.pass-card h3 { margin: 6px 0 14px; font-size: 34px; font-weight: 900; color: var(--bone); }
.pass-note { margin-bottom: 18px !important; text-transform: none !important; letter-spacing: 0 !important; font-size: 13.5px !important; color: var(--text-dim) !important; }
@keyframes fade { from { opacity: 0; } }

/* ───── the squelch takeover ─────
   A squelch costs you the whole turn, so it takes the whole screen: a fixed red
   wash over the header, the scoreboard, the tray and the dock, with the stamp
   dead centre. Brief on purpose — the choreography is still dice settle → 1.5s
   reading pause → takeover → clean release, and the takeover itself is about
   1.3s so it never feels like a sentence to sit through. It swallows taps while
   it is up, and only opacity/transform animate. */
.squelch-fx {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  background:
    radial-gradient(circle at 50% 44%, rgba(236, 96, 66, 0.62), rgba(104, 15, 8, 0.9) 62%, rgba(46, 5, 2, 0.97) 100%);
  animation: squelch-takeover 1.3s cubic-bezier(0.2, 0.8, 0.3, 1) forwards;
  /* nothing underneath is tappable while the screen belongs to the squelch */
  pointer-events: auto;
  overflow: hidden;
}
/* a wheel of light behind the slab: it gives the fill somewhere to come FROM */
.squelch-fx__rays {
  position: absolute;
  width: 180vmax;
  height: 180vmax;
  background: conic-gradient(
    from 0deg,
    rgba(255, 226, 214, 0.13) 0 6deg, rgba(255, 226, 214, 0) 6deg 30deg,
    rgba(255, 226, 214, 0.13) 30deg 36deg, rgba(255, 226, 214, 0) 36deg 60deg,
    rgba(255, 226, 214, 0.13) 60deg 66deg, rgba(255, 226, 214, 0) 66deg 90deg,
    rgba(255, 226, 214, 0.13) 90deg 96deg, rgba(255, 226, 214, 0) 96deg 120deg,
    rgba(255, 226, 214, 0.13) 120deg 126deg, rgba(255, 226, 214, 0) 126deg 150deg,
    rgba(255, 226, 214, 0.13) 150deg 156deg, rgba(255, 226, 214, 0) 156deg 180deg
  );
  mask-image: radial-gradient(circle, #000 0 22%, transparent 62%);
  -webkit-mask-image: radial-gradient(circle, #000 0 22%, transparent 62%);
  animation: squelch-rays 1.3s linear forwards;
}
/* while the squelch owns the screen nothing behind it scrolls or gets tapped */
body.is-squelching { overflow: hidden; }
.squelch-fx__slab {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 14px;
  text-align: center;
  padding: 0 16px;
}
.squelch-fx__sub {
  font-size: clamp(12px, 3.4vw, 16px);
  font-style: normal;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(255, 238, 233, 0.82);
  animation: squelch-sub 0.5s 0.18s ease-out both;
}
@keyframes squelch-takeover {
  0%   { opacity: 0; }
  10%  { opacity: 1; }
  82%  { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes squelch-rays {
  from { transform: rotate(0deg) scale(0.9); }
  to   { transform: rotate(26deg) scale(1.06); }
}
@keyframes squelch-sub {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
/* The stamp is the biggest thing on the screen when the screen is all squelch —
   but it must never touch the edges. `nowrap` plus `max-width` CLIPS rather than
   shrinks, and the -9deg tilt adds roughly h·sin9° to the bounding width, so the
   ramp is set from the widest case (eight glyphs at 900 weight) rather than left
   to spill: at 390px this lands near 50px, well inside the viewport, and it
   still caps at 132px on a desktop. */
.squelch-fx .stamp {
  font-size: clamp(44px, 12.6vw, 132px);
  border-width: 8px;
  padding: 8px 18px 14px;
  max-width: 86vw;
}
/* shared stamp treatment: the rotated slab. SQUELCH! is the default dressing,
   Chicken Feed! re-dresses it below. */
.stamp {
  font-size: clamp(42px, 15vw, 92px);
  font-weight: 900;
  letter-spacing: 0.02em;
  /* a stamp is one line by definition — it must never wrap or spill the slab */
  white-space: nowrap;
  max-width: 94vw;
  color: var(--stamp-ink);
  border: 6px solid var(--stamp-line);
  border-radius: 12px;
  padding: 6px 18px 10px;
  transform: rotate(-9deg);
  animation: stamp 0.45s cubic-bezier(0.2, 1.8, 0.4, 1) both;
  text-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
@keyframes stamp { 0% { transform: rotate(-9deg) scale(2.4); opacity: 0; } 60% { transform: rotate(-9deg) scale(0.94); opacity: 1; } 100% { transform: rotate(-9deg) scale(1); } }

/* ───────── the Chicken ─────────
   Banked small, so you get ribbed for it. Amber wash, amber stamp, tipped the
   other way to the SQUELCH! slab so the two moments never read as the same
   event. Only transform/opacity animate. */
.chicken-fx {
  position: absolute;
  inset: 0;
  z-index: 41;
  display: grid;
  place-items: center;
  background: var(--chicken-bg);
  animation: chicken-wash 260ms ease-out both;
  pointer-events: none;
}
@keyframes chicken-wash { from { opacity: 0; } to { opacity: 1; } }
.chicken-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.chicken {
  width: min(310px, 74vw);
  height: auto;
  overflow: visible;
}

/* ── the 1.6s choreography ──
   Three beats, and every part of the bird is timed against the SAME clock as
   EFFECTS.chicken in js/sfx.js, so the call and the picture land together:

     0–650ms    RUN     in from off-screen left, across the middle of the screen
                        (five hurried clucks)
     650–900ms  PIVOT   plant, hop, mirror — it is now facing back the way it came
                        (the BAGAWK)
     900–1500ms SCRATCH three rakes of the front foot, head pecking down, tail up,
                        dust behind the foot (three low buks)
     1500–1600ms SETTLE everything returns to a standing pose

   Then the "Chicken Feed!" stamp lands at CHICKEN_STRUT_MS (1600ms) and the whole
   overlay leaves at CHICKEN_BEAT_MS (2500ms) — both in js/app.js.

   The looping parts are written as CHAINED animations (a fast run cycle with an
   iteration count, then a delayed scratch animation) rather than one giant
   keyframe list: each animation has fill-mode `none`, so between and after them
   the part falls back to its base standing pose, which is also exactly what a
   cancel mid-beat or `prefers-reduced-motion` needs to land on. */

/* the travel. `calc(-50vw - 55%)` puts the bird's centre half a bird-width past
   the left edge of the screen whatever the screen size, so it always starts
   fully off-stage, and the small positive offsets keep it inside the right edge
   at 390px as well as on desktop. */
.ck-run { animation: ck-cross 1600ms linear both; }
@keyframes ck-cross {
  0%       { transform: translateX(calc(-50vw - 55%)); }
  40.625%  { transform: translateX(14%); }   /* 650ms — across the middle */
  46.5%    { transform: translateX(20%); }   /* the skid past the stop */
  56.25%   { transform: translateX(8%); }    /* 900ms — planted, turned */
  100%     { transform: translateX(8%); }
}

/* the turn itself: squash the bird flat, come back out mirrored, with a hop.
   The mirror is on this WRAPPER, so nothing inside the SVG has to know. */
.ck-turn { transform-origin: 50% 88%; animation: ck-turn 1600ms ease-in-out both; }
@keyframes ck-turn {
  0%, 40.625% { transform: scaleX(1) translateY(0); }
  44%         { transform: scaleX(0.46) translateY(-7%) rotate(-3deg); }
  48.5%       { transform: scaleX(-0.46) translateY(-9%) rotate(2deg); }
  53%         { transform: scaleX(-1) translateY(-2%) rotate(0deg); }
  56.25%, 100% { transform: scaleX(-1) translateY(0); }
}
.ck-shadow { fill: rgba(0, 0, 0, 0.34); }
.ck-body, .ck-skull, .ck-neck {
  fill: var(--chicken-body);
  stroke: var(--chicken-outline);
  stroke-width: 5;
}
.ck-wing > path:first-child {
  fill: var(--chicken-shade);
  stroke: var(--chicken-outline);
  stroke-width: 5;
}
.ck-wing-line { fill: none; stroke: var(--chicken-outline); stroke-width: 3.2; stroke-linecap: round; opacity: 0.5; }
.ck-tail path { fill: var(--chicken-shade); stroke: var(--chicken-outline); stroke-width: 5; }
.ck-comb, .ck-wattle { fill: var(--chicken-comb); stroke: var(--chicken-outline); stroke-width: 4.5; }
.ck-beak { fill: var(--chicken-beak); stroke: var(--chicken-outline); stroke-width: 4; }
.ck-eye { fill: var(--chicken-outline); }
.ck-eye-spark { fill: #fff; }
.ck-legs path { fill: none; stroke: var(--chicken-beak); stroke-width: 6; stroke-linecap: round; }
.ck-squawk path { fill: none; stroke: var(--chicken-ink); stroke-width: 5; stroke-linecap: round; }

/* HEAD — bobbing on the run, whipping round on the turn, pecking at the ground
   while it scratches. Positive rotation about the neck dips the beak, because
   the bird is drawn facing right and SVG y grows downward. */
.ck-head {
  transform-origin: 176px 90px;
  animation:
    ck-bob 162ms ease-in-out 4,
    ck-head-whip 250ms ease-out 650ms 1,
    ck-peck 200ms ease-in-out 900ms 3;
}
@keyframes ck-bob {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  38%      { transform: translateY(-5px) rotate(-6deg); }
  70%      { transform: translateY(2px) rotate(3deg); }
}
@keyframes ck-head-whip {
  0%   { transform: translateY(0) rotate(0deg); }
  40%  { transform: translateY(-7px) rotate(-13deg); }
  100% { transform: translateY(0) rotate(0deg); }
}
@keyframes ck-peck {
  0%   { transform: translateY(4px) rotate(10deg); }
  45%  { transform: translateY(15px) rotate(26deg); }
  100% { transform: translateY(4px) rotate(10deg); }
}

/* WING — flapping hard on the run, one big beat on the pivot, folded and only
   twitching over the scratching. */
.ck-wing {
  transform-origin: 116px 102px;
  animation:
    ck-flap 162ms ease-in-out 4,
    ck-flap-big 250ms ease-out 650ms 1,
    ck-flap-small 200ms ease-in-out 900ms 3;
}
@keyframes ck-flap {
  0%, 100% { transform: rotate(9deg) translateY(1px); }
  50%      { transform: rotate(-31deg) translateY(-6px); }
}
@keyframes ck-flap-big {
  0%   { transform: rotate(0deg) translateY(0); }
  45%  { transform: rotate(-44deg) translateY(-9px); }
  100% { transform: rotate(0deg) translateY(0); }
}
@keyframes ck-flap-small {
  0%, 100% { transform: rotate(0deg); }
  50%      { transform: rotate(-9deg) translateY(-2px); }
}

/* BODY — bouncing on the run, then tipped forward over the scratching and back
   upright to settle. */
.ck-body {
  transform-origin: 122px 130px;
  animation:
    ck-bounce 162ms ease-in-out 4,
    ck-body-tip 600ms ease-in-out 900ms 1;
}
@keyframes ck-bounce {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-4px) rotate(1.8deg); }
}
@keyframes ck-body-tip {
  0%   { transform: rotate(0deg) translateY(0); }
  18%  { transform: rotate(9deg) translateY(3px); }
  50%  { transform: rotate(7deg) translateY(2px); }
  82%  { transform: rotate(9deg) translateY(3px); }
  100% { transform: rotate(0deg) translateY(0); }
}

/* LEGS — a fast run cycle (162ms a stride against the old 440ms strut), then the
   front foot RAKES backward three times while the back leg braces. */
.ck-leg--front {
  transform-origin: 138px 138px;
  animation:
    ck-stride 162ms ease-in-out 4,
    ck-rake 200ms ease-in 900ms 3;
}
.ck-leg--back {
  transform-origin: 108px 138px;
  animation:
    ck-stride 162ms ease-in-out reverse 4,
    ck-brace 600ms ease-in-out 900ms 1;
}
@keyframes ck-stride {
  0%, 100% { transform: rotate(-22deg); }
  50%      { transform: rotate(24deg); }
}
/* forward reach, then a hard scrape back under the bird */
@keyframes ck-rake {
  0%   { transform: rotate(0deg) translateY(0); }
  30%  { transform: rotate(26deg) translateY(-5px); }
  70%  { transform: rotate(-30deg) translateY(0); }
  100% { transform: rotate(0deg) translateY(0); }
}
@keyframes ck-brace {
  0%   { transform: rotate(0deg); }
  20%  { transform: rotate(-13deg); }
  80%  { transform: rotate(-13deg); }
  100% { transform: rotate(0deg); }
}

/* TAIL — flicking on the run, held UP while the bird is tipped over its foot. */
.ck-tail {
  transform-origin: 70px 108px;
  animation:
    ck-flick 216ms ease-in-out 3,
    ck-tail-lift 600ms ease-in-out 900ms 1;
}
@keyframes ck-flick {
  0%, 100% { transform: rotate(-5deg); }
  45%      { transform: rotate(11deg); }
}
@keyframes ck-tail-lift {
  0%   { transform: rotate(0deg); }
  18%  { transform: rotate(17deg); }
  50%  { transform: rotate(13deg); }
  82%  { transform: rotate(17deg); }
  100% { transform: rotate(0deg); }
}

/* DUST — kicked up by each rake. Invisible outside its three cycles because the
   group's base opacity is 0 and the animation does not fill. */
.ck-dust {
  fill: var(--chicken-shade);
  opacity: 0;
  transform-origin: 116px 172px;
  animation: ck-dust 200ms ease-out 900ms 3;
}
@keyframes ck-dust {
  0%   { opacity: 0; transform: scale(0.4) translateX(0); }
  35%  { opacity: 0.55; transform: scale(1) translateX(-6px); }
  100% { opacity: 0; transform: scale(1.5) translateX(-16px); }
}

/* SQUAWK MARKS — one per call in EFFECTS.chicken: five quick ones in stride, a
   big one on the BAGAWK at 660ms, three softer ones over the scratching. */
.ck-squawk {
  transform-origin: 240px 52px;
  opacity: 0;
  animation:
    ck-squawk 130ms ease-out 5,
    ck-squawk-big 300ms ease-out 660ms 1,
    ck-squawk 200ms ease-out 1000ms 3;
}
@keyframes ck-squawk {
  0%   { transform: scale(0.6); opacity: 0; }
  35%  { transform: scale(1.12); opacity: 1; }
  100% { transform: scale(1.3); opacity: 0; }
}
@keyframes ck-squawk-big {
  0%   { transform: scale(0.5); opacity: 0; }
  25%  { transform: scale(1.35); opacity: 1; }
  100% { transform: scale(1.7); opacity: 0; }
}

/* "Chicken Feed!" is thirteen characters where "Chicken!" was eight, so the
   slab is re-fitted rather than left to overflow: a shorter type ramp, tighter
   tracking and tighter side padding. At 390px that lands around 36px — still a
   heavy 900-weight slab across most of the width, so it keeps its impact — and
   at 1280px it caps at 68px. */
.stamp--chicken {
  color: var(--chicken-ink);
  border-color: var(--chicken-line);
  font-size: clamp(30px, 9.4vw, 68px);
  letter-spacing: -0.005em;
  padding: 6px 14px 9px;
  transform: rotate(7deg);
  animation: stamp-chicken 0.42s cubic-bezier(0.2, 1.8, 0.4, 1) both;
}
@keyframes stamp-chicken {
  0%   { transform: rotate(7deg) scale(2.2); opacity: 0; }
  60%  { transform: rotate(7deg) scale(0.95); opacity: 1; }
  100% { transform: rotate(7deg) scale(1); }
}
.shake { animation: shake 0.42s cubic-bezier(0.36, 0.07, 0.19, 0.97) 2; }
@keyframes shake {
  10%, 90% { transform: translate(-2px, 1px); }
  30%, 70% { transform: translate(5px, -2px); }
  50% { transform: translate(-6px, 2px); }
}
.hot-fx {
  position: absolute;
  inset: 0;
  z-index: 35;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.hot-fx span {
  font-size: clamp(26px, 8vw, 52px);
  font-weight: 900;
  letter-spacing: 0.06em;
  max-width: 88vw;
  text-align: center;
  text-wrap: balance;
  line-height: 1.02;
  color: var(--amber);
  text-shadow: 0 0 26px rgba(229, 163, 60, 0.7);
  animation: hot 0.9s ease-out forwards;
}
@keyframes hot { 0% { transform: scale(0.6); opacity: 0; } 30% { transform: scale(1.02); opacity: 1; } 100% { transform: scale(1.14); opacity: 0; } }

.toast {
  position: absolute;
  left: 50%;
  bottom: calc(150px + var(--safe-b));
  transform: translateX(-50%);
  z-index: 45;
  margin: 0;
  background: rgba(12, 18, 14, 0.96);
  border: 1px solid var(--line-strong);
  color: var(--bone);
  font-size: 13.5px;
  font-weight: 700;
  padding: 11px 16px;
  border-radius: 999px;
  max-width: 86%;
  text-align: center;
  animation: toast-in 0.2s ease;
  box-shadow: var(--shadow-lift);
}
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 8px); } }

/* ───────── Jump-off Point ─────────
   A klaxon strip, not a curtain: it sits in the banner slot above the scoreboard
   rail, so it never covers the dice or the dock, and it takes itself away after
   a few seconds without a tap. Alarm colours come from the active theme, so it
   is at home in all 17 tables. */
.jumpoff-fx {
  position: relative;
  z-index: 3;
  padding: 0 12px;
  margin: 6px 0 2px;
  pointer-events: none;
}
.jumpoff-fx__bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 8px 12px;
  border: 2px solid var(--alarm);
  border-radius: var(--r-md, 12px);
  background: linear-gradient(180deg, color-mix(in srgb, var(--alarm) 30%, transparent), color-mix(in srgb, var(--alarm) 14%, transparent));
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35), 0 10px 24px -14px var(--alarm);
  animation: jumpoff-in 0.28s cubic-bezier(0.2, 1.5, 0.4, 1) both, jumpoff-pulse 0.62s ease-in-out 3 0.28s;
}
.jumpoff-fx.is-out .jumpoff-fx__bar { animation: jumpoff-out 0.3s ease both; }
.jumpoff-fx b {
  font-size: clamp(12.5px, 3.6vw, 16px);
  font-weight: 900;
  letter-spacing: 0.03em;
  text-align: center;
  color: var(--stamp-ink, #fff);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
}
.jumpoff-klaxon { width: 26px; height: 20px; flex: 0 0 26px; }
.jumpoff-klaxon__horn { fill: var(--alarm); stroke: var(--stamp-ink, #fff); stroke-width: 1.6; }
.jumpoff-klaxon__wave { fill: none; stroke: var(--stamp-ink, #fff); stroke-width: 2; stroke-linecap: round; }
.jumpoff-klaxon__wave--1 { animation: jumpoff-wave 0.62s ease-in-out infinite; }
.jumpoff-klaxon__wave--2 { animation: jumpoff-wave 0.62s ease-in-out 0.14s infinite; }
@keyframes jumpoff-in { from { opacity: 0; transform: translateY(-10px); } }
@keyframes jumpoff-out { to { opacity: 0; transform: translateY(-8px); } }
@keyframes jumpoff-pulse {
  50% { box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35), 0 0 22px -2px var(--alarm); }
}
@keyframes jumpoff-wave { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }

/* ───────── the Twos ─────────
   Two moments, deliberately opposite in every way a screen can be. The taunt is
   a hard-edged slab at the top of the flow (same slot as the klaxon strip above,
   so like that strip it can never cover the dice or the dock). The blessing is a
   soft cloud that floats over the table and leaves. Neither one holds anything. */

/* "Ignore the twos at your peril!" — dark slab, crimson edge, a shake it
   inherits from having been shoved onto the screen. */
.peril-fx {
  position: relative;
  z-index: 3;
  padding: 0 12px;
  margin: 6px 0 2px;
  pointer-events: none;
}
.peril-fx__bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 8px 12px;
  border: 2px solid var(--peril-edge);
  border-radius: var(--r-md, 12px);
  /* darker than the klaxon strip: a threat, not an announcement */
  background: linear-gradient(180deg, rgba(12, 6, 10, 0.88), rgba(38, 10, 16, 0.82));
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 10px 24px -14px var(--peril-edge);
  animation: peril-in 0.26s cubic-bezier(0.2, 1.5, 0.4, 1) both,
             peril-shake 0.42s cubic-bezier(0.36, 0.07, 0.19, 0.97) 0.24s 2;
}
.peril-fx.is-out .peril-fx__bar { animation: peril-out 0.3s ease both; }
.peril-fx__text {
  font-size: clamp(12.5px, 3.6vw, 16px);
  font-weight: 900;
  letter-spacing: 0.03em;
  text-align: center;
  color: var(--peril-ink);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}
/* the die that is being ignored, staring back: two pips as eyes, brows lowered */
.peril-die { width: 24px; height: 24px; flex: 0 0 24px; }
.peril-die__body {
  fill: rgba(240, 232, 220, 0.94);
  stroke: var(--peril-edge);
  stroke-width: 2;
}
.peril-die__pip { fill: #1a0d12; animation: peril-glare 0.84s ease-in-out infinite; }
.peril-die__brow {
  stroke: var(--peril-edge);
  stroke-width: 2.6;
  stroke-linecap: round;
  fill: none;
}
@keyframes peril-in { from { opacity: 0; transform: translateY(-10px); } }
@keyframes peril-out { to { opacity: 0; transform: translateY(-8px); } }
/* a short, tight shake — the bar arrives like a finger jabbed at the table */
@keyframes peril-shake {
  10%, 90% { transform: translateX(-1.5px); }
  20%, 80% { transform: translateX(2.5px); }
  30%, 50%, 70% { transform: translateX(-3px); }
  40%, 60% { transform: translateX(3px); }
}
@keyframes peril-glare { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }

/* "Saved by the grace of the Twos" — a cream cloud with light behind it,
   crossing the table in 1.5s (GRACE_SHOW_MS in js/app.js). */
.grace-fx {
  position: absolute;
  inset: 0;
  z-index: 40;              /* over the table, under the chicken's 41 */
  overflow: hidden;
  pointer-events: none;     /* it must never eat a tap on the dice */
  display: grid;
  place-content: center;    /* centred, so the drift below is symmetric about it */
}
/* The drift carries the cloud right across and off. The easing LINGERS in the
   middle third — a linear crossing at this speed is unreadable — so the words
   are legible for most of the pass while the cloud still visibly travels. */
.grace-fx__drift {
  width: min(74vw, 340px);
  /* 3s, doubled on request: it must equal GRACE_SHOW_MS in js/app.js or the cloud
     either parks off-screen waiting to be hidden, or is cut off mid-crossing. */
  animation: grace-drift 3s cubic-bezier(0.08, 0.46, 0.92, 0.54) both;
}
.grace-cloud { position: relative; display: grid; place-items: center; }
.grace-puff { width: 100%; height: auto; display: block; }
.grace-puff__body {
  fill: var(--grace-cloud);
  /* warm rim light, as if the sun is behind it */
  stroke: var(--grace-rim);
  stroke-width: 2.5;
  filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.35));
}
/* the golden fan behind the cloud */
.grace-rays {
  position: absolute;
  inset: -34% 0 auto 0;
  width: 100%;
  height: auto;
  opacity: 0.75;
}
.grace-rays__fan path {
  stroke: var(--grace-ray);
  stroke-width: 9;
  stroke-linecap: round;
  fill: none;
  animation: grace-shine 3s ease-in-out both;   /* matches grace-drift, doubled */
}
.grace-fx__text {
  position: absolute;
  /* the puff's viewBox is 320x140 and the body sits low in it, so the words are
     nudged down to land in the cloud's belly rather than in the empty top */
  top: 50%;
  transform: translateY(-50%);
  /* 76% fit the puff's BOUNDING BOX with 35px to spare, but the painted cloud
     silhouette slopes well inside that box, so the tail of the phrase ("Twos")
     landed on the rim and off onto the near-black table, where the dark brown ink
     is illegible. 58% keeps the words inside the belly and lets them take two
     lines, which the cloud has room for. */
  width: 58%;
  text-align: center;
  font-size: clamp(11.5px, 3.1vw, 15px);
  font-weight: 900;
  line-height: 1.18;
  letter-spacing: 0.01em;
  color: var(--grace-ink);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
}
/* Viewport-relative, NOT a percentage of the cloud. Measured, a +/-118% self-width
   drift crossed a 390px phone but only covered the left half of a 1280px desktop
   — the cloud never left the screen and never looked like it was passing over.
   50vw carries it from fully off one edge to fully off the other at any width. */
@keyframes grace-drift {
  from { transform: translateX(calc(-50vw - 60%)); }
  to { transform: translateX(calc(50vw + 60%)); }
}
@keyframes grace-shine {
  0% { opacity: 0; }
  30% { opacity: 0.85; }
  100% { opacity: 0; }
}
.field-hint--bad { color: var(--alarm); font-weight: 700; }
.field input.is-bad { border-color: var(--alarm); }

/* ───────── game over ───────── */
.winner-card {
  text-align: center;
  border: 1px solid rgba(229, 163, 60, 0.45);
  border-radius: var(--r-xl);
  padding: 22px 18px;
  background: radial-gradient(120% 100% at 50% 0%, rgba(229, 163, 60, 0.18), rgba(0, 0, 0, 0.25));
}
.winner-card .crown { width: 54px; height: 54px; border-radius: 50%; margin: 0 auto 12px; display: block; }
.winner-card span { font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--amber); }
.winner-card h3 { margin: 6px 0 2px; font-size: 32px; font-weight: 900; color: var(--bone); }
.winner-card b { font-size: 44px; font-weight: 900; color: var(--amber); display: block; line-height: 1.05; }
.winner-card em { font-style: normal; font-size: 12.5px; color: var(--text-dim); }

/* ───────── The Big Wiener ─────────
   The winner gets a hot dog with their name on it. It sits at the TOP of the
   Game Over scroller and is deliberately compact — the final standings, the
   per-player stats, the winner's theme picker and Rematch all keep their place
   directly underneath and are never pushed off the screen.
   Food is food in every theme: the bun, sausage, mustard, ketchup, chili, cheese,
   relish and pickle are literal colours rather than theme tokens, so the dog
   reads correctly on all 17 tables. Only the headline borrows the theme. */
.wiener-crown {
  --wn-bun: #e8b263;
  --wn-bun-hi: #f6cf92;
  --wn-bun-line: rgba(140, 84, 24, 0.5);
  --wn-dog: #c1512f;
  --wn-dog-hi: rgba(255, 214, 196, 0.5);
  --wn-mustard: #f5c518;
  --wn-ketchup: #d8331f;
  --wn-chili: #8c3a1c;
  --wn-bean: #5d2a13;
  --wn-cheese: #ffc93f;
  --wn-relish: #4d7a1f;
  --wn-relish-bit: #7fb53a;
  --wn-relish-bit-pale: #a8cf6a;
  --wn-onion: #f7f1e0;
  --wn-pickle: #4f8a2b;
  text-align: center;
  margin-bottom: 12px;
}
.wiener-hed {
  margin: 0 0 2px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  line-height: 1;
}
.wiener-hed__the {
  font-size: 11px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.wiener-hed b {
  font-size: clamp(27px, 8.4vw, 40px);
  font-weight: 900;
  letter-spacing: 0.01em;
  color: var(--amber);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.35);
  animation: wn-hed 0.5s cubic-bezier(0.2, 1.7, 0.4, 1) both;
}
@keyframes wn-hed { 0% { opacity: 0; transform: scale(0.72); } 100% { opacity: 1; transform: none; } }
.wiener-stage { margin: 2px auto 0; max-width: 420px; }
.wiener { width: 100%; height: auto; overflow: visible; display: block; }
.wiener-sub {
  margin: 2px 0 0;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--text-dim);
  text-wrap: balance;
}

/* the drawing */
.wn-shadow { fill: rgba(0, 0, 0, 0.28); }
.wn-bun { fill: var(--wn-bun); stroke: var(--wn-bun-line); stroke-width: 3; }
.wn-bun--front { fill: var(--wn-bun-hi); }
.wn-bun-line { fill: none; stroke: var(--wn-bun-line); stroke-width: 3; stroke-linecap: round; opacity: 0.75; }
.wn-dog { fill: var(--wn-dog); stroke: rgba(90, 30, 14, 0.55); stroke-width: 3; }
.wn-dog-sheen { fill: none; stroke: var(--wn-dog-hi); stroke-width: 7; stroke-linecap: round; }
.wn-mustard { fill: none; stroke: var(--wn-mustard); stroke-width: 11; stroke-linejoin: round; stroke-linecap: round; }
.wn-hatch path { fill: none; stroke-width: 8; stroke-linecap: round; }
.wn-hatch--mustard path { stroke: var(--wn-mustard); }
.wn-hatch--ketchup path { stroke: var(--wn-ketchup); }
.wn-chili { fill: var(--wn-chili); }
.wn-beans circle { fill: var(--wn-bean); }
.wn-cheese { fill: var(--wn-cheese); opacity: 0.95; }
.wn-relish { fill: var(--wn-relish); }
/* chopped relish: flecks over the bed so it reads as relish, not a green ribbon */
.wn-relish-bit { fill: var(--wn-relish-bit); stroke: rgba(30, 60, 12, 0.45); stroke-width: 1.5; }
.wn-relish-bit--pale { fill: var(--wn-relish-bit-pale); }
.wn-onions path { fill: none; stroke: var(--wn-onion); stroke-width: 5; stroke-linecap: round; }
.wn-pickle path:first-child { fill: var(--wn-pickle); stroke: rgba(20, 60, 10, 0.6); stroke-width: 3; }
.wn-pickle-line { fill: none; stroke: rgba(240, 255, 220, 0.6); stroke-width: 3; stroke-linecap: round; }
/* the name is the point: plated and stroked so it reads over any dressing */
.wn-banner-plate { fill: #fdf6e6; stroke: rgba(90, 30, 14, 0.6); stroke-width: 3; }
.wn-banner-tail { fill: #e6d6b4; stroke: rgba(90, 30, 14, 0.5); stroke-width: 3; }
.wn-name {
  font-family: var(--font-display, inherit);
  font-weight: 900;
  letter-spacing: 0.01em;
}
.wn-name--banner { fill: #7c2d12; }
.wn-name--script {
  fill: var(--wn-mustard);
  stroke: #3f1405;
  paint-order: stroke fill;
  stroke-linejoin: round;
}
/* the seared band the mustard script is piped onto: it hides the same-colour
   zigzag behind the letters, so the name reads as clearly as the banner dogs */
.wn-name-sear {
  fill: none;
  stroke: rgba(58, 16, 5, 0.9);
  stroke-linecap: round;
}
.wn-conf--0 { fill: var(--amber); }
.wn-conf--1 { fill: var(--mint, #6fcf9b); }
.wn-conf--2 { fill: var(--wn-ketchup); }
.wn-conf--3 { fill: var(--wn-mustard); }

/* three entrances, picked at random per win — transform and opacity only */
.wiener--in-bounce { animation: wn-bounce 0.72s cubic-bezier(0.18, 1.5, 0.34, 1) both; }
.wiener--in-spin { animation: wn-spin 0.8s cubic-bezier(0.2, 1.2, 0.3, 1) both; }
.wiener--in-rise { animation: wn-rise 0.78s cubic-bezier(0.16, 1.2, 0.3, 1) both; }
@keyframes wn-bounce {
  0%   { opacity: 0; transform: translateY(-70%) scale(0.7); }
  46%  { opacity: 1; transform: translateY(6%) scale(1.06); }
  70%  { transform: translateY(-3%) scale(0.985); }
  100% { opacity: 1; transform: none; }
}
@keyframes wn-spin {
  0%   { opacity: 0; transform: rotate(-320deg) scale(0.4); }
  70%  { opacity: 1; transform: rotate(8deg) scale(1.05); }
  100% { opacity: 1; transform: none; }
}
@keyframes wn-rise {
  0%   { opacity: 0; transform: translateY(58%) scale(0.86); }
  60%  { opacity: 1; transform: translateY(-4%) scale(1.03); }
  100% { opacity: 1; transform: none; }
}
/* confetti only rides in with `rise` */
.wn-conf { animation: wn-conf 1.05s ease-in 0.12s both; }
@keyframes wn-conf {
  0%   { opacity: 0; transform: translateY(-26px) scale(0.6); }
  30%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(150px) rotate(120deg); }
}
.standings { width: 100%; border-collapse: collapse; font-size: 15px; }
.standings td { padding: 11px 6px; border-bottom: 1px solid var(--line); }
.standings .pos { width: 26px; color: var(--text-faint); font-weight: 800; }
.standings .sc { text-align: right; font-weight: 900; color: var(--bone); }
.standings i { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 8px; }
.sect { margin: 18px 0 4px; font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--amber); }
.stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
.stat-card {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 12px;
  background: rgba(255, 255, 255, 0.03);
}
.stat-card h4 { margin: 0 0 8px; font-size: 14px; display: flex; align-items: center; gap: 7px; }
.stat-card h4 i { width: 9px; height: 9px; border-radius: 50%; }
.stat-card dl { margin: 0; display: grid; grid-template-columns: 1fr auto; gap: 3px 8px; font-size: 12.5px; }
.stat-card dt { color: var(--text-faint); }
.stat-card dd { margin: 0; text-align: right; font-weight: 800; color: var(--bone-dim); }
.stat-card h4 small { margin-left: auto; font-size: 11px; font-weight: 600; color: var(--text-faint); letter-spacing: 0.02em; }
.stat-grid--wide { grid-template-columns: 1fr; }
/* Last game vs Lifetime, side by side */
.stats-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin: 18px 0 8px; }
.stats-head .sect { margin: 0 0 4px; }
.stats-head .seg { flex: 1 1 100%; }
.seg--sm { padding: 3px; gap: 4px; }
.seg--sm button { min-height: 38px; padding: 0 8px; font-size: 13px; white-space: nowrap; }
#stats-view { margin-bottom: 10px; }
.cmp { display: grid; grid-template-columns: 1fr auto auto; gap: 5px 14px; font-size: 13px; align-items: baseline; }
.cmp-h { font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-faint); text-align: right; }
.cmp-k { color: var(--text-faint); }
.cmp-last { text-align: right; font-weight: 800; color: var(--bone); white-space: nowrap; }
.cmp-life { text-align: right; color: var(--bone-dim); white-space: nowrap; }
.cmp-up, .cmp-down { font-style: normal; font-size: 9px; margin-left: 5px; vertical-align: 1px; }
.cmp-up { color: #8fd18a; }
.cmp-down { color: #ef9a86; }
.cmp-foot { margin: 10px 0 0; font-size: 11.5px; color: var(--text-faint); }
.stat-remove {
  margin-top: 10px; background: none; border: 1px solid var(--line); border-radius: var(--r-sm, 8px);
  color: var(--text-faint); font: 600 12px var(--font-ui); padding: 7px 10px; min-height: 36px;
}
.stat-remove[data-armed] { color: #ef9a86; border-color: rgba(239, 154, 134, 0.5); }
.stats-empty { text-align: center; color: var(--text-dim); padding: 30px 12px; }

/* ───────── wide screens: two-column table ───────── */
@media (min-width: 900px) {
  .screen--sheet .scroller, .screen--home .home-wrap { max-width: 720px; margin: 0 auto; width: 100%; }
  .screen--sheet .dock, .topbar { max-width: 100%; }
  .screen--sheet .dock { justify-content: center; }
  .screen--sheet .dock .btn--wide { flex: 0 1 320px; }
  .brand { padding-top: 3vh; }

  .rail { justify-content: center; }
  .game-main { flex-direction: row; gap: 0; }
  /* The 320px log column beside the table is gone, so the board takes the width
     it freed instead of handing it to the margins. */
  .game-board { flex: 1; padding: 10px 24px 0; max-width: 1180px; margin: 0 auto; }
  /* The desktop log used to take a 320px column beside the table, and hid its
     own toggle. Both are gone: the log is the same bottom strip at every width,
     the toggle is the strip so it can never be unreachable, and expanded it is
     the same full-width attached panel — not a box floating over the table. */
  .log { padding: 8px 28px 10px; }
  .die { --d: 92px; }
  .dice-zone { --persp: 2000px; }
  .dice { gap: 24px; }
  .tally-turn b { font-size: 58px; }
  .tally-bank b { font-size: 34px; }
  /* the squeeze rule outranks the plain selector, so the desktop sizes have to
     be restated inside it or the countdown would stay at its phone size */
  .tally.has-need .tally-need b { font-size: 34px; }
  .tally.has-need .tally-bank b { font-size: 30px; }
  .tally.has-need .tally-turn b { font-size: 44px; }
  .dock--game { align-items: center; }
  .dock--game .readout { font-size: 15px; }
  .dock-btns { width: min(620px, 100%); }
  .dock-assist { width: min(620px, 100%); }
  .held-tray .die { --d: 36px; }
}
@media (min-width: 900px) and (max-height: 780px) {
  .die { --d: 74px; }
}
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
  /* the invited Join button is still the lit primary — it just holds a steady ring */
  .btn--invited { animation: none !important; box-shadow: 0 6px 0 -2px var(--cta-shadow), var(--shadow-lift), 0 0 0 4px rgba(229, 163, 60, 0.4) !important; }
  /* no tumbling: the dice simply appear at rest with a short cross-fade */
  .die.rolling .die-cube { animation: none !important; }
  .die.rolling {
    animation: die-settle 180ms ease both !important;
    will-change: auto;
  }
  /* The squelch still takes the whole screen — losing the turn is the point —
     but nothing lunges, spins or flashes: the wash and the stamp are simply
     there, held by the same JS timer, and then gone. */
  .squelch-fx { animation: none !important; opacity: 1 !important; }
  .squelch-fx .stamp,
  .squelch-fx__sub { animation: none !important; opacity: 1 !important; transform: rotate(-9deg); }
  .squelch-fx__sub { transform: none; }
  .squelch-fx__rays { display: none; }
  /* The Big Wiener is still crowned — hot dog, headline and name all present and
     final — it simply arrives without the bounce, the spin or the confetti. */
  .wiener, .wiener-hed b { animation: none !important; opacity: 1 !important; transform: none !important; }
  /* no confetti at all when motion is reduced: hide the group and stop the
     individual bits, which animate on their own timelines */
  .wn-confetti, .wn-conf { display: none; animation: none !important; }
  /* Jump-off Point still calls out and still leaves by itself; it stops flashing */
  .jumpoff-fx__bar { animation: none !important; opacity: 1 !important; transform: none !important; }
  .jumpoff-klaxon__wave { animation: none !important; opacity: 1 !important; }
  /* The Twos both still say their line and still leave on their own JS timers.
     The taunt stops shaking and its pips stop glaring; the cloud stops crossing
     and simply sits in the middle of the table for its 1.5s, which is where the
     drift would have put it at the moment it was most readable anyway. */
  .peril-fx__bar { animation: none !important; opacity: 1 !important; transform: none !important; }
  .peril-die__pip { animation: none !important; opacity: 1 !important; }
  .grace-fx__drift { animation: none !important; transform: none !important; margin: 0 auto; }
  .grace-rays__fan path { animation: none !important; opacity: 0.7 !important; }
  /* The Chicken keeps its stamp and its cluck — the call-out still happens, and
     the overlay still leaves by itself on the same JS timers — but the bird does
     not run, turn, scratch or kick up dust. Killing the animations outright
     leaves every part on its base pose, which is a chicken standing in the
     middle of the screen facing right: .ck-run's travel and .ck-turn's mirror
     are animation-only, so with no animation it is centred and unflipped. */
  .chicken { animation: none !important; }
  .ck-run, .ck-turn, .ck-head, .ck-wing, .ck-body, .ck-leg--front, .ck-leg--back,
  .ck-tail, .ck-squawk, .ck-dust {
    animation: none !important;
    transform: none !important;
  }
  /* the squawk marks are drawn beside the beak and simply stay there: their base
     opacity is 0 because the animation gates them, so it has to be forced back */
  .ck-squawk { opacity: 1 !important; }
  /* no dust at all: it only ever exists inside its animation */
  .ck-dust { display: none; }
  @keyframes die-settle {
    from { opacity: 0; }
    to { opacity: 1; }
  }
}

/* Around the corner is mandatory: BANK is removed and ROLL ON takes the dock. */
.dock-btns--forced .btn--roll {
  animation: forcedPulse 1.6s ease-in-out infinite;
  font-size: 17px;
}
@keyframes forcedPulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ember) 40%, transparent); }
  50% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--ember) 0%, transparent); }
}

/* the game signs its name: title + tagline, wherever a screen needs a footer */
.brand-foot {
  margin: 18px 0 4px;
  text-align: center;
  color: var(--text-faint);
  font-size: 12px;
  line-height: 1.5;
}
.brand-foot b {
  display: block;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 15px;
  letter-spacing: 0.05em;
  color: var(--text-dim);
}
.brand-foot span { display: block; letter-spacing: 0.03em; }

/* ───────── the WebGL table ─────────
   When the 3D renderer is live the canvas draws the surface and the dice, and
   the existing .die buttons stay in the DOM as transparent hit targets parked
   over each rendered die. Nothing else about the screen changes, so falling
   back to the CSS table is a class flip away. */
.dice-zone.is-3d {
  perspective: none;
  /* The hit targets are positioned from the canvas, which is wider than the
     play window, so a target can sit a few pixels outside the zone. `clip`
     keeps that out of the ancestors' scroll width — with plain overflow the
     table screen became sideways-scrollable and one nudge of that scroll put
     every hit target out of register with the dice underneath. */
  overflow: clip;
}
.dice-zone.is-3d::before,
.dice-zone.is-3d::after { opacity: 0; }
.dice-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  touch-action: manipulation;
}
.dice-zone.is-3d .dice {
  position: absolute;
  inset: 0;
  display: block;
  gap: 0;
  z-index: 2;
}
.dice-zone.is-3d .die {
  position: absolute;
  left: 0;
  top: 0;
  margin: 0;
  background: none;
  transition: none;
  animation: none !important;
  border-radius: 12px;
}
/* the cube visuals are the canvas's job now — the button keeps only its hit area */
.dice-zone.is-3d .die > * { opacity: 0 !important; }
.dice-zone.is-3d .die:focus-visible {
  outline: 2px solid var(--gold, #f0c25a);
  outline-offset: 3px;
}
.dice-zone.is-3d .dice-empty { z-index: 3; }

/* ───────── the WebGL table goes full-bleed ─────────
   The canvas covers the whole table screen and the UI floats on top of it, so
   the environment is the room you are playing in rather than a rectangle inset
   in a page. Every piece of chrome over the canvas becomes a scrim: a gradient
   dark enough to hold its text at full contrast, with no hard border to read as
   the edge of a viewport. */
.screen--game.has-3d { position: fixed; }
.screen--game.has-3d > .dice-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
}
.screen--game.has-3d > *:not(.dice-canvas) { position: relative; z-index: 1; }
/* … but the Chicken is an OVERLAY, not part of the column. The rule above beats
   `.chicken-fx { position: absolute }` on specificity, which quietly turned the
   bird into a ~200px band pinned under the turn log on the 3D table — it has to
   own the whole screen so it can run across the MIDDLE of it. Fixed here rather
   than by loading `.chicken-fx` with !important. */
.screen--game.has-3d > .chicken-fx { position: absolute; inset: 0; }
/* The grace cloud is an OVERLAY too, and it fell into exactly the same trap: the
   rule above beat `.grace-fx { position: absolute }` on specificity, so on the 3D
   table the cloud became a 126px band wedged into the column — it shoved the whole
   board down for the 1.5s it was on screen, then snapped it back. Measured at 390px:
   rect 0,60,390,126 as a flow child vs 0,0,390,844 once absolute again. */
.screen--game.has-3d > .grace-fx { position: absolute; inset: 0; }
.screen--game.has-3d .topbar--game {
  background: linear-gradient(180deg, rgba(6, 9, 8, 0.92), rgba(6, 9, 8, 0.62) 62%, rgba(6, 9, 8, 0));
  border-bottom: none;
  backdrop-filter: none;
}
.screen--game.has-3d .rail-wrap {
  background: linear-gradient(180deg, rgba(6, 9, 8, 0.6), rgba(6, 9, 8, 0));
  border-bottom: none;
  box-shadow: none;
}
.screen--game.has-3d .chip { background: rgba(6, 9, 8, 0.55); backdrop-filter: none; }
.screen--game.has-3d .tally { text-shadow: 0 2px 10px rgba(0, 0, 0, 0.75); }
.screen--game.has-3d .held { position: relative; z-index: 1; }
.screen--game.has-3d .held-tray {
  background: rgba(4, 7, 6, 0.42);
  border-style: solid;
  border-color: rgba(255, 255, 255, 0.14);
}
.screen--game.has-3d .log-pane {
  background: rgba(4, 7, 6, 0.86);
  border-top: none;
}
/* the dock keeps a hard floor of contrast: its own dark plate, always */
.screen--game.has-3d .dock--game {
  background:
    linear-gradient(180deg, rgba(4, 7, 6, 0.5) 0%, rgba(4, 7, 6, 0.86) 20%, rgba(4, 7, 6, 0.95) 58%, rgba(4, 7, 6, 0.97) 100%);
  border-top: none;
  backdrop-filter: none;
  padding-top: 22px;
}
/* The desktop side-column scrims (and their empty-state variants) are gone with
   the column itself: one 28px strip reads the same at every width. */

/* ─────────────────────────────────────────────────────────────
   Held / set-aside dice next to a 3D table.
   The set-aside dice used to read as small flat CSS dice parked beside six
   photographic ones. They are now presented as the SAME die, seen square-on and
   at rest: no roll jitter, no tilt, the bone face and near-black pips the 3D
   skin uses, a real bevel and a contact shadow. A set-aside die is a die you
   have finished with, so reading it face-on is also the more useful thing.
   ───────────────────────────────────────────────────────────── */
.held-tray .die {
  --d: 34px;
  --jx: 0px; --jy: 0px; --jr: 0deg; --tiltX: 0deg; --tiltY: 0deg;
  cursor: default;
  transform: none;
  filter: drop-shadow(0 3px 5px rgba(0, 0, 0, 0.55));
}
/* square-on: the value's own face is the one facing the camera already, so
   cancelling the rest rotation shows it flat without touching the markup */
.held-tray .die .die-cube { transform: none; }
.held-tray .die .die-face {
  border-radius: calc(var(--d) * 0.16);
  padding: calc(var(--d) * 0.15);
  /* the 3D die's bevel, as a rim: bright top-left, dark bottom-right */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.85),
    inset 0 -1.5px 2px rgba(0, 0, 0, 0.22),
    inset 0 0 0 1px rgba(0, 0, 0, 0.12);
}
/* only the face pointing at the camera is drawn; the other five would poke out
   past the un-rotated silhouette */
.held-tray .die .die-face { opacity: 0; }
.held-tray .die[data-face='1'] .die-face--1,
.held-tray .die[data-face='2'] .die-face--2,
.held-tray .die[data-face='3'] .die-face--3,
.held-tray .die[data-face='4'] .die-face--4,
.held-tray .die[data-face='5'] .die-face--5,
.held-tray .die[data-face='6'] .die-face--6 { opacity: 1; transform: none; }
.held-tray .die .die-face::before { background: none; }
.held-tray .die .pip {
  background: var(--pip);
  box-shadow: inset 0 -0.5px 0.5px rgba(255, 255, 255, 0.28);
}
.held-tray .die-cast { display: none; }
.held-tray { align-items: center; padding: 6px 8px; }

/* ── perf HUD: opt-in with ?perf=1, never shipped on ── */
.perf-hud {
  position: absolute;
  top: 6px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 9;
  padding: 3px 9px;
  border-radius: 999px;
  font: 600 10.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: 0.04em;
  color: #d8ffe4;
  background: rgba(0, 0, 0, 0.72);
  border: 1px solid rgba(120, 255, 180, 0.35);
  pointer-events: none;
  white-space: nowrap;
}

/* ── dock contrast: the primary action must never be washed out ──
   `filter: saturate()` on a disabled button drained the CTA to a pink-grey on
   the warmer themes. Disabled state now dims with opacity alone and keeps a
   solid ink colour, so ROLL ON stays readable in every theme. */
.btn:disabled { opacity: 0.55; cursor: not-allowed; filter: none; }
.btn--cta:disabled { background: var(--ember); color: var(--cta-ink); }

/* ─────────────────────────────────────────────────────────────
   Desktop, 3D on: the log used to float as a 316px column over the right of the
   table, and the board reserved 328px of padding for it. The log is a bottom
   strip now, so the board takes the full width of the room.
   ───────────────────────────────────────────────────────────── */
@media (min-width: 900px) {
  .screen--game.has-3d .game-main { position: relative; }
  .screen--game.has-3d .game-board {
    max-width: none;
    margin: 0;
    padding: 10px 28px 0;
  }
}
/* the set-aside row sits over the table, so it carries its own scrim */
.screen--game.has-3d .held {
  padding: 4px 10px 6px;
  border-radius: 14px;
  background: linear-gradient(180deg, rgba(4, 6, 5, 0.0), rgba(4, 6, 5, 0.52) 34%, rgba(4, 6, 5, 0.66));
}
.screen--game.has-3d .held-label { color: rgba(255, 255, 255, 0.72); }

/* ─────────────────────────────────────────────────────────────
   THE OVERLAY CONTRAST FLOOR — one rule set, all 17 rooms.

   Stage 2 replaced dark felt backdrops with real rooms, and five of them
   (modern, diner, restaurant, beach, mountain) are genuinely pale: plaster,
   cream laminate, linen, sand and snow all come out near white on screen. Every
   piece of 2D chrome sits over that canvas, so any label drawn in a theme's
   --text-dim / --text-faint grey was grey-on-grey — measured at 1.4:1 for
   “NOT ON BOARD” over the modern studio wall.

   The fix is deliberately NOT per-theme colour tweaks. Overlay text gets a
   scrim it can rely on (a plate behind the words, never a curtain over the
   room) plus a fixed ink pair that does not follow the theme, so the same
   numbers hold in the brightest and the darkest room. Ratios are measured on
   the rendered pixels of all 17 rooms — see squelch-qa/uimeasure.py.
   ───────────────────────────────────────────────────────────── */
.screen--game.has-3d {
  --ui-scrim: rgba(6, 9, 8, 0.88);
  --ui-scrim-soft: rgba(6, 9, 8, 0.78);
  --ui-ink: #f4f7f9;        /* anything you actually read */
  --ui-ink-muted: #d9e0e6;  /* labels: still quiet, still over 7:1 on the scrim */
}
/* header: the plate now carries the whole bar, not just its top edge */
.screen--game.has-3d .topbar--game {
  background: linear-gradient(180deg, rgba(6, 9, 8, 0.96), rgba(6, 9, 8, 0.88) 64%, rgba(6, 9, 8, 0.42));
}
.screen--game.has-3d .topbar-note,
.screen--game.has-3d .turn-of em { color: var(--ui-ink-muted); }
.screen--game.has-3d .turn-of b { color: var(--ui-ink); }
/* player cards: their own plate, so a card reads the same over snow and over
   a night sky. The active card keeps its amber tint, laid ON the plate. */
.screen--game.has-3d .rail-wrap {
  background: linear-gradient(180deg, rgba(6, 9, 8, 0.62), rgba(6, 9, 8, 0.1));
}
.screen--game.has-3d .chip {
  background: var(--ui-scrim);
  border-color: rgba(255, 255, 255, 0.18);
}
.screen--game.has-3d .chip.is-active {
  background: linear-gradient(0deg, rgba(229, 163, 60, 0.16), rgba(229, 163, 60, 0.16)), var(--ui-scrim);
}
.screen--game.has-3d .chip-name { color: var(--ui-ink); }
.screen--game.has-3d .chip-flag { color: var(--ui-ink-muted); }
.screen--game.has-3d .chip.is-off .chip-score { color: var(--ui-ink-muted); }
.screen--game.has-3d .chip.is-out { opacity: 0.72; }
/* the title badges need the same treatment the log kinds needed: over a photo
   environment a translucent amber wash on a translucent card is mush, so the
   badge gets its own near-opaque plate and the ink goes to full amber. */
.screen--game.has-3d .chip-title,
.screen--game.has-3d .chip.is-active .chip-title {
  color: var(--amber);
  background: rgba(6, 9, 8, 0.82);
  border-color: rgba(229, 163, 60, 0.75);
}
/* turn / banked readout: a pill, sized to the numbers, centred over the table */
.screen--game.has-3d .tally {
  align-self: center;
  padding: 3px 20px 5px;
  border-radius: 18px;
  background: var(--ui-scrim-soft);
  box-shadow: 0 12px 26px -16px rgba(0, 0, 0, 0.9);
}
.screen--game.has-3d .tally span { color: var(--ui-ink-muted); }
.screen--game.has-3d .tally-bank b { color: var(--ui-ink); }
/* the countdown keeps its alarm colour over the felt — it must NOT read as an
   ordinary score — but the label needs lifting off the dark plate */
.screen--game.has-3d .tally-need span { color: var(--peril-ink); }
.screen--game.has-3d .tally-need.is-ahead span { color: var(--mint); }
/* the empty-table hint sits in the middle of the room, so it carries a pill */
.screen--game.has-3d .dice-empty {
  color: var(--ui-ink);
  padding: 4px 12px;
  border-radius: 999px;
  background: var(--ui-scrim-soft);
}
/* set-aside strip */
.screen--game.has-3d .held {
  background: linear-gradient(180deg, rgba(4, 6, 5, 0.34), rgba(4, 6, 5, 0.82) 34%, rgba(4, 6, 5, 0.9));
}
.screen--game.has-3d .held-label { color: var(--ui-ink-muted); }
.screen--game.has-3d .held-empty { color: var(--ui-ink-muted); }
.screen--game.has-3d .held-tray {
  background: rgba(4, 7, 6, 0.55);
  border-color: rgba(255, 255, 255, 0.2);
}
/* turn log strip */
.screen--game.has-3d .log-pane {
  background: rgba(4, 7, 6, 0.92);
}
.screen--game.has-3d .log { background: rgb(4, 7, 6); }
.screen--game.has-3d .log-strip,
.screen--game.has-3d .log-caret { color: var(--ui-ink-muted); }
.screen--game.has-3d .log-latest { color: var(--ui-ink); }
.screen--game.has-3d .log-empty,
.screen--game.has-3d .log .log-empty { color: var(--ui-ink-muted); }
.screen--game.has-3d .log li { color: var(--ui-ink); }
.screen--game.has-3d .log li::before { color: var(--ui-ink-muted); }
/* …but the kind colours still have to win over that legibility default. The
   two rules above are more specific than `.log li.k-squelch`, so without these
   every entry flattened to white over 3D and only the bold weight survived —
   which is worst on the minimised strip, where a single coloured line IS the
   whole summary. All four read at high contrast on the near-black plate. */
.screen--game.has-3d .log li.k-bank,
.screen--game.has-3d .log-latest.k-bank { color: var(--mint); }
.screen--game.has-3d .log li.k-squelch,
.screen--game.has-3d .log-latest.k-squelch { color: var(--alarm); }
.screen--game.has-3d .log li.k-chicken,
.screen--game.has-3d .log-latest.k-chicken { color: var(--chicken-beak); }
.screen--game.has-3d .log li.k-reject,
.screen--game.has-3d .log-latest.k-reject { color: var(--amber); }
.screen--game.has-3d .log li.k-final,
.screen--game.has-3d .log li.k-win,
.screen--game.has-3d .log-latest.k-final,
.screen--game.has-3d .log-latest.k-win { color: var(--amber); }
.screen--game.has-3d .log li.k-title,
.screen--game.has-3d .log-latest.k-title { color: var(--amber); }
.screen--game.has-3d .log li.k-peril,
.screen--game.has-3d .log-latest.k-peril { color: var(--peril-log); }
.screen--game.has-3d .log li.k-grace,
.screen--game.has-3d .log-latest.k-grace { color: var(--grace-rim); }
/* dock: the plate was already there; the words on it were still theme grey */
.screen--game.has-3d .readout { color: var(--ui-ink); }
/* Disabled is signalled by the dashed border and the copy (“Nothing to take
   yet”), not by fading the words into the plate: a 0.55 opacity wash took both
   dock buttons under 3:1. */
.screen--game.has-3d .btn--assist:disabled {
  opacity: 1;
  color: var(--ui-ink-muted);
  border-color: rgba(255, 255, 255, 0.34);
}
.screen--game.has-3d .dock-btns .btn--bank:disabled {
  opacity: 1;
  color: var(--ui-ink-muted);
  border-color: rgba(255, 255, 255, 0.3);
  background: rgba(255, 255, 255, 0.06);
}
/* The desktop backplates for an empty side column went with the column: a 28px
   strip carries one solid plate at every width, so the words never sit on bare
   room. */
/* the klaxon's own tint is 14-30% alarm; over a plaster wall that is pale pink
   and the white words vanish. It gets a dark plate under the tint. */
.screen--game.has-3d .jumpoff-fx__bar {
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--alarm) 34%, transparent), color-mix(in srgb, var(--alarm) 18%, transparent)),
    rgba(4, 6, 5, 0.94);
}
