/* ============================================================================
   PARLOUR — design system
   Zero web fonts, zero image files, zero frameworks. Hand-written CSS + SVG.
   Dark-first, because these games are played at night in dim rooms.
   ========================================================================= */

/* ---------------------------------------------------------------- tokens -- */
:root {
  color-scheme: dark light;

  /* surfaces */
  --bg-sunken: #06080c;
  --bg-base: #0b0e14;
  --bg-inset: #10141b;
  --bg-raised: #151a23;
  --bg-overlay: #1e242f;
  --bg-hover: #242b38;
  --scrim: rgb(4 6 10 / 0.74);

  /* text — never pure white: halation on OLED in a dark room */
  --text-1: #f5f1e6;
  --text-2: #b6becc;
  --text-3: #8b95a5;
  --text-off: #5b6472; /* decorative only, fails AA by design */
  --text-on-accent: #0b0e14;
  --text-on-danger: #f7f3ea;

  /* lines */
  --line: #232a36;
  --line-2: #333b4a;
  --line-strong: #6b7688;

  /* semantic */
  --accent: #e8b84b;
  --accent-hi: #f2c864;
  --accent-lo: #cfa13b;
  --danger: #ff6b5e;
  --danger-fill: #b7332a;
  --danger-fill-hi: #ce3b30;
  --success: #3fd98b;
  --success-fill: #147a50;
  --info: #66b8f5;
  --warn: #f5b63d;
  --focus: #7fd3ff;

  /* teams — hue is one of THREE channels; see .tm glyph + pattern */
  --team-0: #8b95a5;
  --team-1: #5ba8ff;
  --team-2: #ff7a45;
  --team-3: #2fd3a0;
  --team-4: #f2e14d;
  --team-5: #e48fd6;
  --team-6: #45d6e8;

  --game-accent: var(--accent);

  /* space / radii / metrics */
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px;
  --r-1: 4px; --r-2: 8px; --r-3: 12px; --r-4: 18px; --r-5: 26px; --r-pill: 999px;
  --tap: 48px; --tap-lg: 56px; --shell: 520px; --shell-wide: 640px; --hair: 1px;

  /* elevation — each pairs a rim-light inset with a drop shadow, which is
     what separates "physical card" from "div with a shadow" */
  --e-1: 0 1px 0 rgb(255 255 255 / 0.04) inset, 0 1px 2px rgb(0 0 0 / 0.5);
  --e-2: 0 1px 0 rgb(255 255 255 / 0.05) inset, 0 4px 10px -2px rgb(0 0 0 / 0.6);
  --e-3: 0 1px 0 rgb(255 255 255 / 0.06) inset, 0 12px 28px -8px rgb(0 0 0 / 0.7);
  --e-4: 0 24px 60px -12px rgb(0 0 0 / 0.8);
  --press: inset 0 2px 6px rgb(0 0 0 / 0.55);
  --deboss: inset 0 1px 1px rgb(0 0 0 / 0.7), inset 0 -1px 0 rgb(255 255 255 / 0.05);

  /* motion */
  --d-1: 90ms; --d-2: 160ms; --d-3: 240ms; --d-4: 420ms; --d-5: 700ms;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in: cubic-bezier(0.55, 0, 1, 0.45);
  --ease-both: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-snap: cubic-bezier(0.34, 1.56, 0.64, 1);

  /* Sampled damped springs, matching the iOS system presets closely enough
     that they read as native on the phone in your hand. Use the paired
     duration — a spring curve at the wrong duration stops being physics.
     Springs are for things that MOVE; keep --ease-out for things that only
     change appearance, where a spring just looks like a stutter. */
  --spring-snappy: linear(0, 0.027 4%, 0.095 8%, 0.486 24%, 0.657 32%, 0.772 39%,
    0.858 46%, 0.936 56%, 0.981 67%, 1.004 83%, 1); /* 420ms */
  --spring-bouncy: linear(0, 0.028 4%, 0.122 9%, 0.561 25%, 0.75 33%, 0.859 39%,
    0.948 46%, 1.015 55%, 1.044 66%, 1.016 99%, 1); /* 550ms */

  /* Every travel distance is multiplied by this. Reduced motion sets it to 0,
     which removes movement while leaving opacity and duration intact — so an
     animation that carries information still carries it. */
  --motion: 1;

  /* type */
  --font-body: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto,
    'Helvetica Neue', Arial, sans-serif;
  /* A serif for the few places that carry the game's voice: titles, role
     names, narration. iOS resolves New York, Android resolves Noto Serif.
     Nothing else on either platform is not SF or Roboto, so one narrowly-used
     serif is the cheapest character this app can buy — zero bytes. */
  --font-display: ui-serif, 'New York', Georgia, 'Noto Serif', 'Times New Roman', serif;
  --font-ui: ui-sans-serif, system-ui, -apple-system, 'Segoe UI Variable Display',
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Roboto Mono',
    'Segoe UI Mono', monospace;

  /* 17px body floor: read at arm's length, at an angle, in the dark */
  --t-2xs: clamp(11px, 2.6vw, 13px);
  --t-xs: clamp(13px, 3.1vw, 15px);
  --t-sm: clamp(15px, 3.6vw, 17px);
  --t-md: clamp(17px, 4.2vw, 19px);
  --t-lg: clamp(20px, 5vw, 24px);
  --t-xl: clamp(25px, 6.4vw, 31px);
  --t-2xl: clamp(32px, 8.6vw, 42px);
  --t-3xl: clamp(42px, 12vw, 60px);
  --t-4xl: clamp(50px, 15vw, 80px);

  /* grain: rasterized once when the data-URI decodes, then it is a cached
     bitmap. Applying filter:url() to live DOM would re-run it every frame. */
  --grain: 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='.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");
  --grain-op: 0.055;
  --card-stock: linear-gradient(158deg, #1b212c 0%, #141922 46%, #10151d 100%);

  --kb: 0px; /* keyboard inset, synced from VisualViewport */
}

/* A 1px border on a 3× phone is three device pixels, and reads as a drawn line
   rather than an edge. This one rule sharpens every inset outline in the file. */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
  :root { --hair: 0.5px; }
}

/* Shadows tinted toward the game's accent rather than pure black, and split
   into a CONTACT shadow that never scales and an AMBIENT one that does. That
   pairing is what makes a surface read as resting on something instead of
   floating above a blur. Plain-black fallbacks stay above for older engines. */
@supports (color: oklch(from red l c h)) {
  :root {
    --shade: oklch(from var(--game-accent) 0.16 calc(c * 0.5) h);
    --e-1: 0 1px 0 rgb(255 255 255 / 0.04) inset, 0 1px 1px oklch(from var(--shade) l c h / 0.5);
    --e-2: 0 1px 0 rgb(255 255 255 / 0.05) inset,
           0 1px 1px oklch(from var(--shade) l c h / 0.5),
           0 4px 10px -2px oklch(from var(--shade) l c h / 0.5);
    --e-3: 0 1px 0 rgb(255 255 255 / 0.06) inset,
           0 1px 1px oklch(from var(--shade) l c h / 0.5),
           0 12px 28px -8px oklch(from var(--shade) l c h / 0.62);
    --e-4: 0 2px 2px oklch(from var(--shade) l c h / 0.5),
           0 24px 60px -12px oklch(from var(--shade) l c h / 0.72);
  }
}

/* Light: system preference with no explicit override.
   Every token is defined on bare :root first — a token defined ONLY inside a
   media query breaks when [data-theme] is set against the OS preference. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme='dark']) {
    color-scheme: light dark;
    --bg-sunken: #e8e3d6; --bg-base: #f4f1e9; --bg-inset: #ede9de;
    --bg-raised: #fff; --bg-overlay: #fff; --bg-hover: #f0ece1;
    --scrim: rgb(28 25 18 / 0.46);
    --text-1: #16191f; --text-2: #4a5260; --text-3: #57616f; --text-off: #98a0ac;
    --text-on-accent: #16191f; --text-on-danger: #fff;
    --line: #ddd7c8; --line-2: #c9c2b0; --line-strong: #807965;
    --accent: #b4870f; --accent-hi: #c9971a; --accent-lo: #8f6b08;
    --danger: #a82a20; --danger-fill: #b7332a; --danger-fill-hi: #9c2a21;
    --success: #046a50; --success-fill: #046a50;
    --info: #10559c; --warn: #8a6a00; --focus: #0a59b2;
    --team-0: #57616f; --team-1: #0a59b2; --team-2: #b03a0a;
    --team-3: #046a50; --team-4: #6e5f00; --team-5: #9333a8; --team-6: #0c6273;
    /* Light comes from above, so in a light theme the WHITE rim belongs on the
       underside bevel and the top edge is a dark hairline. Reusing the dark
       theme's top-white inset lit every surface from below. */
    --e-1: 0 -1px 0 rgb(255 255 255 / 0.9) inset, 0 1px 0 rgb(28 25 18 / 0.05) inset,
           0 1px 2px rgb(60 52 32 / 0.14);
    --e-2: 0 -1px 0 rgb(255 255 255 / 0.9) inset, 0 1px 0 rgb(28 25 18 / 0.06) inset,
           0 1px 1px rgb(60 52 32 / 0.10), 0 6px 16px -6px rgb(60 52 32 / 0.16);
    --e-3: 0 -1px 0 rgb(255 255 255 / 0.9) inset,
           0 1px 1px rgb(60 52 32 / 0.12), 0 10px 24px -8px rgb(60 52 32 / 0.24);
    --e-4: 0 2px 3px rgb(60 52 32 / 0.14), 0 22px 52px -14px rgb(60 52 32 / 0.3);
    --press: inset 0 2px 5px rgb(60 52 32 / 0.22);
    --deboss: inset 0 1px 1px rgb(60 52 32 / 0.18), inset 0 -1px 0 rgb(255 255 255 / 0.7);
    --grain-op: 0.038;
    --card-stock: linear-gradient(158deg, #fffdf7 0%, #faf7ee 52%, #f2eee2 100%);
  }
}

/* Light: explicit toggle. Must repeat in full — CSS has no @extend. */
:root[data-theme='light'] {
  color-scheme: light dark;
  --bg-sunken: #e8e3d6; --bg-base: #f4f1e9; --bg-inset: #ede9de;
  --bg-raised: #fff; --bg-overlay: #fff; --bg-hover: #f0ece1;
  --scrim: rgb(28 25 18 / 0.46);
  --text-1: #16191f; --text-2: #4a5260; --text-3: #57616f; --text-off: #98a0ac;
  --text-on-accent: #16191f; --text-on-danger: #fff;
  --line: #ddd7c8; --line-2: #c9c2b0; --line-strong: #807965;
  --accent: #b4870f; --accent-hi: #c9971a; --accent-lo: #8f6b08;
  --danger: #a82a20; --danger-fill: #b7332a; --danger-fill-hi: #9c2a21;
  --success: #046a50; --success-fill: #046a50;
  --info: #10559c; --warn: #8a6a00; --focus: #0a59b2;
  --team-0: #57616f; --team-1: #0a59b2; --team-2: #b03a0a;
  --team-3: #046a50; --team-4: #6e5f00; --team-5: #9333a8; --team-6: #0c6273;
  /* Same correction as the media-query copy above: white rim underneath. */
  --e-1: 0 -1px 0 rgb(255 255 255 / 0.9) inset, 0 1px 0 rgb(28 25 18 / 0.05) inset,
         0 1px 2px rgb(60 52 32 / 0.14);
  --e-2: 0 -1px 0 rgb(255 255 255 / 0.9) inset, 0 1px 0 rgb(28 25 18 / 0.06) inset,
         0 1px 1px rgb(60 52 32 / 0.10), 0 6px 16px -6px rgb(60 52 32 / 0.16);
  --e-3: 0 -1px 0 rgb(255 255 255 / 0.9) inset,
         0 1px 1px rgb(60 52 32 / 0.12), 0 10px 24px -8px rgb(60 52 32 / 0.24);
  --e-4: 0 2px 3px rgb(60 52 32 / 0.14), 0 22px 52px -14px rgb(60 52 32 / 0.3);
  --press: inset 0 2px 5px rgb(60 52 32 / 0.22);
  --deboss: inset 0 1px 1px rgb(60 52 32 / 0.18), inset 0 -1px 0 rgb(255 255 255 / 0.7);
  --grain-op: 0.038;
  --card-stock: linear-gradient(158deg, #fffdf7 0%, #faf7ee 52%, #f2eee2 100%);
}

/* Dark toggle under a light OS: bare :root already holds the dark values. */
:root[data-theme='dark'] { color-scheme: dark light; }

@media (prefers-contrast: more) {
  :root { --text-2: #d6dce6; --text-3: #aeb6c4; --line: #3a4252; --line-2: #4e5768; }
}

/* Per-game identity: exactly two knobs — an accent and an emblem. Everything
   else is shared, which is what keeps N games at one stylesheet. */
[data-game='oddoneout'] { --game-accent: #45d6e8; }
[data-game='spectrum']  { --game-accent: #e48fd6; }
[data-game='nightfall'] { --game-accent: #7c6bff; }
[data-game='cheat']     { --game-accent: #3fd98b; }
[data-game='holdem']    { --game-accent: #2fb87a; }
[data-game='pokerlab']  { --game-accent: #5ba8ff; }
[data-game='council']   { --game-accent: #e8b84b; }
[data-game='sabotage']  { --game-accent: #ff7a45; }

/* ----------------------------------------------------------------- reset -- */
*, *::before, *::after { box-sizing: border-box; }

html {
  background: var(--bg-base);
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  height: 100vh;   /* legacy fallback */
  height: 100dvh;  /* the shell never scrolls, so dvh == svh permanently */
  overflow: hidden;
  overscroll-behavior: none; /* no pull-to-refresh, no rubber-band chaining */
  background: var(--bg-base);
  color: var(--text-1);
  font-family: var(--font-body);
  font-size: var(--t-md);
  line-height: 1.5;
  font-synthesis-weight: none; /* stop fake-bold mush on Android */
  -webkit-tap-highlight-color: transparent;
  /* manipulation kills double-tap zoom but KEEPS pinch-zoom (WCAG 1.4.4) */
  touch-action: manipulation;
}

/* Load-bearing. Without an explicit height here, `.screen { height: 100% }`
   resolves against an auto-height parent and collapses to content height, so
   the .flow scroll container ends up exactly as tall as its content and never
   scrolls — while body's overflow:hidden silently clips everything past the
   fold. That made every screen taller than the viewport unreachable. */
#app { height: 100%; }

button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; padding: 0; cursor: pointer; touch-action: manipulation; }
/* Never below 16px computed or iOS Safari zooms the viewport on focus. */
input, select, textarea { font-size: max(16px, 1rem); }

:focus { outline: none; }
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: inherit;
}

/* Serif carries the voice; UI chrome stays on the system sans. 700 not 800:
   Georgia and Noto Serif stop at bold, and font-synthesis-weight is off. */
h1, h2, h3 { font-family: var(--font-display); font-weight: 700; margin: 0; text-wrap: balance; }
.bar h1, .bar h2, .bar h3, .btn, .chip, .label, .seg button {
  font-family: var(--font-ui);
}
p { margin: 0; text-wrap: pretty; }
ul, ol { list-style: none; margin: 0; padding: 0; }

.sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.hidden { display: none !important; }

/* ------------------------------------------------------------ typography -- */
.t-sm { font-size: var(--t-sm); }
.t-lg { font-size: var(--t-lg); line-height: 1.28; letter-spacing: -0.005em; }
.t-xl { font-size: var(--t-xl); line-height: 1.18; letter-spacing: -0.012em; }
/* Capped at -0.02em: iOS switches SF to its Display optical variant above
   20pt and tightens again on its own, so asking for more reads as cramped. */
.t-2xl { font-size: var(--t-2xl); line-height: 1.06; letter-spacing: -0.018em; }
.t-3xl { font-size: var(--t-3xl); line-height: 1; letter-spacing: -0.02em; }

.label {
  font-size: var(--t-2xs); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.14em; color: var(--text-3);
  /* 'case' raises punctuation to cap height in all-caps runs. Free on SF. */
  font-feature-settings: 'case' 1;
}

/* Trims half-leading so text sits on its cap-height box. This is the fix for
   "my padding is 12px but it looks like 10 top / 14 bottom". */
@supports (text-box-trim: trim-both) {
  .btn, .chip, .label, .pcard__rank, .pokerpot__amt, .raise__amt, .cheatpile__n,
  .t-lg, .t-xl, .t-2xl, .t-3xl {
    text-box-trim: trim-both;
    text-box-edge: cap alphabetic;
  }
}
/* Mandatory on anything that counts down or changes in place — without it a
   "1" is narrower than a "0" and the timer visibly jitters every second. */
.num { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1, 'lnum' 1; }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.dim { color: var(--text-2); }
.muted { color: var(--text-3); }
.center { text-align: center; }

/* ----------------------------------------------------------------- shell -- */
/* A gradient mesh keyed to the game's accent, not a flat fill. Three large
   low-opacity radial gradients over the base, blended — one pseudo-element,
   no animation, and the single largest change to "does this look designed".
   The grain on top is what stops the gradients banding on OLED. */
.screen::before {
  content: ''; position: fixed; inset: 0; z-index: -2; pointer-events: none;
  background:
    radial-gradient(120% 70% at 12% -12%, rgb(from var(--game-accent) r g b / 0.16), transparent 62%),
    radial-gradient(95% 62% at 106% 6%, rgb(from var(--game-accent) r g b / 0.10), transparent 60%),
    radial-gradient(150% 95% at 50% 120%, var(--bg-sunken), transparent 72%);
}
.screen::after {
  content: ''; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background-image: var(--grain); opacity: var(--grain-op);
  mix-blend-mode: soft-light;
}
@supports not (color: rgb(from red r g b)) {
  .screen::before { background: radial-gradient(150% 95% at 50% 120%, var(--bg-sunken), transparent 72%); }
}

.screen {
  display: grid;
  /* minmax(0,1fr) is load-bearing: plain 1fr has an auto min-size and refuses
     to shrink, which pushes the action bar off-screen on long content. */
  grid-template-rows: auto minmax(0, 1fr) auto;
  height: 100%;
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

.bar {
  display: flex; align-items: center; gap: var(--sp-3);
  padding-inline: var(--sp-4); background: var(--bg-raised); z-index: 2;
}
.bar--top {
  min-height: var(--tap);
  padding-top: max(env(safe-area-inset-top), var(--sp-1));
  padding-bottom: var(--sp-1);
  border-bottom: var(--hair) solid var(--line);
  box-shadow: var(--e-2);
  justify-content: space-between;
}
.bar--bottom {
  flex-direction: column; align-items: stretch; gap: var(--sp-2);
  padding-top: var(--sp-3);
  padding-bottom: max(var(--sp-3), env(safe-area-inset-bottom));
  border-top: var(--hair) solid var(--line);
  box-shadow: 0 -8px 24px -12px rgb(0 0 0 / 0.7);
  /* lifted above the iOS keyboard by --kb, synced from VisualViewport */
  transform: translateY(calc(-1 * var(--kb)));
  transition: transform var(--d-2) var(--ease-out);
}

.flow {
  overflow-y: auto;
  overscroll-behavior: contain; /* scroll never chains to the page */
  padding: var(--sp-5) var(--sp-4) var(--sp-6);
  scrollbar-width: thin;
}
.shell { width: 100%; max-width: var(--shell); margin-inline: auto; }
@media (min-width: 600px) { .shell { max-width: var(--shell-wide); } }

.stack { display: flex; flex-direction: column; gap: var(--sp-4); }
.stack--tight { gap: var(--sp-2); }
.row { display: flex; align-items: center; gap: var(--sp-3); }
/* Two equal choices side by side — used where neither is the safe default and
   giving one of them more width would be a thumb on the scale. */
.row--split { width: 100%; }
.row--split > * { flex: 1 1 0; min-width: 0; }
.grow { flex: 1; min-width: 0; }

/* --------------------------------------------------------------- buttons -- */
.btn {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap); padding: 0 var(--sp-5);
  border-radius: var(--r-4);
  font-weight: 700; font-size: var(--t-md); letter-spacing: 0.01em;
  user-select: none; -webkit-user-select: none;
  transition: transform var(--d-1) var(--ease-out), box-shadow var(--d-1) var(--ease-out),
    background-color var(--d-2) var(--ease-out), opacity var(--d-2) linear;
}
.btn--block { display: flex; width: 100%; min-height: var(--tap-lg); }
/* The primary action wears the game's accent, so a poker screen is not gold
   with one green number on it. Falls back to the flat house gold wherever
   relative colour is unavailable. */
.btn--primary {
  background: linear-gradient(180deg, var(--accent-hi), var(--accent));
  color: var(--text-on-accent);
  box-shadow: var(--e-2), inset 0 1px 0 rgb(255 255 255 / 0.35);
}
@supports (color: oklch(from red l c h)) {
  .btn--primary {
    background: linear-gradient(180deg in oklab,
      oklch(from var(--game-accent) calc(l + 0.06) c h),
      var(--game-accent) 58%,
      oklch(from var(--game-accent) calc(l - 0.045) calc(c * 1.05) h));
    /* A tinted dark rather than pure black: the single most recognisable
       "expensive" cue available, and it costs one line. */
    color: oklch(from var(--game-accent) 0.2 calc(c * 0.3) h);
    box-shadow:
      inset 0 1px 0 oklch(from var(--game-accent) 0.96 calc(c * 0.4) h / 0.5),
      inset 0 -1px 0 oklch(from var(--game-accent) 0.32 c h / 0.5),
      0 1px 1px oklch(from var(--game-accent) 0.22 c h / 0.4),
      0 5px 14px -4px oklch(from var(--game-accent) 0.48 c h / 0.34);
  }
  .btn--primary:active:not([disabled]) {
    background: oklch(from var(--game-accent) calc(l - 0.06) c h);
  }
}
.btn--secondary {
  background: var(--bg-raised); color: var(--text-1);
  box-shadow: inset 0 0 0 var(--hair) var(--line-strong), var(--e-1);
}
.btn--danger {
  background: linear-gradient(180deg, var(--danger-fill-hi), var(--danger-fill));
  color: var(--text-on-danger);
  box-shadow: var(--e-2), inset 0 1px 0 rgb(255 255 255 / 0.18);
}
.btn--ghost { background: none; color: var(--text-2); box-shadow: none; min-height: var(--tap); }
.btn:active:not([disabled]) { transform: translateY(1px) scale(0.985); box-shadow: var(--press); }
.btn--primary:active:not([disabled]) { background: var(--accent-lo); }
/* Asymmetric press: instant on the way down, eased on the way back. */
.btn { transition: transform 180ms var(--ease-out), box-shadow 180ms var(--ease-out); }
.btn:active:not([disabled]) {
  transition-duration: 55ms;
  transform: scale(calc(1 - 0.028 * var(--motion))) translateY(calc(1px * var(--motion)));
}
.btn--danger:active:not([disabled]) { background: var(--danger-fill); }
/* Disabled loses its rim-light and gains an inset — a SHAPE cue, not just alpha */
.btn[disabled], .btn[aria-disabled='true'] {
  opacity: 0.42; pointer-events: none;
  background: var(--bg-raised); color: var(--text-2); box-shadow: var(--deboss);
}

.iconbtn {
  display: grid; place-items: center;
  width: var(--tap); height: var(--tap); border-radius: var(--r-pill);
  color: var(--text-2);
}
.iconbtn:active { background: var(--bg-hover); }

/* ----------------------------------------------------------------- icons -- */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.ico {
  width: 20px; height: 20px; flex: 0 0 auto; display: block;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.ico--sm { width: 16px; height: 16px; stroke-width: 1.75; }
.ico--lg { width: 26px; height: 26px; }
.ico--xl { width: 40px; height: 40px; stroke-width: 1.6; }
.ico--solid { fill: currentColor; stroke: none; }

/* ----------------------------------------------------------------- cards -- */
.card {
  position: relative; border-radius: var(--r-4); padding: var(--sp-4);
  background: var(--card-stock);
  box-shadow: var(--e-2), inset 0 0 0 var(--hair) var(--line-2),
    inset 0 1px 0 rgb(255 255 255 / 0.055);
  overflow: hidden;
}
.card--flat { background: var(--bg-raised); box-shadow: inset 0 0 0 var(--hair) var(--line); }
.grain::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background-image: var(--grain); opacity: var(--grain-op);
  mix-blend-mode: overlay; border-radius: inherit;
}
.well { background: var(--bg-inset); box-shadow: var(--deboss); border-radius: var(--r-3); }

/* ------------------------------------------------------------- room code -- */
.roomcode {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
  width: 100%; padding: var(--sp-4); border-radius: var(--r-4);
  background: var(--card-stock);
  box-shadow: var(--e-2), inset 0 0 0 var(--hair) var(--line-2);
}
.roomcode__cells { display: flex; gap: var(--sp-2); }
.roomcode__cells i {
  font-family: var(--font-mono); font-style: normal; font-weight: 700;
  font-size: var(--t-4xl); line-height: 1;
  min-width: 0.74em; padding: var(--sp-2) var(--sp-1);
  border-radius: var(--r-2); text-align: center;
  background: var(--bg-inset); box-shadow: var(--deboss);
  color: var(--accent); text-shadow: 0 2px 0 rgb(0 0 0 / 0.55);
}
.roomcode__hint { display: inline-flex; align-items: center; gap: 6px; }
.roomcode.is-copied .roomcode__hint { color: var(--success); }

/* QR needs its white plate in every theme — never invert a QR code. */
.qr {
  width: min(58vw, 220px); margin-inline: auto; padding: var(--sp-3);
  background: #fff; border-radius: var(--r-3); box-shadow: var(--e-2);
}
.qr svg { display: block; width: 100%; height: auto; color: #0b0e14; shape-rendering: crispEdges; }
.qr rect { fill: currentColor; }

/* ------------------------------------------------------------- role card -- */
.rolecard { perspective: 1400px; width: min(76vw, 290px); margin-inline: auto; }
.rolecard__btn { display: block; width: 100%; aspect-ratio: 5 / 7; }
.rolecard__inner {
  position: relative; width: 100%; height: 100%;
  transform-style: preserve-3d;
  transition: transform var(--d-5) var(--ease-snap);
  will-change: transform;
}
/* Hold-to-peek needs no JS at all. The exposure window is user-controlled and
   physically self-limiting, and the holding hand shades the screen. */
.rolecard__btn:active .rolecard__inner,
.rolecard.is-revealed .rolecard__inner { transform: rotateY(180deg); }

.rolecard__face {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-3); padding: var(--sp-5);
  border-radius: var(--r-5);
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
  box-shadow: var(--e-4); overflow: hidden; text-align: center;
}
.rolecard__back {
  background: var(--card-stock);
  box-shadow: var(--e-4), inset 0 0 0 var(--hair) var(--line-2);
}
.rolecard__back::before {
  content: ''; position: absolute; inset: 10px; border-radius: calc(var(--r-5) - 6px);
  box-shadow: inset 0 0 0 var(--hair) rgb(255 255 255 / 0.09), inset 0 1px 0 rgb(0 0 0 / 0.5);
  pointer-events: none;
}
.rolecard__back::after, .rolecard__front::after {
  content: ''; position: absolute; inset: 0; background-image: var(--grain);
  opacity: var(--grain-op); mix-blend-mode: overlay; pointer-events: none;
}
.rolecard__emblem { width: 64px; height: 64px; color: var(--game-accent); opacity: 0.5; }
.rolecard__front {
  transform: rotateY(180deg);
  background: radial-gradient(120% 90% at 50% 0%, var(--tint, transparent), transparent 70%),
    var(--card-stock);
  box-shadow: var(--e-4), inset 0 0 0 2px var(--tc, var(--game-accent));
}
.rolecard__glyph { width: 40px; height: 40px; color: var(--tc, var(--game-accent)); }
.rolecard__name {
  margin: 0; font-size: var(--t-2xl); line-height: 1.04; letter-spacing: -0.012em;
  color: var(--text-1);
}
.rolecard__obj { font-size: var(--t-sm); color: var(--text-2); max-width: 24ch; }

.holdring { position: absolute; bottom: 16px; width: 34px; height: 34px; transform: rotate(-90deg); }
.holdring circle { fill: none; stroke-width: 3; }
.holdring__t { stroke: rgb(255 255 255 / 0.12); }
.holdring__p {
  stroke: var(--game-accent); stroke-linecap: round;
  stroke-dasharray: 125.66; stroke-dashoffset: 125.66;
}
.rolecard__btn:active .holdring__p { animation: hold var(--d-5) linear forwards; }
@keyframes hold { to { stroke-dashoffset: 0; } }

/* ---------------------------------------------------------- player tiles -- */
.plist { display: flex; flex-direction: column; gap: var(--sp-2); }
/* A tappable tile is a <button> inside its <li>, so the list keeps its
   semantics; display:contents makes the button the flex item as before. */
.ptile__slot { display: contents; }
.ptile {
  position: relative; display: flex; align-items: center; gap: var(--sp-3);
  min-height: 64px; padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-4);
  border-radius: var(--r-3); width: 100%; text-align: left;
  background: var(--bg-raised);
  box-shadow: inset 0 0 0 var(--hair) var(--line);
  /* Scope invalidation to the row: these lists re-render on every message. */
  contain: layout paint style;
  transition: background-color var(--d-2) var(--ease-out), opacity var(--d-3) var(--ease-out);
}
.ptile__av {
  flex: 0 0 auto; width: 48px; height: 48px; border-radius: var(--r-pill);
  overflow: hidden; box-shadow: var(--deboss), 0 0 0 var(--hair) var(--line-2);
}
.ptile__av svg { display: block; width: 100%; height: 100%; }
.ptile__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.ptile__name {
  font-size: var(--t-lg); font-weight: 600; line-height: 1.15;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ptile__sub { line-height: 1.2; }
.ptile__badges { display: flex; gap: var(--sp-1); flex: 0 0 auto; }

/* Selectable rows: two-stage select -> commit. A misfire is caught by reading
   the button label, not by dismissing a dialog. */
.ptile--pick { cursor: pointer; }
.plist.is-picking .ptile--pick { opacity: 0.45; }
.plist.is-picking .ptile--pick.is-selected { opacity: 1; }
.ptile--pick.is-selected {
  background: var(--bg-hover);
  box-shadow: inset 0 0 0 3px var(--game-accent);
}
.ptile--pick:active { transform: scale(0.99); }
.ptile[disabled] { opacity: 0.4; pointer-events: none; }

/* Current turn: rail + chevron + label. Three cues, never colour alone. */
.ptile[data-state='turn'] { background: var(--bg-hover); }
.ptile[data-state='turn']::before {
  content: ''; position: absolute; left: 0; top: 8px; bottom: 8px; width: 3px;
  border-radius: 0 3px 3px 0; background: var(--game-accent);
}
/* Eliminated: opacity + grayscale + halftone + strike-through + badge. */
.ptile[data-state='dead'] { opacity: 0.55; }
.ptile[data-state='dead'] .ptile__name { text-decoration: line-through; text-decoration-thickness: 2px; }
.ptile[data-state='dead'] .ptile__av { filter: grayscale(1) contrast(0.85); }
.ptile[data-state='dead']::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: radial-gradient(circle at 2px 2px, rgb(0 0 0 / 0.42) 1.2px, transparent 1.3px) 0 0 / 5px 5px;
}
.ptile.is-offline .ptile__av { opacity: 0.5; }

.bdg {
  display: grid; place-items: center; width: 24px; height: 24px;
  border-radius: var(--r-pill); background: var(--bg-overlay); color: var(--text-2);
  box-shadow: inset 0 0 0 var(--hair) var(--line-2);
}
.bdg .ico { width: 14px; height: 14px; }
.bdg--host { color: var(--accent); box-shadow: inset 0 0 0 var(--hair) var(--accent); }
.bdg--ready { color: var(--success); box-shadow: inset 0 0 0 var(--hair) var(--success); }
.bdg--voted { color: var(--info); }
.bdg--dead { color: var(--danger); }
.bdg--off {
  color: var(--text-3);
  background: repeating-linear-gradient(45deg, var(--bg-overlay) 0 3px, var(--bg-inset) 3px 6px);
}

/* ----------------------------------------------------------------- votes -- */
.votepair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.vote {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  min-height: 76px; border-radius: var(--r-4);
  background: var(--bg-raised); color: var(--text-2);
  box-shadow: inset 0 0 0 2px var(--line-strong), var(--e-1);
  font-weight: 700; font-size: var(--t-sm); letter-spacing: 0.02em;
  transition: transform var(--d-1) var(--ease-out), box-shadow var(--d-2) var(--ease-out),
    background-color var(--d-2) var(--ease-out), color var(--d-2) var(--ease-out);
}
.vote .ico { width: 26px; height: 26px; }
.vote:active { transform: scale(0.97); }
.vote--yes[aria-checked='true'] {
  background: var(--success-fill); color: #f2fbf6;
  box-shadow: inset 0 0 0 2px var(--success), var(--press);
}
.vote--no[aria-checked='true'] {
  background: var(--danger-fill); color: var(--text-on-danger);
  box-shadow: inset 0 0 0 2px var(--danger), var(--press);
}

.tally {
  display: flex; height: 14px; border-radius: var(--r-pill); overflow: hidden;
  background: var(--bg-inset); box-shadow: var(--deboss);
}
.tally__seg { flex: var(--n) 0 0; min-width: 0; transition: flex-grow var(--d-4) var(--ease-out); }
.tally__seg--yes {
  background: var(--success-fill);
  background-image: radial-gradient(circle at 3px 3px, rgb(0 0 0 / 0.3) 1.3px, transparent 1.4px);
  background-size: 6px 6px;
}
.tally__seg--no {
  background: var(--danger-fill);
  background-image: repeating-linear-gradient(45deg, transparent 0 4px, rgb(0 0 0 / 0.28) 4px 8px);
}
.tally__seg--non { background: var(--bg-hover); }

/* ----------------------------------------------------------------- timer -- */
.timer { position: relative; display: grid; place-items: center; width: 44px; height: 44px; }
.timer__ring { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.timer__ring circle { fill: none; stroke-width: 6; }
.timer__trk { stroke: var(--bg-inset); }
.timer__bar {
  stroke: var(--game-accent); stroke-linecap: round;
  stroke-dasharray: 276.46;
  stroke-dashoffset: calc(276.46 * (1 - var(--p, 1)));
  transition: stroke-dashoffset 1s linear, stroke var(--d-3) var(--ease-out);
}
.timer__n { font-family: var(--font-mono); font-size: var(--t-xs); font-weight: 700; }
.timer--hero { width: 128px; height: 128px; }
.timer--hero .timer__n { font-size: var(--t-xl); }
.timer.is-urgent { --game-accent: var(--danger); }
.timer.is-urgent .timer__n { color: var(--danger); }
.timer.is-urgent::after {
  content: ''; position: absolute; inset: -6px; border-radius: 50%;
  box-shadow: 0 0 0 2px var(--danger); opacity: 0;
  animation: urgent 1s var(--ease-out) infinite;
}
@keyframes urgent {
  0% { transform: scale(0.94); opacity: 0.85; }
  100% { transform: scale(1.14); opacity: 0; }
}

/* ----------------------------------------------------------------- pills -- */
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 28px; padding: 0 12px; border-radius: var(--r-pill);
  font-size: var(--t-2xs); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.1em; background: var(--bg-raised); color: var(--text-2);
  box-shadow: inset 0 0 0 var(--hair) var(--line-2);
}
.pill i { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.pill--live { color: var(--success); }
.pill--warn { color: var(--warn); }
.pill--warn i { animation: breathe 0.9s var(--ease-both) infinite; }
.pill--dead {
  color: var(--text-3);
  background: repeating-linear-gradient(45deg, var(--bg-raised) 0 4px, var(--bg-inset) 4px 8px);
}
.pill--dead i { border-radius: 1px; } /* shape changes too, not just colour */
@keyframes breathe { 50% { opacity: 0.28; transform: scale(0.72); } }

.chip {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: 34px; padding: 0 var(--sp-3); border-radius: var(--r-pill);
  background: var(--bg-overlay); box-shadow: inset 0 0 0 var(--hair) var(--line-2);
  font-size: var(--t-xs); font-weight: 600;
}
/* A chip that is actually tappable needs a real target (WCAG 2.5.5 / HIG 44pt),
   even though a chip used as a static label does not. */
button.chip { min-height: var(--tap); padding-inline: var(--sp-4); }

/* Hold-to-peek role chip. Masked by default so a glance from across the table
   gets nothing; the label text is swapped, not merely hidden, so it is absent
   from the DOM at rest. */
.chip--peek { color: var(--text-3); }
.chip--peek .chip__masked { font-variant: small-caps; letter-spacing: 0.06em; }
.chip--peek::before {
  content: ''; width: 9px; height: 9px; border-radius: 2px;
  background: currentColor; opacity: 0.5;
}
.chip--peek.is-peeking { box-shadow: inset 0 0 0 2px currentColor; }
.chip--peek.is-peeking::before { opacity: 1; border-radius: 50%; }

/* --------------------------------------------------------------- banners -- */
.banner {
  padding: var(--sp-3) var(--sp-4); border-radius: var(--r-3);
  background: var(--bg-overlay); box-shadow: inset 0 0 0 var(--hair) var(--line-2);
  font-size: var(--t-sm);
}
.banner--accent { box-shadow: inset 0 0 0 var(--hair) var(--game-accent); }
.banner--danger { box-shadow: inset 0 0 0 var(--hair) var(--danger); }
.banner--good { box-shadow: inset 0 0 0 var(--hair) var(--success); }

.toast {
  position: fixed; left: 50%; translate: -50% 0;
  top: calc(env(safe-area-inset-top) + var(--tap) + var(--sp-2));
  z-index: 60; width: min(100% - var(--sp-6), var(--shell));
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4); border-radius: var(--r-3);
  background: var(--bg-overlay); color: var(--text-1);
  box-shadow: var(--e-3), inset 0 0 0 var(--hair) var(--line-2);
  overflow: hidden; animation: toast-in var(--d-3) var(--ease-out) both;
}
@keyframes toast-in { from { opacity: 0; transform: translateY(-12px) scale(0.98); } }

/* --------------------------------------------------------------- overlay -- */
/* Full-screen attention takeover. Breathe, never flash: WCAG 2.3.1 forbids
   >3 flashes/sec, and a flash reads as an error rather than a summons. */
.turnwash {
  position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background: radial-gradient(80% 50% at 50% 0%, var(--game-accent), transparent 70%);
  opacity: 0.14; animation: wash 2.4s var(--ease-both) infinite;
}
@keyframes wash { 50% { opacity: 0.05; } }

/* Scope the layout to [open]: a bare `display:flex` on the element would beat
   the UA's `dialog:not([open]) { display: none }` and leave the sheet
   permanently stuck to the bottom of every screen. */
.sheet {
  width: 100%; max-width: var(--shell); margin: auto auto 0;
  max-height: 86dvh; padding: 0; border: 0;
  border-radius: var(--r-5) var(--r-5) 0 0;
  background: var(--bg-overlay); color: var(--text-1);
  box-shadow: var(--e-4);
}
.sheet[open] { display: flex; flex-direction: column; }
.sheet::backdrop { background: var(--scrim); }
.sheet__grab {
  width: 40px; height: 4px; border-radius: 2px; background: var(--line-strong);
  margin: var(--sp-3) auto var(--sp-1); flex: 0 0 auto;
}
.sheet__hd {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4); border-bottom: var(--hair) solid var(--line); flex: 0 0 auto;
}
.sheet__bd {
  overflow-y: auto; overscroll-behavior: contain;
  padding: var(--sp-4) var(--sp-4) max(var(--sp-6), env(safe-area-inset-bottom));
  display: flex; flex-direction: column; gap: var(--sp-4);
}

/* ---------------------------------------------------------------- inputs -- */
.field { display: flex; flex-direction: column; gap: var(--sp-2); }
.input {
  width: 100%; min-height: var(--tap-lg); padding: 0 var(--sp-4);
  border: 0; border-radius: var(--r-3);
  background: var(--bg-inset); color: var(--text-1);
  box-shadow: var(--deboss), inset 0 0 0 var(--hair) var(--line-2);
}
/* One input, not four boxes: four boxes break paste, confuse screen readers,
   and misbehave with Android IME. */
.input--code {
  text-transform: uppercase; letter-spacing: 0.35em; text-indent: 0.35em;
  font-family: var(--font-mono); font-size: 2rem; font-weight: 700;
  text-align: center; min-height: 72px;
}
.input--code::placeholder { letter-spacing: 0.35em; color: var(--text-off); }

/* ----------------------------------------------------------------- games -- */
.gamegrid { display: flex; flex-direction: column; gap: var(--sp-3); }
.gametile {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-4); border-radius: var(--r-4); text-align: left; width: 100%;
  background: var(--card-stock);
  box-shadow: var(--e-2), inset 0 0 0 var(--hair) var(--line-2);
  transition: transform var(--d-1) var(--ease-out), box-shadow var(--d-2) var(--ease-out);
}
.gametile:active { transform: translateY(1px) scale(0.99); box-shadow: var(--press); }
.gametile.is-selected { box-shadow: var(--e-2), inset 0 0 0 2px var(--game-accent); }
.gametile { align-items: flex-start; }
.gametile__emblem { width: 40px; height: 40px; flex: 0 0 auto; color: var(--game-accent); margin-top: 2px; }
/* These are spans inside a span, so they need to be told to stack. */
.gametile__name { display: block; font-size: var(--t-lg); font-weight: 700; line-height: 1.15; }
.gametile__meta { display: block; font-size: var(--t-xs); color: var(--text-3); margin-top: 2px; }
/* The recognisable name. Deliberately high-contrast and directly under the
   title -- it is what most people scan for. */
/* One step below the game name, accent-coloured: this is the line most people
   actually scan for, so it must not be the smallest text on the tile. */
.gametile__familiar {
  display: block; margin-top: 3px;
  font-size: var(--t-sm); font-weight: 700; color: var(--game-accent);
}

/* Join collapses to a single row so the game list clears the fold. */
/* Tighter rhythm on the home screen only: every 16px here pushes the game
   list further below the fold on a small phone. */
.home.stack { gap: var(--sp-3); }
.home .flow { padding-top: var(--sp-4); }
.joinrow { display: flex; gap: var(--sp-2); align-items: stretch; }
.joinrow .input--code {
  flex: 1; min-height: var(--tap); font-size: 1.35rem; letter-spacing: 0.2em;
  text-indent: 0.2em; min-width: 0;
}
.joinrow .btn { flex: 0 0 auto; padding-inline: var(--sp-4); }

/* The start bar carries the name field and the button together, so the field is
   never scrolled away from the action that needs it. */
.bar--bottom .input { margin-bottom: var(--sp-2); }

.secret {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
  padding: var(--sp-5) var(--sp-4); border-radius: var(--r-4);
  background: var(--bg-inset); box-shadow: var(--deboss);
  text-align: center;
}
.secret__value { font-size: var(--t-2xl); font-weight: 800; line-height: 1.05; color: var(--game-accent); }

.optionrow {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  min-height: var(--tap); padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-3); background: var(--bg-raised);
  box-shadow: inset 0 0 0 var(--hair) var(--line);
}
.seg { display: flex; gap: 2px; padding: 3px; border-radius: var(--r-pill); background: var(--bg-inset); }
.seg button {
  min-height: 38px; padding: 0 var(--sp-3); border-radius: var(--r-pill);
  font-size: var(--t-xs); font-weight: 700; color: var(--text-2);
}
.seg button[aria-pressed='true'] { background: var(--bg-hover); color: var(--text-1); box-shadow: var(--e-1); }

/* ---------------------------------------------------------- playing cards - */
/* Cards are typography, not artwork: no images, no web fonts, nothing to load.
   CHILD metrics are a fraction of the card's own RENDERED width via container
   query units, not of the --pc-w we asked for: on the board the cards
   flex-shrink below --pc-w, and a calc() off the requested width leaves
   oversized glyphs on an undersized card.
   Properties set on the CARD ITSELF must use calc(var(--pc-w) * n) instead.
   Container query units on the container resolve against the nearest ANCESTOR
   container — the viewport, here — and are dropped outright for anything that
   affects the container's own size. Measured: `inset 0 0 0 5.5cqw` on a 46px
   card produced a 21px frame that swallowed the whole face, and `padding: 7cqw`
   was silently discarded. */
.pcard {
  --pc-w: 46px;
  container-type: inline-size;
  position: relative; display: inline-flex; flex-direction: column;
  align-items: center; justify-content: center;
  width: var(--pc-w); height: calc(var(--pc-w) * 1.4); /* 2.5 x 3.5in */
  /* A real card's corner is 1/8in on a 2.5in card — 5% of its width. 13% read
     as an app tile; 7% reads as a card without looking laser-cut.
     NOT in cqw: container query units used ON the container resolve against
     the nearest ANCESTOR container, not the element itself, so 7cqw here was
     7% of the viewport and every card came out a pill. */
  border-radius: calc(var(--pc-w) * 0.07);
  background: linear-gradient(177deg, #fffdf7 0%, #f6f3ea 55%, #efeade 100%);
  color: #14181f;
  box-shadow:
    inset 0 0 0 var(--hair) rgb(20 24 31 / 0.3),
    inset 0 1px 0 rgb(255 255 255 / 0.85),
    0 1px 1px rgb(20 24 31 / 0.4);
  font-variant-numeric: tabular-nums; line-height: 1; user-select: none;
  /* Rank and suit are trimmed to their ink boxes, so without an explicit gap
     a J and a heart butt into each other and read as one shape. */
  gap: calc(var(--pc-w) * 0.05);
}
.pcard--xs { --pc-w: 22px; }
.pcard--sm { --pc-w: 32px; }
.pcard--md { --pc-w: 46px; }
.pcard--lg { --pc-w: 74px; }

/* Small: two glyphs, both as large as they will go. Below ~13px a rank is not
   readable at arm's length, which is the whole job at this size. */
.pcard__rank { font-size: 56cqw; font-weight: 800; letter-spacing: -0.03em; }
.pcard__suit { font-size: 40cqw; line-height: 1; }

/* Large: a corner index plus one big centre suit. Real pip layouts are the
   traditional answer and the wrong one here — seven diamonds at 74px are 11px
   each, which is noise. The index carries the information; the watermark is
   atmosphere, and it is what makes the card read as a card at a glance.
   Keyed on the size class, NOT on @container: a container query can only ever
   style DESCENDANTS of the container, so `@container { .pcard { padding } }`
   silently never applies to the card it is written for. The glyphs inside stay
   in cqw so they still track the card if it flex-shrinks. */
.pcard--lg {
  align-items: flex-start; justify-content: flex-start;
  padding: calc(var(--pc-w) * 0.07) calc(var(--pc-w) * 0.08);
}
/* cqw resolves against the container's CONTENT box, so adding padding above
   shrank these. Sized so the big card's index is genuinely the largest rank on
   screen — it was coming out smaller than the board's. */
.pcard--lg .pcard__rank { font-size: 44cqw; letter-spacing: -0.04em; position: relative; z-index: 1; }
.pcard--lg .pcard__suit { font-size: 30cqw; position: relative; z-index: 1; }
/* The centre suit is a watermark, not a second index. At full strength it
   fights the corner for the same 74px and neither wins; sat behind the index
   it just reads as two spades of different sizes. Bottom-right keeps it clear. */
.pcard--lg::before {
  content: attr(data-suit);
  position: absolute; inset: auto 0 0 auto; z-index: 0;
  width: 80cqw; height: 80cqw;
  display: grid; place-items: center;
  font-size: 66cqw; line-height: 1; opacity: 0.12; pointer-events: none;
}

.pcard--red { color: #c62f28; }
.pcard--black { color: #14181f; }
.pcard.is-dim { opacity: 0.42; }

/* The winning five, marked with a ring rather than a colour — colour alone
   would be invisible to a third of the table's eyes on a phone in a bar. The
   lift and the sheen are the reward on top of that, not instead of it. */
.pcard.is-best {
  box-shadow:
    inset 0 0 0 var(--hair) rgb(20 24 31 / 0.3),
    0 0 0 2px var(--success),
    0 6px 14px -4px rgb(20 24 31 / 0.5);
  transform: translateY(calc(-6px * var(--motion)));
  transition: transform 220ms var(--spring-snappy);
}

/* A single sweep of light across the winning cards. @property is what makes a
   custom property animatable at all; without the registration this is a step
   function from one value to the other. */
@property --sheen { syntax: '<percentage>'; initial-value: -35%; inherits: false; }
.pcard.is-best::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(105deg,
    transparent var(--sheen),
    rgb(255 255 255 / 0.6) calc(var(--sheen) + 11%),
    transparent calc(var(--sheen) + 24%));
  animation: sheen 820ms var(--ease-out) both;
}
@keyframes sheen { to { --sheen: 115%; } }

/* Card backs. The single biggest "this is a real card" cue is the frame of
   stock colour around the pattern — without it a back reads as a swatch.
   background-color and background-image are set separately on purpose:
   `background:` is a shorthand, so a later background-image wipes the colour
   out and leaves a back you can see straight through. */
.pcard--back {
  --bk: #2b3350;
  color: transparent;
  background-color: var(--bk);
  background-image:
    repeating-conic-gradient(from 45deg, #39426a 0deg 90deg, var(--bk) 90deg 180deg),
    radial-gradient(closest-side, #454f7d 0 38%, transparent 40%);
  background-size: calc(var(--pc-w) * 0.14) calc(var(--pc-w) * 0.14), 100% 100%;
  background-position: center;
  box-shadow:
    inset 0 0 0 calc(var(--pc-w) * 0.055) #f2eee2,
    inset 0 0 0 calc(var(--pc-w) * 0.075) #1e2440,
    inset 0 0 0 var(--hair) rgb(20 24 31 / 0.4),
    0 1px 1px rgb(20 24 31 / 0.4);
}
.pcard--slot {
  background: none; box-shadow: inset 0 0 0 var(--hair) var(--line); opacity: 0.5;
}
.pcard--back::before, .pcard--slot::before { content: none; }

/* Dealing: from a fixed origin, staggered, and short. This is replayed dozens
   of times a session, so the whole flop lands inside 480ms. */
@keyframes deal {
  from {
    transform: translate3d(calc(var(--dx, 0px) * var(--motion)), calc(var(--dy, -40px) * var(--motion)), 0)
               rotate(calc(-14deg * var(--motion))) scale(calc(1 - 0.08 * var(--motion)));
    opacity: 0;
  }
  60% { opacity: 1; }
  to { transform: none; opacity: 1; }
}
.pcard.is-dealing {
  animation: deal 290ms var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 62ms * var(--motion));
}

/* ---------------------------------------------------------------- holdem -- */
.pokerpot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
}
.pokerpot__mid { display: flex; align-items: center; gap: var(--sp-2); }
.pokerpot__amt {
  font-size: var(--t-xl); font-weight: 800; color: var(--game-accent);
  font-variant-numeric: tabular-nums; font-family: var(--font-ui);
}
/* The pot as chips, not as a number. One element: the gradient IS the stack,
   so a 40-chip pot costs the same as a 4-chip one. */
.chipstack {
  --c: #c2402f; --n: 6; --band: 4px; --w: 26px;
  width: var(--w); height: calc(var(--n) * var(--band)); flex: 0 0 auto;
  border-radius: calc(var(--w) / 2) / var(--band);
  background: repeating-linear-gradient(180deg,
    var(--c) 0 calc(var(--band) - 1px),
    rgb(0 0 0 / 0.35) calc(var(--band) - 1px) var(--band));
  box-shadow:
    inset 0 var(--band) 0 rgb(255 255 255 / 0.28),
    inset 0 0 0 1px rgb(0 0 0 / 0.4),
    0 2px 3px rgb(0 0 0 / 0.5);
  transition: height 260ms var(--ease-out);
}
.chiprow { display: flex; align-items: flex-end; gap: 3px; min-height: 24px; }
/* Felt, but only here — a full-screen felt reads as a 2009 casino skin. As an
   inset panel it reads as a TABLE inside the app, which is the goal.
   Four ingredients make it fabric rather than green paint: an off-centre
   specular sheen, a vignette, a fine cross-hatch, and the page grain on top.
   Drop any one and it stops working. */
.pokerboard {
  --felt: #245240;
  display: flex; gap: var(--sp-2); justify-content: center; align-items: center;
  padding: var(--sp-4) var(--sp-3); border-radius: var(--r-4);
  min-width: 0; position: relative; isolation: isolate;
  background-color: var(--felt);
  background-image:
    radial-gradient(120% 80% at 34% 8%, rgb(255 255 255 / 0.07), transparent 58%),
    radial-gradient(130% 110% at 50% 122%, rgb(0 0 0 / 0.45), transparent 66%),
    repeating-linear-gradient(45deg, rgb(255 255 255 / 0.016) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(-45deg, rgb(0 0 0 / 0.025) 0 1px, transparent 1px 3px);
  box-shadow:
    inset 0 0 0 var(--hair) #16342a,
    inset 0 1px 0 rgb(255 255 255 / 0.08),
    inset 0 -16px 28px -16px rgb(0 0 0 / 0.65),
    0 1px 1px rgb(0 0 0 / 0.4);
}
.pokerboard::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  background-image: var(--grain); opacity: 0.05;
  mix-blend-mode: soft-light; pointer-events: none;
}
/* Five cards must fit at 320px. They shrink rather than wrap or overflow —
   which is exactly why the card's own metrics are in cqw and not in --pc-w. */
.pokerboard .pcard { --pc-w: min(46px, 15.5vw); flex: 0 0 auto; }

.pokerhand {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-3); border-radius: var(--r-3);
  background: var(--bg-raised); box-shadow: inset 0 0 0 var(--hair) var(--line);
}
.pokerhand.is-folded { opacity: 0.6; }
.pokerhand__cards { display: flex; gap: var(--sp-2); flex: 0 0 auto; }
.pokerhand__meta { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.pokerhand__name { font-size: var(--t-md); color: var(--text-1); }
.pokerpeek { display: inline-flex; gap: 2px; }
.bdg--btn { font-weight: 800; font-size: var(--t-2xs); color: var(--text-1); box-shadow: inset 0 0 0 var(--hair) var(--line-strong); }

/* Fold is fixed-width and separated; check/call is the widest, safest target. */
.pokeract { display: flex; gap: var(--sp-2); width: 100%; }
.pokeract .btn { flex: 1 1 0; min-width: 0; padding-inline: var(--sp-2); }
.pokeract .btn--danger { flex: 0 0 auto; min-width: 84px; }

.raise { display: flex; flex-direction: column; gap: var(--sp-3); width: 100%; }
.raise__head { display: flex; align-items: baseline; justify-content: space-between; }
.raise__amt { font-size: var(--t-xl); font-weight: 800; color: var(--game-accent); font-variant-numeric: tabular-nums; }
.raise__slider { width: 100%; height: var(--tap); background: none; }
.raise__slider::-webkit-slider-runnable-track { height: 8px; border-radius: 999px; background: var(--bg-inset); }
.raise__slider::-moz-range-track { height: 8px; border-radius: 999px; background: var(--bg-inset); }
.raise__slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; margin-top: -12px;
  width: 32px; height: 32px; border-radius: 50%;
  background: var(--game-accent); box-shadow: var(--e-2);
}
.raise__slider::-moz-range-thumb {
  width: 32px; height: 32px; border: none; border-radius: 50%;
  background: var(--game-accent); box-shadow: var(--e-2);
}
.seg--wrap { flex-wrap: wrap; }
.seg--wrap button { flex: 1 1 0; }

/* ----------------------------------------------------------------- cheat -- */
/* Ten-plus cards on a 320px screen: a wrapping grid, not a row that scrolls.
   Anything you have to scroll to find, you forget you have. */
.cheathand {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  padding: var(--sp-3); border-radius: var(--r-3);
  background: var(--bg-inset); box-shadow: inset 0 0 0 var(--hair) var(--line);
}
.cheatcard {
  padding: 0; border-radius: calc(46px * 0.13); line-height: 0;
  /* The tap target is the whole card, and a card is already thumb-sized. */
  transition: transform 120ms ease;
}
.cheatcard[disabled] { opacity: 0.75; }
.cheatcard.is-picked { transform: translateY(-10px); }
.cheatcard.is-picked .pcard { box-shadow: 0 0 0 3px var(--game-accent), 0 6px 12px rgba(0, 0, 0, 0.4); }

.cheatpile {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4); border-radius: var(--r-3);
  background: var(--bg-raised); box-shadow: inset 0 0 0 var(--hair) var(--line);
}
/* Overlapped, so the pile reads as a pile rather than a hand. */
.cheatpile__stack { display: flex; flex: 0 0 auto; }
.cheatpile__stack .pcard + .pcard { margin-left: -32px; }
.cheatpile__n { font-size: var(--t-xl); font-weight: 800; color: var(--game-accent); font-variant-numeric: tabular-nums; }

/* @starting-style plus allow-discrete is what lets a <dialog> animate OUT.
   Before these, display:none cut the exit animation off at frame one, which is
   why almost every hand-rolled sheet on the web pops rather than closes. */
@supports (transition-behavior: allow-discrete) {
  .sheet {
    opacity: 0;
    translate: 0 calc(16px * var(--motion));
    transition:
      opacity var(--d-3) var(--ease-out),
      translate var(--d-4) var(--spring-snappy),
      display var(--d-4) allow-discrete,
      overlay var(--d-4) allow-discrete;
  }
  .sheet[open] { opacity: 1; translate: 0 0; }
  @starting-style {
    .sheet[open] { opacity: 0; translate: 0 calc(16px * var(--motion)); }
  }
  .sheet::backdrop { background: rgb(4 6 10 / 0); transition: background var(--d-3); }
  .sheet[open]::backdrop { background: var(--scrim); }
  @starting-style { .sheet[open]::backdrop { background: rgb(4 6 10 / 0); } }
}

/* ------------------------------------------------------------ reveal log -- */
.log { display: flex; flex-direction: column; gap: var(--sp-2); }
.log li {
  display: flex; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-2); background: var(--bg-raised); font-size: var(--t-sm);
}
.log li b { color: var(--text-1); }

/* --------------------------------------------------------- celebration --- */
/* 24 particles, two nested transforms so they arc under gravity. Outer drifts
   on X linearly, inner falls on Y with ease-in. Compositor-only. */
.burst { position: fixed; left: 50%; top: 32%; width: 0; height: 0; z-index: 70; pointer-events: none; }
.burst span { position: absolute; left: 0; top: 0; animation: bx 1400ms cubic-bezier(0.15, 0.6, 0.4, 1) forwards; }
.burst i {
  display: block; width: 9px; height: 14px; margin: -7px 0 0 -4px;
  background: var(--c, var(--game-accent)); border-radius: 1px;
  animation: by 1400ms cubic-bezier(0.35, 0, 0.9, 0.75) forwards;
}
@keyframes bx { to { transform: translateX(var(--x)); } }
@keyframes by {
  0% { transform: translateY(var(--y0)) rotate(0) scale(0); opacity: 0; }
  9% { opacity: 1; transform: translateY(calc(var(--y0) * 0.7)) rotate(40deg) scale(1); }
  100% { transform: translateY(62vh) rotate(var(--r)) scale(0.85); opacity: 0; }
}
.burst span:nth-child(6n + 1) i { --c: var(--game-accent); }
.burst span:nth-child(6n + 2) i { --c: var(--team-1); border-radius: 50%; }
.burst span:nth-child(6n + 3) i { --c: var(--team-3); width: 6px; height: 6px; }
.burst span:nth-child(6n + 4) i { --c: var(--text-1); height: 3px; width: 16px; }
.burst span:nth-child(6n + 5) i { --c: var(--team-5); border-radius: 50% 50% 0 0; }
.burst span:nth-child(6n) i { --c: var(--team-6); width: 5px; height: 18px; }

/* ------------------------------------------------------- reduced motion --- */
/* Every animation here is REDUNDANT with a static cue, so removing motion
   never removes information. */
@media (prefers-reduced-motion: reduce) {
  :root {
    /* Every travel distance in the file is multiplied by --motion, so zeroing
       it removes movement while leaving opacity and duration alone. An
       animation that carries information still carries it. */
    --motion: 0;
    --spring-snappy: linear(0, 1);
    --spring-bouncy: linear(0, 1);
  }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .rolecard__inner { transform: none !important; }
  .rolecard__face { transition: opacity 140ms linear !important; }
  .rolecard__back { opacity: 1; }
  .rolecard.is-revealed .rolecard__back,
  .rolecard__btn:active .rolecard__back { opacity: 0; }
  .rolecard__front { transform: none !important; }
  .timer.is-urgent::after { display: none; }
  .timer.is-urgent .timer__n { text-decoration: underline; text-decoration-thickness: 3px; }
  .turnwash { animation: none; opacity: 0.14; }
  .pill i { animation: none !important; }
  .burst { display: none; }
}

/* ------------------------------------------------------------- the coach -- */
/* ============================================================ the lab ==== */
/* The teaching surfaces. Three rules run through all of it:

   ONE HERO PER SCREEN. The hero is 1.8x the runner-up and 3x its own label.
   That gap IS the hierarchy — the old panel had seven active sizes, which is
   another way of saying it had none.

   SPACE BETWEEN GROUPS IS TWICE THE SPACE WITHIN THEM. Nearly every "this has
   no hierarchy" complaint on a dense screen is that ratio sitting at 1:1.

   COLOUR IS NEVER THE ONLY CHANNEL. Every grade band carries a word, so the
   screen survives greyscale and colour-blindness both. */

/* --- while you decide: the name of the idea, and nothing else ------------ */
.labprompt {
  display: flex; align-items: baseline; gap: var(--sp-2);
  padding: var(--sp-3); border-radius: var(--r-3);
  background: var(--bg-inset);
  box-shadow: inset 0 0 0 var(--hair) var(--line-2);
}
.labprompt__eyebrow {
  font-size: var(--t-2xs); font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--text-3); flex: 0 0 auto;
}
.labprompt__name { font-size: var(--t-md); color: var(--text-1); }

/* --- guided mode: exactly one number ------------------------------------ */
.labhero {
  display: grid; grid-template-columns: auto 1fr; align-items: baseline;
  gap: 2px var(--sp-3); padding: var(--sp-3);
  border-radius: var(--r-3); background: var(--bg-inset);
  box-shadow: inset 0 0 0 var(--hair) var(--game-accent);
}
.labhero__val {
  font-size: 40px; line-height: 1.1; font-weight: 700; letter-spacing: -0.02em;
  color: var(--game-accent); font-variant-numeric: tabular-nums;
}
.labhero__label {
  font-size: var(--t-xs); font-weight: 500; color: var(--text-3);
}
/* An estimate must never look like a measurement. */
.labhero__val.is-estimate, .labfact__val.is-estimate {
  text-decoration: underline dotted var(--line-strong); text-underline-offset: 5px;
}
.labhero__point {
  grid-column: 1 / -1; margin-top: var(--sp-2);
  font-size: var(--t-sm); line-height: 1.5; color: var(--text-2);
}

/* --- after you act: verdict, comparison, lesson -------------------------- */
.labfeed {
  --tone: var(--line-strong);
  display: flex; flex-direction: column; gap: var(--sp-4);
  padding: var(--sp-4); border-radius: var(--r-3);
  background: var(--bg-inset);
  box-shadow: inset 0 0 0 var(--hair) var(--line), inset 3px 0 0 var(--tone);
}
.labfeed.is-optimal { --tone: var(--success); }
.labfeed.is-fine    { --tone: var(--success); }
.labfeed.is-leak    { --tone: var(--warn); }
.labfeed.is-mistake { --tone: #f97316; }
.labfeed.is-blunder { --tone: var(--danger); }

.labfeed__head { display: flex; align-items: baseline; gap: var(--sp-3); }
.labfeed__grade {
  font-size: var(--t-lg); line-height: 1.25; font-weight: 600;
  letter-spacing: -0.01em; color: var(--tone);
}
.labfeed__cost {
  font-size: var(--t-sm); font-weight: 600; color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.labfeed__verdict {
  font-size: var(--t-sm); line-height: 1.5; color: var(--text-1); margin: 0;
}

/* --- one bar per line: length from a shared baseline --------------------- */
.labbars { display: flex; flex-direction: column; gap: var(--sp-2); }
.labbar {
  display: grid; grid-template-columns: 86px 1fr 52px;
  align-items: center; gap: var(--sp-2); min-height: 28px;
}
.labbar__name {
  font-size: var(--t-xs); color: var(--text-3); text-transform: capitalize;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.labbar__track {
  position: relative; height: 8px; border-radius: 999px;
  background: var(--bg-sunken); overflow: hidden;
}
.labbar__fill {
  position: absolute; top: 0; bottom: 0; border-radius: 999px;
  background: var(--line-strong);
  transition: width var(--d-4) var(--ease-out), left var(--d-4) var(--ease-out);
}
.labbar__zero { position: absolute; top: -2px; bottom: -2px; width: 1px; background: var(--line-2); }
.labbar__ev {
  font-size: var(--t-sm); font-weight: 600; text-align: right; color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.labbar.is-best .labbar__fill { background: var(--game-accent); }
.labbar.is-best .labbar__name, .labbar.is-best .labbar__ev { color: var(--text-1); }
/* The line you took is outlined rather than filled, so "what I did" and "what
   was best" are two different channels and can coincide without ambiguity. */
.labbar.is-chosen .labbar__track { box-shadow: 0 0 0 2px var(--focus); }

/* --- the lesson: the part that is supposed to survive the session -------- */
.lablesson {
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding-top: var(--sp-3); border-top: var(--hair) solid var(--line);
}
.lablesson__tag {
  align-self: flex-start; padding: 3px var(--sp-2); border-radius: var(--r-pill);
  background: var(--bg-raised); color: var(--text-2);
  font-size: var(--t-2xs); font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
}
.lablesson__point { font-size: var(--t-sm); line-height: 1.5; color: var(--text-1); margin: 0; }
.lablesson__rule { font-size: var(--t-sm); line-height: 1.5; color: var(--text-3); margin: 0; }

/* --- level two of two: everything numeric ------------------------------- */
.labmore { border-top: var(--hair) solid var(--line); padding-top: var(--sp-2); }
.labmore__sum {
  display: flex; align-items: center; min-height: var(--tap);
  font-size: var(--t-sm); color: var(--text-2); cursor: pointer;
}
.labmore__body { display: flex; flex-direction: column; gap: var(--sp-4); padding-bottom: var(--sp-2); }
.labfact { display: flex; flex-direction: column; gap: 2px; }
.labfact__label { font-size: var(--t-xs); font-weight: 500; color: var(--text-3); }
.labfact__val {
  font-size: var(--t-md); font-weight: 600; color: var(--text-1);
  font-variant-numeric: tabular-nums;
}
.labfact__detail { font-size: var(--t-xs); line-height: 1.5; color: var(--text-3); }
.labfact__warn {
  font-size: var(--t-sm); line-height: 1.5; color: var(--warn); margin: 0;
}
.labfact__why { font-size: var(--t-sm); line-height: 1.5; color: var(--text-2); margin: 0; }

/* --- progress ------------------------------------------------------------ */
.labstrip {
  display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap;
  font-size: var(--t-xs); color: var(--text-3);
}
.labstrip__v { font-variant-numeric: tabular-nums; color: var(--text-2); }
.labstrip__sep { color: var(--line-2); }
.labstrip__next { margin-left: auto; color: var(--game-accent); }

.labnext {
  display: flex; flex-direction: column; gap: var(--sp-1);
  padding: var(--sp-3); border-radius: var(--r-3); background: var(--bg-inset);
  box-shadow: inset 0 0 0 var(--hair) var(--game-accent); margin-bottom: var(--sp-4);
}
.labnext__eyebrow {
  font-size: var(--t-2xs); font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--text-3);
}
.labnext__name { font-size: var(--t-lg); font-weight: 600; color: var(--text-1); }
.labnext__idea { font-size: var(--t-sm); line-height: 1.5; color: var(--text-2); margin: 0; }
.labnext__rule { font-size: var(--t-sm); line-height: 1.5; color: var(--text-3); margin: 0; }

.labstage { margin-bottom: var(--sp-5); }
.labstage__head { display: flex; align-items: baseline; justify-content: space-between; }
.labstage__count { font-size: var(--t-xs); color: var(--text-3); font-variant-numeric: tabular-nums; }
.labstage__blurb { font-size: var(--t-xs); line-height: 1.5; color: var(--text-3); margin: 2px 0 var(--sp-3); }
.labskills { display: flex; flex-direction: column; gap: var(--sp-2); }
.labskill {
  --band: var(--line-2);
  display: grid; grid-template-columns: 1fr auto; gap: 2px var(--sp-2);
  align-items: baseline;
}
.labskill.is-shaky   { --band: var(--danger); }
.labskill.is-getting { --band: var(--warn); }
.labskill.is-solid   { --band: var(--success); }
.labskill.is-sharp   { --band: var(--game-accent); }
.labskill__name { font-size: var(--t-sm); color: var(--text-2); }
.labskill__band { font-size: var(--t-xs); color: var(--band); }
.labskill__meter {
  grid-column: 1 / -1; height: 4px; border-radius: 999px;
  background: var(--bg-sunken); overflow: hidden;
}
.labskill__fill {
  display: block; height: 100%; border-radius: 999px; background: var(--band);
  transition: width var(--d-4) var(--ease-out);
}

/* A text button that reads as a link, not as a slab. Full 48px hit area from
   padding; the ink is 15px. */
/* A held table. The accent rail thickens and the block gets a rim, so it is
   visibly the thing the one button on screen refers to. */
.labfeed.is-held {
  box-shadow: inset 0 0 0 var(--hair) var(--tone), inset 4px 0 0 var(--tone);
}
.labover { display: flex; gap: var(--sp-2); width: 100%; align-items: stretch; }
.labover .btn--primary { flex: 1 1 auto; }
/* Small ink, full hit box: the visible control is 15px of text, the target is
   the whole 48px-tall padded button. Height is not what makes a control
   reachable — area is, and width is free on a phone. */
.labtext {
  flex: 0 0 auto; min-height: var(--tap); font-size: var(--t-sm); font-weight: 500;
  color: var(--text-3); background: none; box-shadow: none;
  padding-inline: var(--sp-3); white-space: nowrap;
}
.optionnote {
  font-size: var(--t-xs); line-height: 1.5; color: var(--text-3);
  margin: calc(-1 * var(--sp-2)) 0 0;
}

@media (prefers-reduced-motion: reduce) {
  .labbar__fill, .labskill__fill { transition: none; }
}
