/* gamenight.click — layout prototype
   Warm dark theme with a light mode. No remote fonts or assets: the server's
   CSP forbids them, and the site should never depend on one. */

*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }   /* author display rules beat the UA sheet */

:root {
  --bg:        #14100d;
  --bg-glow:   #1d1712;
  --surface:   #1c1713;
  --surface-2: #241d17;
  --line:      #352b22;
  --line-soft: #2a221b;
  --text:      #f2ebe1;
  --muted:     #a19182;
  --accent:    #e8ab3c;
  --accent-dim:#8a6620;
  --on-accent: #201603;
  --live:      #d9694a;
  /* The "connected" green. It was a bare #5fa87a inside the .conn rule, outside
     any token, and it measures 2.56:1 on the light background — under the 3:1
     bar for a non-text indicator. Light needs its own value. */
  --ok:        #5fa87a;
  --radius:    14px;
  --radius-sm: 9px;
  --shadow:    0 1px 2px rgb(0 0 0 / .4), 0 8px 28px -12px rgb(0 0 0 / .7);
  --font: ui-sans-serif, system-ui, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, "Cascadia Mono", "SF Mono", Consolas, monospace;
  /* The ink on a player disc, and deliberately NOT redefined for light: the six
     disc colours are absolute in both themes, so identity does not shift when
     someone flips the theme, and their ink has to be absolute too. Measured
     against the six: 8.77 / 5.16 / 6.26 / 5.26 / 5.19 / 9.81 — all clear AA. */
  --marker-ink: #201603;
}
html[data-theme="light"] {
  --bg:        #f7f2ea;
  --bg-glow:   #fffaf2;
  --surface:   #ffffff;
  --surface-2: #f3ece2;
  --line:      #e2d7c8;
  --line-soft: #ece3d6;
  --text:      #241d16;
  --muted:     #766757;
  --accent:    #b07a15;
  --accent-dim:#e6c98d;
  --on-accent: #fffdf8;
  --live:      #bb4a2c;
  --ok:        #2f7d55;   /* 4.50:1 on the light background; #5fa87a is 2.56:1 */
  --shadow:    0 1px 2px rgb(60 40 10 / .07), 0 10px 30px -14px rgb(60 40 10 / .25);
}
html { color-scheme: dark; }
html[data-theme="light"] { color-scheme: light; }

body {
  /* Both, in this order. On a phone `100vh` is the LARGEST viewport — the one
     with the address bar hidden — so the bottom of the page sits under browser
     chrome until you scroll, and shifts as the bar comes and goes. `dvh` tracks
     the viewport that is actually there. The vh line stays as the fallback for
     an engine that does not know dvh, which drops the second declaration. */
  margin: 0; min-height: 100vh; min-height: 100dvh;
  font-family: var(--font); color: var(--text);
  background: radial-gradient(1100px 620px at 50% -12%, var(--bg-glow), transparent 68%), var(--bg);
  -webkit-font-smoothing: antialiased;
}
h1, h2 { line-height: 1.15; margin: 0 0 .35em; letter-spacing: -.018em; font-weight: 650; }
h2 { font-size: 1.35rem; }
p  { line-height: 1.6; margin: 0 0 1em; }
.dot { color: var(--accent); }
.muted { color: var(--muted); }
.hint { color: var(--muted); font-size: .84rem; line-height: 1.5; margin: .45em 0 0; }
.err  { color: var(--live); font-size: .86rem; font-weight: 550; margin: .5em 0 0; }

/* The PLATFORM's visually-hidden class. The game has its own (.tt-sr) on
   purpose — neither owns the other's. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 20;
  padding: .6rem 1rem; background: var(--accent); color: var(--on-accent);
  font-weight: 650; text-decoration: none;
}
.skip:focus { left: 0; }

/* ── connection state + refusals ───────────────────────────────────────── */

/* The one thing a live site owes the player at all times: whether the server is
   still there. A stale board with no indicator is indistinguishable from a
   board nobody has moved on. */
/* A chip, not a bare dot. A colour on its own is not a status (WCAG 1.4.1) and
   the title attribute it used to rely on cannot be hovered on a phone. It stays
   silent while things are fine and puts a word up when they are not. */
.conn {
  margin: 0 0 0 auto; display: inline-flex; align-items: center; gap: .4rem;
  min-height: 24px; padding: .15rem .5rem .15rem .45rem; border-radius: 999px;
  font-size: .76rem; font-weight: 600; color: var(--text);
  border: 1px solid transparent;
}
.conn-dot { width: 8px; height: 8px; border-radius: 50%; flex: none;
            background: var(--muted); transition: background .2s; }
.conn[data-state="live"]       .conn-dot { background: var(--ok); }
.conn[data-state="connecting"] .conn-dot { background: var(--accent); }
.conn[data-state="lost"]       .conn-dot { background: var(--live); }
.conn[data-state="lost"] {
  border-color: color-mix(in oklab, var(--live) 40%, var(--line));
  background: color-mix(in oklab, var(--live) 10%, transparent);
}
.conn-word:empty { display: none; }
.conn + .who { margin-left: 1rem; }
/* No forced-colors override for .conn-word: it is EMPTY whenever the chip is
   quiet, so forcing it visible showed nothing and added a stray flex gap. The
   chip only stays quiet while everything is fine — the word is present for both
   states worth seeing, in every colour mode. */

/* The server's own words, never a guess made here. Dismissible, and the timer
   pauses while it is being read: this is the only place a refusal is reported,
   and yanking it away mid-sentence loses the only explanation there was. */
.banner {
  display: flex; align-items: center; gap: .8rem;
  margin: 0; padding: .7rem clamp(.9rem, 3vw, 2rem);
  background: color-mix(in oklab, var(--live) 14%, var(--surface));
  border-bottom: 1px solid color-mix(in oklab, var(--live) 40%, var(--line));
  font-size: .88rem; font-weight: 550;
}
.banner span { flex: 1; min-width: 0; }
/* .icon-btn was named in the shared :focus-visible rule and defined nowhere —
   a prototype leftover. Now it is real. */
.icon-btn {
  display: grid; place-items: center; width: 28px; height: 28px; flex: none;
  padding: 0; border: 1px solid transparent; border-radius: var(--radius-sm);
  background: transparent; color: inherit; cursor: pointer; font: inherit;
}
.icon-btn:hover { border-color: var(--line); background: var(--surface); }

/* ── buttons + fields ──────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  font: inherit; font-size: .92rem; font-weight: 600; cursor: pointer;
  padding: .62rem 1.15rem; border-radius: var(--radius-sm);
  border: 1px solid var(--line); background: var(--surface); color: var(--text);
  transition: border-color .12s, background .12s, transform .06s;
}
.btn:hover { border-color: var(--accent-dim); background: var(--surface-2); }
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn-primary:hover { background: color-mix(in oklab, var(--accent) 88%, #fff); border-color: transparent; }
/* --on-accent on --accent measures 3.66:1 in light — under AA for anything that
   is not large text, and the lobby's Join is a .82rem instance. Darkened here
   only, rather than moving the --accent token, because the game uses that token
   too and its own contrast was reasoned about separately. 5.58:1. */
html[data-theme="light"] .btn-primary { background: #8a5f10; border-color: #8a5f10; }
html[data-theme="light"] .btn-primary:hover { background: #9c6c13; border-color: #9c6c13; }
.btn-block { width: 100%; }

label { display: block; font-size: .86rem; font-weight: 600; margin-bottom: .35rem; }
input[type="text"] {
  font: inherit; width: 100%; color: var(--text); background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: .62rem .8rem;
}
/* Enumerated, never a global :where(...). A blanket rule would newly ring the
   game's own controls — .tt-pile is a bare <button> with no .btn class — and
   the game owns its focus styling. */
input:focus-visible, select:focus-visible, .btn:focus-visible,
.who:focus-visible, .sw:focus-visible, .icon-btn:focus-visible, .card:focus-visible,
.back:focus-visible, .brand:focus-visible, .skip:focus-visible,
.invite-code:focus-visible, .empty a:focus-visible, .firstrun a:focus-visible {
  /* No border-radius here. It applied to the ELEMENT, not the ring, so every
     rounded control — the pill-shaped profile chip, the round swatches — was
     squared off for as long as it held focus. */
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* ── the gate ──────────────────────────────────────────────────────────── */
.gate { min-height: 82vh; min-height: 82dvh;
        display: grid; place-items: center; padding: 2rem 1rem 4rem; }
.gate-card {
  width: min(430px, 100%); text-align: center;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: clamp(1.6rem, 5vw, 2.6rem);
}
.gate-mark {
  width: 46px; height: 46px; border-radius: 12px; margin: 0 auto 1.1rem;
  background: linear-gradient(150deg, var(--accent), var(--live));
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .2), 0 8px 24px -10px var(--accent);
}
.gate-card h1 { font-size: 1.55rem; }
.gate-sub { color: var(--muted); font-size: .95rem; margin-bottom: 1.9rem; }
.gate-form { text-align: left; }
#code {
  font-family: var(--mono); font-size: 1.22rem; letter-spacing: .13em;
  text-align: center; text-transform: uppercase; padding: .8rem;
}
#code::placeholder { color: color-mix(in oklab, var(--muted) 55%, transparent); }
.gate-form .btn { margin-top: 1.2rem; }
.gate-foot {
  margin: 1.7rem 0 0; padding-top: 1.2rem; border-top: 1px solid var(--line-soft);
  font-size: .82rem; color: var(--muted);
}

/* ── top bar ───────────────────────────────────────────────────────────── */
.topbar {
  display: flex; align-items: center; gap: 1rem;
  padding: .85rem clamp(.9rem, 3vw, 2rem);
  border-bottom: 1px solid var(--line-soft);
  position: sticky; top: 0; z-index: 10;
  background: color-mix(in oklab, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
}
/* color and text-decoration are not decoration here: these are bare <a>s, and
   without them the wordmark rendered in the browser's default link blue with an
   underline through it. */
.brand { display: flex; align-items: center; gap: .55rem; font-weight: 680;
         letter-spacing: -.02em; color: var(--text); text-decoration: none; }
.brand-mark {
  width: 20px; height: 20px; border-radius: 5px; flex: none;
  background: linear-gradient(150deg, var(--accent), var(--live));
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .18);
}
.who {
  margin-left: auto; display: flex; align-items: center; gap: .5rem;
  font: inherit; font-size: .88rem; font-weight: 550; cursor: pointer;
  padding: .3rem .7rem .3rem .3rem; border-radius: 999px;
  border: 1px solid transparent; background: transparent; color: var(--text);
  text-decoration: none;
}
.who:hover { border-color: var(--line); background: var(--surface); }
/* The disc carries a colour AND an initial. Four brand-new accounts are four
   identical gold discs otherwise, and the initial is the cheapest thing that
   tells them apart. --mk is the one size knob. */
.avatar {
  display: inline-grid; place-items: center;
  width: var(--mk, 26px); height: var(--mk, 26px); border-radius: 50%; flex: none;
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .16);
  font-size: calc(var(--mk, 26px) * .42); font-weight: 700; line-height: 1;
  color: var(--marker-ink); user-select: none;
}
.avatar[data-c="1"] { background: #e8ab3c; }
.avatar[data-c="2"] { background: #d9694a; }
.avatar[data-c="3"] { background: #5fa87a; }
.avatar[data-c="4"] { background: #5d8fc4; }
.avatar[data-c="5"] { background: #a878c0; }
.avatar[data-c="6"] { background: #c9bfae; }
.avatar-sm { --mk: 22px; }
.avatar-lg { --mk: 34px; }

/* ── page + sections ───────────────────────────────────────────────────── */
.page { max-width: 1020px; margin: 0 auto; padding: clamp(1.4rem, 4vw, 2.6rem) clamp(.9rem, 3vw, 2rem) 5rem; }
.block + .block { margin-top: clamp(2.2rem, 6vw, 3.4rem); }
.block-head { margin-bottom: 1.1rem; }
.block-head p { margin: 0; font-size: .9rem; }

/* ── the catalogue card ────────────────────────────────────────────────────
   A game is presented with NO ARTWORK, because there is none and none is
   coming. So the card is typographic: the name IS the picture, set at poster
   size on its own plate over a rail carrying the two facts that decide whether
   a game is playable tonight — how many people, and how long.

   Nothing here depicts a game, and that is the point. A plate that drew cards
   would be a lie the moment game #2 is a 3D room. A plate that drew SEATS would
   be worse: filled seat marks on a catalogue card are indistinguishable from
   "2 of 6 seated", which is invented players on a card with no occupants. */
.grid {
  display: grid; gap: 1rem;
  /* 244px, not 210px. At the 956px content box that is 3 tracks of 308px rather
     than 4 of 227px — one card alone in a row has to look deliberate, and 227px
     is a fragment. min() keeps a single column from overflowing a phone. */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 244px), 1fr));
}
.card {
  container-type: inline-size;       /* the plate's type scales with the card */
  display: flex; flex-direction: column; padding: 0; cursor: pointer;
  text-align: left; font: inherit; color: var(--text);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow);
  transition: border-color .14s, transform .14s;
}
.card:hover { border-color: var(--accent-dim); transform: translateY(-2px); }
.card:active { transform: translateY(0); }
/* Never :disabled — disabling the focused element drops focus to <body>. */
.card[aria-busy="true"] { cursor: progress; transform: none; }

.card-plate {
  position: relative; flex: none;
  display: flex; flex-direction: column; justify-content: space-between; gap: .8rem;
  aspect-ratio: 16 / 10; min-height: 138px; overflow: hidden;
  padding: .85rem .95rem .7rem 1.05rem;
  border-bottom: 1px solid var(--line-soft);
  background-color: var(--surface-2);
  /* A corner wash, not a texture: it fades out by 58% of the height so it never
     reaches the rail, and the only thing it sits under is --text at 14:1. */
  background-image: radial-gradient(120% 90% at 0% 0%,
                    color-mix(in oklab, var(--accent) 8%, transparent), transparent 58%);
}
/* One accent edge, identical on every card — a shelf, not a junk drawer — and a
   second carrier for hover and focus alongside the outline. */
.card-plate::before {
  content: ""; position: absolute; inset: 0 auto 0 0; width: 3px;
  background: var(--accent-dim); transition: background .14s;
}
.card:hover .card-plate::before,
.card:focus-visible .card-plate::before { background: var(--accent); }

.card-name {
  display: block;
  font-weight: 680; line-height: 1.06; letter-spacing: -.022em;
  overflow-wrap: anywhere; text-wrap: balance;
  /* Two declarations, in this order and on purpose. The first is the fallback
     for an engine without container queries, which drops the second outright.
     --name-scale is set inline from the string's own length, because a single
     long word cannot wrap and so must count for more than the total. */
  font-size: calc(var(--name-scale, 1) * 1.45rem);
  font-size: calc(var(--name-scale, 1) * clamp(1.2rem, 8.8cqi, 2.05rem));
}
.card-meta {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: .5rem; flex-wrap: wrap;
  padding-top: .55rem; border-top: 1px solid var(--line);
}
/* --muted on --surface-2 measures 5.46:1 dark and 4.65:1 light. Both clear AA,
   and the light figure is close enough that the ground under this line has to
   stay flat — which is why the wash above fades out before it. */
.card-fact {
  font-family: var(--mono); font-size: .68rem; font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
  white-space: nowrap;
}
.card-body { display: flex; flex-direction: column; flex: 1;
             padding: .8rem 1rem .85rem 1.05rem; }
.card-tagline {
  font-size: .84rem; line-height: 1.45; color: var(--muted); margin: 0;
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden;
}
/* margin-top:auto is load-bearing, not tidiness: it is the only thing holding
   every footer on a row level when the taglines differ in length. */
.card-go {
  display: flex; align-items: center; gap: .45rem;
  margin-top: auto; padding-top: .85rem;
  font-size: .82rem; font-weight: 620; color: var(--text);
}
/* Drawn, not a glyph and not an asset. The label stays --text: --accent as small
   text is 3.72:1 on white, the same failure the game's wild chip documents. A
   2px graphic is held to 3:1, which it clears. */
.card-go::after {
  content: ""; flex: none; width: 6px; height: 6px;
  border-top: 2px solid var(--accent); border-right: 2px solid var(--accent);
  rotate: 45deg; transition: translate .14s;
}
.card:hover .card-go::after { translate: 3px 0; }

@media (forced-colors: active) { .card-plate { border-bottom-color: CanvasText; } }
/* A 16:10 plate on a 352px phone is 220px of poster before the first word. */
@media (max-width: 560px) { .card-plate { aspect-ratio: 5 / 2; } }

/* ── lobby rows ────────────────────────────────────────────────────────────
   A lobby entry is a RECORD, not a poster: every table carries the same fields
   in the same order, and the reader is MATCHING — a code that was read out, a
   name they know — rather than browsing. So the fields are bands: game name
   under game name, code under code, action under action, and a scan is one
   vertical sweep. The Games grid above is cards because those items differ in
   identity; these differ only in data.

   Nothing here reads a game-specific field. A WebGL room scene renders the
   identical row. */
.rows { list-style: none; margin: 0; padding: 0;
        display: flex; flex-direction: column; gap: .6rem; }
.row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "title action" "people action" "meta action";
  align-items: center; column-gap: 1rem; row-gap: .32rem;
  padding: .8rem 1rem .85rem 1.1rem;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm); border-left: 3px solid var(--line);
  transition: border-color .18s, background .18s;
}
.row[data-status="waiting"] { border-left-color: var(--accent); }
.row[data-status="playing"] { border-left-color: var(--live); }
/* No :hover background. The row is not clickable — only the Join button is —
   and a full-surface change is the strongest hover signal there is. */

.row-head  { grid-area: title; display: flex; align-items: baseline;
             gap: .5rem; flex-wrap: wrap; min-width: 0; }
.row-title { margin: 0; font-size: 1rem; font-weight: 640; line-height: 1.2;
             letter-spacing: -.012em; }
/* State in three channels — the rail colour, the pill's dot, and the WORD — so
   it never rests on colour alone. The word is --text on a tint, never --accent. */
.row-state {
  display: inline-flex; align-items: center; gap: .35rem; flex: none;
  padding: .1rem .5rem .1rem .4rem; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--text);
  font-size: .66rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
}
.row-state-dot { width: 7px; height: 7px; border-radius: 50%; flex: none;
                 background: var(--muted); }
.row[data-status="waiting"] .row-state {
  border-color: var(--accent-dim);
  background: color-mix(in oklab, var(--accent) 12%, transparent); }
.row[data-status="waiting"] .row-state-dot { background: var(--accent); }
.row[data-status="playing"] .row-state {
  border-color: color-mix(in oklab, var(--live) 45%, var(--line));
  background: color-mix(in oklab, var(--live) 12%, transparent); }
.row[data-status="playing"] .row-state-dot { background: var(--live); }

/* WHO is seated and how many chairs are free, as one object on one line. An
   earlier version stacked a disc track above a names line; with real accounts
   most discs render the same initial, so that was a row of near-identical
   circles above a line already spelling the names out. Here the discs carry
   colour and capacity, the text carries names, neither repeats the other. */
.row-people { grid-area: people; display: flex; align-items: center;
              gap: .5rem; min-width: 0; }
.row-faces  { display: flex; align-items: center; gap: .25rem; flex: none; }
.row-names  { margin: 0; font-size: .84rem; color: var(--muted); min-width: 0;
              overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* An empty chair, drawn rather than apologised for — the same idiom the game's
   empty card slot uses. Hidden on a running table: nobody can take one, so
   drawing them would misstate the roster. */
.seat-ring { width: 22px; height: 22px; border-radius: 50%; flex: none;
             border: 1px dashed var(--line); }

.row-meta { grid-area: meta; margin: 0; display: flex; align-items: baseline;
            gap: .4rem; flex-wrap: wrap;
            font-size: .76rem; color: var(--muted);
            font-variant-numeric: tabular-nums; }
/* The code IDENTIFIES the row — two tables of the same game are otherwise the
   same row twice — and does not compete with the game name. Mono and tracked so
   six characters read as six, --muted so it stays in the fact tier, and no box:
   a bordered full-contrast chip out-weighted the title and made a column of
   mono chips the strongest rhythm on the page. */
.row-code { font-family: var(--mono); font-weight: 650; letter-spacing: .1em;
            color: var(--muted); }
.row-sep::before { content: "\00b7"; }

.row-action { grid-area: action; display: flex; align-items: center; gap: .6rem; }
.row-join   { min-width: 5.5rem; }
/* A word, not a disabled button: a disabled control is skipped in several
   screen-reader browse modes, which would delete running tables from the lobby
   a screen reader can perceive. */
.row-note   { font-size: .8rem; font-weight: 600; color: var(--muted);
              white-space: nowrap; }

@media (max-width: 620px) {
  .row { grid-template-columns: minmax(0, 1fr);
         grid-template-areas: "title" "people" "meta" "action";
         row-gap: .45rem; }
  .row-action { margin-top: .15rem; }
  .row-action .btn { flex: 1; }
  .row-names { white-space: normal; }   /* two lines beat an ellipsis on a phone */
}
/* Forced colours flattens backgrounds, which would make a taken chair and a
   free one the same circle and erase the state pill. */
@media (forced-colors: active) {
  .row-state { border-color: CanvasText; }
  .avatar    { border: 1px solid CanvasText; }
  .seat-ring { border: 1px dashed CanvasText; }
}

.btn-sm { padding: .35rem .8rem; font-size: .82rem; }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn:disabled:hover { border-color: var(--line); background: var(--surface); }
.btn-danger { color: var(--live); border-color: color-mix(in oklab, var(--live) 40%, var(--line)); }
.btn-danger:hover { color: var(--live); border-color: var(--live);
                    background: color-mix(in oklab, var(--live) 10%, var(--surface)); }

.empty {
  padding: 2.2rem 1.5rem; text-align: center;
  border: 1px dashed var(--line); border-radius: var(--radius);
  background: color-mix(in oklab, var(--surface) 45%, transparent);
}
.empty p { margin: 0; font-size: .92rem; }
.empty p + p { margin-top: .4rem; }
.empty-title { font-weight: 650; }
/* A table and chairs nobody is in, drawn from PLATFORM shapes — seats — so it
   carries no suggestion of cards or turns and stays true when game #2 is a 3D
   room. Used ONLY where nothing is occupied, so it cannot read as occupancy. */
.mark { display: block; width: 132px; height: auto; margin: 0 auto 1rem; opacity: .8; }
.mark-table { fill: var(--surface-2); stroke: var(--line); stroke-width: 1.5; }
.mark-seat  { fill: none; stroke: var(--line); stroke-width: 1.5; stroke-dasharray: 3 3; }

/* ── profile page ──────────────────────────────────────────────────────── */
.narrow { max-width: 660px; }
.back {
  display: inline-block; margin-bottom: 1.2rem; text-decoration: none;
  color: var(--muted); font-size: .88rem; font-weight: 550;
}
.back:hover { color: var(--accent); }

/* Settings groups. Adding a setting is one .setrow; adding a category is one
   .setgroup — the page is built to grow, since more settings are coming. */
.setgroup { border: 0; padding: 0; margin: 2rem 0 0; }
.setgroup:first-of-type { margin-top: 1.6rem; }
.setgroup h3 {
  margin: 0 0 .2rem; padding-bottom: .6rem; border-bottom: 1px solid var(--line-soft);
  font-size: .74rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted);
}
.setrow {
  display: flex; align-items: flex-start; gap: 1.5rem; flex-wrap: wrap;
  padding: 1.1rem 0; border-bottom: 1px solid var(--line-soft);
}
.setrow:last-child { border-bottom: 0; }
.setrow-label { flex: 1 1 240px; min-width: 0; }
.setrow-label label, .pseudo-label {
  display: block; font-size: .95rem; font-weight: 600; margin-bottom: .2rem;
}
.setrow-label .hint { margin: 0; }
.setrow-control { flex: 0 1 260px; min-width: 200px; }
.setrow-control code {
  /* A 36-character UUID in a 200px-min control overflows at 375px without this. */
  font-family: var(--mono); font-size: .78rem; color: var(--muted);
  overflow-wrap: anywhere; user-select: all; max-width: 100%;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: .5rem .7rem; display: inline-block;
}

.swatches { display: flex; gap: .55rem; flex-wrap: wrap; }
.sw {
  width: 34px; height: 34px; border-radius: 50%; cursor: pointer; padding: 0;
  border: 2px solid transparent; outline-offset: 3px;
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .18);
}
.sw { display: grid; place-items: center; color: var(--marker-ink); }
.sw[aria-checked="true"] { border-color: var(--text); }
/* Selection carried by SHAPE as well as a border tint. */
.sw-check { width: 15px; height: 15px; fill: none; stroke: currentColor;
            stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.sw[aria-checked="false"] .sw-check { visibility: hidden; }
.setrow-preview .seat { pointer-events: none; }

.form-foot {
  display: flex; align-items: center; gap: .9rem;
  margin-top: 2rem; padding-top: 1.4rem; border-top: 1px solid var(--line-soft);
}
.saved { font-size: .85rem; color: var(--accent); font-weight: 600; }

select {
  font: inherit; width: 100%; color: var(--text); background: var(--bg); cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: .62rem .8rem;
}
select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.who[aria-current="page"] { border-color: var(--line); background: var(--surface); }

/* ── the table ─────────────────────────────────────────────────────────── */
.table-head {
  display: flex; align-items: flex-start; gap: 1rem; flex-wrap: wrap;
  margin-bottom: 1.4rem;
}
.table-head p { margin: 0; font-size: .9rem; }
/* Wraps so the sentence explaining why Start is off sits under the button it
   explains, rather than beside it. */
.table-actions { margin-left: auto; display: flex; gap: .6rem;
                 flex-wrap: wrap; justify-content: flex-end; }

/* Every seat the table HAS, occupied or not, so "how many more people do we
   need" is something you count rather than compute. */
.seats { list-style: none; margin: 0 0 .9rem; padding: 0;
         display: flex; flex-wrap: wrap; gap: .5rem; }
.seat {
  display: flex; align-items: center; gap: .5rem;
  padding: .4rem .8rem .4rem .4rem; min-height: 42px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 999px;
  font-size: .88rem; font-weight: 550;
}
.seat .avatar { --mk: 30px; }
.seat-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.seat-free { background: none; border-style: dashed; color: var(--muted); font-weight: 500; }
.seat-free[data-need="true"] { border-color: var(--accent-dim); color: var(--text); }
.seat-free .seat-disc { width: 30px; height: 30px; border-radius: 50%; flex: none;
                        border: 1px dashed var(--line); }
.seat-free[data-need="true"] .seat-disc { border-color: var(--accent-dim); }
/* The dashed outline alone is not enough — --line on --surface is about 1.3:1 in
   dark, well under the 3:1 non-text bar — which is why the WORD is on the plate
   in --muted rather than the meaning being left to the border. */
.tag {
  font-size: .66rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted); border: 1px solid var(--line); border-radius: 999px; padding: .05rem .45rem;
}

/* ── boot ─────────────────────────────────────────────────────────────────
   The page starts here and render() is what takes it away. A failed /api/me
   used to leave a silently empty page with no message and no way back. */
.boot { min-height: 46vh; min-height: 46dvh;
        display: grid; place-items: center; text-align: center;
        padding: clamp(1.4rem, 4vw, 2.6rem) clamp(.9rem, 3vw, 2rem); }
/* Held back 250ms, so a local answer never paints it at all. The delay survives
   prefers-reduced-motion deliberately: the global clamp shortens DURATION, not
   delay, and `both` keeps the element invisible until the delay elapses. It is
   also why there is no spinner — a rotation clamped to .001ms and set to
   infinite is a strobe, not a still. */
.boot-msg { margin: 0; font-size: .92rem; color: var(--muted);
            animation: gn-hold 120ms ease-out 250ms both; }
@keyframes gn-hold { from { opacity: 0; } }
.boot-error { max-width: 34rem; }
.boot-title { margin: 0 0 .3rem; font-size: 1.05rem; font-weight: 650; }
.boot-error .muted { margin: 0 0 1.1rem; font-size: .92rem; }

/* ── first run ────────────────────────────────────────────────────────────
   Every new account is created with an empty name and renders as "Player" to
   everyone else. One field, one Save, and nothing is blocked — a modal for this
   would be a wall in front of the thing the person came to do. */
.firstrun {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: .8rem 1.2rem;
  margin-bottom: clamp(2.2rem, 6vw, 3.4rem);
  padding: clamp(1rem, 3vw, 1.35rem);
  background: var(--surface);
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.firstrun-head { flex: 1 1 18rem; min-width: 0; }
.firstrun-head h2 { margin: 0 0 .25rem; font-size: 1.15rem; }
.firstrun-head p  { margin: 0; font-size: .9rem; }
.firstrun-form { display: flex; gap: .6rem; flex: 0 1 22rem; min-width: 0; }
.firstrun-form input[type="text"] { flex: 1 1 auto; min-width: 0; }
.firstrun-form .btn { flex: none; }
.firstrun .hint { flex: 1 1 100%; margin: 0; }
.firstrun .hint a { color: var(--text); text-underline-offset: 2px; }

/* ── join by code ─────────────────────────────────────────────────────────
   The paste target for a code someone read out. Resolved against the table list
   this client already holds — no endpoint, no judgement. */
.codejoin { display: flex; align-items: flex-end; gap: .5rem;
            margin: .9rem 0 0; flex-wrap: wrap; }
.codejoin .field { flex: 0 0 12rem; }
.codejoin label { margin-bottom: .3rem; }
#joinCode { font-family: var(--mono); letter-spacing: .16em; text-transform: uppercase;
            text-align: center; padding: .5rem .6rem; }
.codejoin .err { flex: 1 1 100%; margin: .35rem 0 0; }

/* ── the waiting room ─────────────────────────────────────────────────────
   Platform, not game, and deliberately NOT inside #gameSurface: this is the
   same screen when game #2 is a WebGL room. The only picture on it is a dashed
   circle, which is a chair nobody is in. */
.waitroom { display: grid; gap: 1.1rem; margin-bottom: 1.6rem; }
.invite {
  display: flex; flex-direction: column; align-items: flex-start; gap: .55rem;
  padding: 1rem 1.1rem 1.1rem;
  background: var(--surface); border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.invite-label { margin: 0; font-size: .72rem; font-weight: 700; letter-spacing: .1em;
                text-transform: uppercase; color: var(--muted); }
/* The one thing on this screen a person reads out loud, so it is the largest
   thing on it. A BUTTON because clicking it SELECTS it: navigator.clipboard is
   undefined on the tailnet's plain-http origin, which SERVICES.md records as an
   intended way in, so the guaranteed affordance has to be the primary one. The
   extra right padding pays for the trailing letter-space, which is otherwise
   applied after the last character and pushes the text off-centre. */
.invite-code {
  font: inherit; font-family: var(--mono); font-weight: 700; line-height: 1.05;
  font-size: clamp(1.5rem, 5vw, 2.2rem); letter-spacing: .16em;
  color: var(--text); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: .35rem .5rem .35rem .66rem; cursor: pointer;
  transition: border-color .12s;
}
.invite-code:hover { border-color: var(--accent-dim); }
/* Not .saved, which is --accent at .85rem and carries the light-theme contrast
   problem the game's wild chip documents. */
.invite-state { margin: 0; font-size: .82rem; font-weight: 600; color: var(--text);
                min-height: 1.2em; }
.invite .hint { margin: 0; max-width: 52ch; }

/* ── the device link, on the profile page ─────────────────────────────────
   Deliberately the same furniture as the table code above: same class, same
   click-to-select affordance, same reason (no clipboard API on plain http). A
   second visual language for "here is a code, read it out" would be a second
   thing to learn for no gain. */
.linkbox {
  display: flex; flex-direction: column; align-items: flex-start; gap: .55rem;
  margin-top: .9rem; padding: 1rem 1.1rem 1.1rem;
  background: var(--surface-2); border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
}
/* Smaller than the table code, because it is nearly three times as long: the
   table's is 6 characters and this is 15 in three groups. At the shared size a
   phone in portrait wraps it mid-group, which is precisely how a code gets
   misread aloud. */
.linkbox .invite-code {
  font-size: clamp(1rem, 3.9vw, 1.45rem); letter-spacing: .12em;
  background: var(--surface);
}
/* "Already sent": outstanding, but minted where this browser cannot see it. Not
   a live control, and it must not look like one. */
.linkbox .invite-code:disabled {
  color: var(--muted); cursor: default; font-family: inherit;
  font-weight: 600; letter-spacing: normal; font-size: 1rem;
}
.linkbox .invite-code:disabled:hover { border-color: var(--line); }
.linkbox .hint { margin: 0; max-width: 52ch; }

/* ── the lobby, in two lists ──────────────────────────────────────────────
   Open tables and running ones answer different questions, so the running ones
   sit under their own quieter heading rather than mixed into the joinable list. */
.running { margin-top: 1.4rem; }
.running-head {
  margin: 0 0 .5rem; font-size: .72rem; text-transform: uppercase;
  letter-spacing: .08em; color: var(--muted);
}
/* What a table is SET TO — the game's own option labels, resolved by the server
   so browsing the lobby never means loading a game's code. */
.row-settings {
  margin: .2rem 0 0; font-size: .78rem; color: var(--muted);
}
.row-settings:empty { display: none; }

/* ── friends ──────────────────────────────────────────────────────────────── */
.findform { display: flex; gap: .5rem; align-items: center; }
.findform input {
  flex: 1; min-width: 0; font: inherit; padding: .55rem .7rem;
  color: var(--text); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
#findHint { margin: .35rem 0 1rem; }
.people { list-style: none; margin: 0 0 .4rem; padding: 0; display: grid; gap: .4rem; }
.person {
  display: flex; align-items: center; gap: .55rem;
  padding: .5rem .7rem;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.person-name {
  flex: 1; min-width: 0; font-weight: 650;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.person-note { font-size: .78rem; color: var(--muted); }

/* ── the archive ──────────────────────────────────────────────────────────── */
.matches { list-style: none; margin: 0; padding: 0; display: grid; gap: .7rem; }
.match {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: .8rem .95rem;
}
.match-head {
  display: flex; align-items: baseline; gap: .6rem;
  margin-bottom: .55rem; padding-bottom: .5rem; border-bottom: 1px solid var(--line-soft);
}
.match-game { font-weight: 700; }
.match-when { margin-left: auto; font-size: .8rem; color: var(--muted); }
.match-seats { list-style: none; margin: 0; padding: 0; display: grid; gap: .3rem; }
.match-seat { display: flex; align-items: center; gap: .55rem; font-size: .9rem; }
/* Your own row, marked without colour alone — the weight carries it too. */
.match-seat[data-you="true"] { font-weight: 700; }
.match-seat[data-you="true"] .match-name { color: var(--accent); }
.match-place {
  min-width: 2.4em; font-size: .78rem; font-weight: 700;
  color: var(--muted); font-variant-numeric: tabular-nums;
}
.match-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.match-score { font-variant-numeric: tabular-nums; font-weight: 650; }

/* ── how to play ──────────────────────────────────────────────────────────
   The GAME supplies this content (DESIGN.md §4); the platform only sizes it for
   reading. Capped at a comfortable measure — long lines are the single easiest
   way to make rules text unreadable. */
.rules { max-width: 62ch; }
.rules h3 {
  margin: 1.5rem 0 .4rem; font-size: .76rem; text-transform: uppercase;
  letter-spacing: .08em; color: var(--muted);
}
.rules p { margin: 0 0 .7rem; }
.rules-lede { font-size: 1.02rem; }
.rules-list { margin: 0 0 .4rem; padding-left: 1.2rem; display: grid; gap: .35rem; }
.rules-list li { line-height: 1.55; }
.rules-note {
  margin-top: 1.4rem; padding: .7rem .85rem;
  background: var(--surface-2); border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm); font-size: .9rem;
}

/* ── the seat picker on the setup screen ──────────────────────────────────
   One decision, every value one press. A number spinner would hide the range
   the game actually allows behind two tiny arrows. */
.seatpick { display: flex; flex-wrap: wrap; gap: .4rem; }
.seatpick-n {
  font: inherit; font-weight: 700; font-variant-numeric: tabular-nums;
  min-width: 2.6rem; min-height: 2.6rem; padding: .3rem .5rem;
  color: var(--text); background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  cursor: pointer; transition: border-color .12s, background .12s;
}
.seatpick-n:hover { border-color: var(--accent-dim); }
.seatpick-n[aria-checked="true"] {
  background: var(--accent); border-color: var(--accent); color: var(--on-accent);
}
html[data-theme="light"] .seatpick-n[aria-checked="true"] {
  background: #8a5f10; border-color: #8a5f10;
}

/* ── the way back to a table you are sitting at ───────────────────────────
   In the top bar because it has to be reachable from every page — that is the
   entire reason it exists (DESIGN.md §4). */
.tablechip {
  display: inline-flex; align-items: center; gap: .4rem;
  margin-left: auto; padding: .3rem .6rem;
  font-size: .82rem; font-weight: 650; text-decoration: none;
  color: var(--text); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 999px;
  white-space: nowrap; max-width: 42vw; overflow: hidden; text-overflow: ellipsis;
}
.tablechip:hover { border-color: var(--accent-dim); }
.tablechip-dot {
  width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--ok); flex: none;
}

.gate-line { margin: 0 0 1.2rem; font-size: .9rem; font-weight: 600; color: var(--text); }
.gate-line[data-blocked="false"] { color: var(--muted); font-weight: 550; }
.gate-line:empty { display: none; }

.start-why { flex: 1 0 100%; margin: .1rem 0 0; text-align: right;
             font-size: .82rem; color: var(--muted); }
.start-why.is-nudge { animation: gn-nudge 900ms ease-out; }
@keyframes gn-nudge { 0%, 100% { color: var(--muted); } 20%, 60% { color: var(--text); } }

/* aria-disabled, NOT disabled — see the JS. Because the control stays focusable
   and stays clickable, it has to stay readable: a colour change rather than the
   opacity used for a genuinely inert button. */
.btn[aria-disabled="true"] { color: var(--muted); cursor: not-allowed; }
.btn[aria-disabled="true"]:active { transform: none; }
.btn-primary[aria-disabled="true"],
.btn-primary[aria-disabled="true"]:hover {
  background: var(--surface-2); border-color: var(--line);
  color: var(--muted); box-shadow: none;
}

/* A table that came back from disk written by a different build of its game.
   It keeps its row, its seats and its state, but it is never handed to the
   engine — a board that is quietly wrong three turns later is far worse than one
   that says so. */
.stale-note {
  margin: 0 0 1.2rem; padding: .8rem .95rem;
  font-size: .9rem; line-height: 1.5;
  background: color-mix(in oklab, var(--live) 10%, var(--surface));
  border: 1px solid color-mix(in oklab, var(--live) 40%, var(--line));
  border-left: 3px solid var(--live);
  border-radius: var(--radius-sm);
}

/* ── motion ───────────────────────────────────────────────────────────────
   Four cues, each attached to a state change worth noticing, each with a static
   carrier so nothing is lost when motion is off. */
.row[data-new="true"] { animation: gn-row-in 180ms cubic-bezier(.2,.7,.3,1); }
@keyframes gn-row-in { from { opacity: 0; translate: 0 -8px; } }
.row[data-started="true"] { animation: gn-row-start 700ms ease-out; }
@keyframes gn-row-start {
  0%, 100% { background: var(--surface); }
  30% { background: color-mix(in oklab, var(--live) 15%, var(--surface)); }
}
.seat[data-arrived="true"] { animation: gn-seat-in 200ms cubic-bezier(.2,.7,.3,1); }
@keyframes gn-seat-in { from { opacity: 0; scale: .94; } }


/* ── Three Thirteen ────────────────────────────────────────────────────────
   Owned by the game, not the platform (DESIGN.md §4). Every rule below is
   about how this one game looks; nothing here is meant to be reused by the
   next one, which may not use the DOM at all.

   Cards are drawn, never fetched: each pip is an inline <svg> filled with
   currentColor. No request, no font gamble, correct in both themes for free. */

/* THE TOKENS ARE ON BOTH ROOTS, and that is not tidiness.
   The reveal overlay is a SIBLING of .tt, not a descendant — it has to be, so a
   repaint of the table cannot destroy it mid-read. Custom properties inherit
   down the tree and nowhere else, so tokens declared on .tt alone reach none of
   the reveal: every card in it collapsed to 1.28px with no radius, no shadow,
   and hearts rendered in --text instead of red, which is the entire information
   content of a suit. Any new token goes on both selectors or it does not exist
   inside the reveal. */
.tt, .tt-reveal {
  /* One knob. Every card dimension derives from it, so the hand, the piles and
     the reveal are one object at four sizes. Tracks the viewport rather than
     stepping — a container query cannot be used here, because an element
     cannot query itself and inline-size containment would imply layout
     containment on .tt, trapping the lifted card's z-index. */
  --tt-card-w: clamp(34px, 2.9vw + 23px, 46px);
  --tt-slot-min: 24px;                            /* the minimum touch target */
  --tt-lift: 10px;

  /* Card red. Game-specific by necessity, and deliberately NOT --live: that is
     the platform's alarm colour, and the reveal needs --live to mean "this cost
     you points". If hearts were --live too, half the reveal would read as an
     alarm and the cost signal would die. 5.25:1 on --surface in dark, 5.93:1 in
     light — both clear AA. */
  --tt-red: #e8615c;

  /* A 34px card under --shadow (a panel shadow) is a smudge. */
  --tt-card-shadow: 0 1px 1px rgb(0 0 0 / .28), 0 4px 10px -6px rgb(0 0 0 / .5);
}
html[data-theme="light"] :is(.tt, .tt-reveal) {
  --tt-red: #c0261f;
  --tt-card-shadow: 0 1px 1px rgb(60 40 10 / .10), 0 4px 10px -6px rgb(60 40 10 / .25);
}

.tt {
  display: flex; flex-direction: column; gap: 1rem;
  position: relative;
  /* A lifted card must still escape upward, so this is only ever horizontal. */
  overflow-x: clip;
}

/* Game-scoped visually hidden text. */
.tt-sr {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* Never display:none — some engines refuse to resolve <use> into a
   display:none subtree. */
.tt-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

.tt-table { display: grid; gap: 1rem 1.4rem; align-items: start; }
@media (min-width: 820px) {
  .tt-table { grid-template-columns: minmax(0, 1fr) clamp(260px, 30%, 320px); }
}
.tt-felt, .tt-rail { display: flex; flex-direction: column; gap: .9rem; min-width: 0; }

/* The dock holds the hand and every commit button under the thumb. Sticky
   bottom, which is why .tt is a flex column and not a grid: a sticky grid item
   in a self-sized row has nowhere to travel. */
.tt-dock {
  position: sticky; bottom: 0; z-index: 6;
  display: flex; flex-direction: column; gap: .5rem;
  padding: .55rem 0 calc(.55rem + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(to top, var(--bg) 82%, transparent);
}

/* ── status strip ─────────────────────────────────────────────────────── */
.tt-status {
  display: flex; align-items: center; gap: .8rem; flex-wrap: wrap;
  padding-bottom: .7rem; border-bottom: 1px solid var(--line-soft);
}
.tt-round { font-weight: 650; font-size: 1.02rem; }
.tt-round span { color: var(--muted); font-size: .8rem; font-weight: 500; }
.tt-counts { margin-left: auto; display: flex; gap: .9rem; font-size: .78rem; color: var(--muted); }
.tt-counts b { font-family: var(--mono); color: var(--text); font-weight: 650; }

/* The wild chip. --accent as SMALL TEXT is a genuine light-theme failure:
   #b07a15 on #f7f2ea is about 3.3:1, which is what the old .tt-wild did — the
   one thing you must not misread was the least legible text on the surface.
   Accent is a FILL here, behind --on-accent, at a size that clears the
   large-text bar; the words stay --text on --surface at 15:1. */
.tt-wildchip {
  display: inline-flex; align-items: center; gap: .4rem; flex: none;
  padding: .12rem .3rem .12rem .12rem; border-radius: 999px;
  border: 1px solid var(--accent-dim); background: var(--surface);
}
/* 1.2rem = 19.2px at the default root size, and bold — deliberately over the
   18.66px-bold threshold at which WCAG treats text as "large" and the AA bar
   drops to 3:1. --on-accent on --accent measures 8.77:1 in dark but only
   3.66:1 in light, so at any smaller size the light theme would fail, and this
   is the one glyph on the surface a player must never misread. Sizing the BOX
   to 24px does not help: the threshold is on the font. */
.tt-wildchip-rank {
  display: grid; place-items: center; min-width: 28px; height: 28px; padding: 0 .3rem;
  font-family: var(--mono); font-size: 1.2rem; font-weight: 700; line-height: 1;
  color: var(--on-accent); background: var(--accent); border-radius: 999px;
}
.tt-wildchip-word {
  font-size: .74rem; font-weight: 650; letter-spacing: .04em; color: var(--text);
}
.tt-dockbar { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; min-height: 26px; }
.tt-turn { margin: 0; font-weight: 600; font-size: .92rem; flex: 1 1 12rem; min-width: 0; }
.tt-turn[data-mine="true"] { color: var(--text); }
.tt-notice { margin: 0; font-size: .86rem; font-weight: 600; color: var(--live); }

.tt-log {
  list-style: none; margin: 0; padding: 0;
  max-height: 4.6rem; overflow-y: auto; overscroll-behavior: contain;
  font-size: .8rem; color: var(--muted); line-height: 1.5;
}
.tt-log li { padding: .1rem 0; }
.tt-log li + li { border-top: 1px solid var(--line-soft); }

/* ── seats ────────────────────────────────────────────────────────────── */
.tt-seats { display: flex; flex-direction: column; gap: .3rem; }
.tt-seat {
  display: flex; align-items: center; gap: .8rem;
  padding: .45rem .8rem; border-radius: var(--radius-sm);
  border: 1px solid transparent; font-size: .9rem;
}
.tt-seat[data-active="true"] { border-color: var(--accent-dim); background: var(--surface); }
.tt-seat[data-me="true"] .tt-seat-name { font-weight: 680; }
.tt-seat-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tt-seat-cards { color: var(--muted); font-size: .82rem; }
.tt-seat-total { font-family: var(--mono); font-weight: 650; min-width: 3ch; text-align: right; }
.tt-rail-head {
  font-size: .72rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 .3rem;
}
.tt-seat-final { font-size: .78rem; color: var(--muted); min-width: 1.2em; text-align: center; }
.tt-seat[data-final="taken"] .tt-seat-final { color: var(--accent); }

/* ── the card ─────────────────────────────────────────────────────────── */
.tt-card {
  position: relative; display: block; box-sizing: border-box;
  /* aspect-ratio, not a --tt-card-h token. A height token is computed where it
     is DECLARED, so overriding --tt-card-w further down the tree (the small
     cards in a meld tray, the 30px cards in a claimed group) changed the width
     and left the height at the outer value — a 26px card stretched to 47.6px.
     Deriving from the element's own width cannot drift that way. */
  width: var(--tt-card-w); aspect-ratio: 5 / 7;   /* 2.5 x 3.5in, the poker ratio */
  padding: 0; font: inherit; line-height: 1; overflow: hidden;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--line); border-radius: calc(var(--tt-card-w) * .12);
  box-shadow: var(--tt-card-shadow);
}
.tt-card[data-suit="2"], .tt-card[data-suit="4"] { color: var(--tt-red); }

/* The index is load-bearing: in an overlapped fan the leftmost ~24px is all you
   see, and the corner index is the part of a real card designed for exactly
   that. Inset past the 3px wild bar. */
.tt-card-corner {
  position: absolute;
  top:  calc(var(--tt-card-w) * .077);
  left: calc(var(--tt-card-w) * .15);
  display: flex; flex-direction: column; align-items: center;
  gap: calc(var(--tt-card-w) * .02);
  pointer-events: none;
}
.tt-card-rank {
  font-size: max(10px, calc(var(--tt-card-w) * .30));
  font-weight: 700; letter-spacing: -.045em;
  font-variant-numeric: lining-nums tabular-nums;
}
.tt-pip { display: block; fill: currentColor; pointer-events: none; }
.tt-pip-sm { width: calc(var(--tt-card-w) * .24); height: calc(var(--tt-card-w) * .24); }
/* Bottom-right, not centred: in a fan this is what gets covered, and it is
   better fully hidden than sliced. */
.tt-pip-lg {
  position: absolute;
  right:  calc(var(--tt-card-w) * .08);
  bottom: calc(var(--tt-card-w) * .07);
  width:  calc(var(--tt-card-w) * .44); height: calc(var(--tt-card-w) * .44);
  opacity: .9;
}
.tt-cards[data-size="sm"] { --tt-card-w: 26px; }
.tt-cards[data-size="sm"] .tt-pip-lg { display: none; }

/* WILD is a fact about the ROUND, not the card. It changes every hand and
   misreading it loses the round, so it gets three channels, and the one that
   survives an 80%-overlapped fan lives in the leftmost 3px. Deliberately not a
   background tint: mixing accent into --surface in dark mode raises the card's
   luminance enough to drop --tt-red below 4.5:1. The third channel is the word
   "wild" in the accessible name. */
.tt-card[data-wild="true"] {
  border-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent), var(--tt-card-shadow);
}

/* SELECTED. aria-pressed on the slot is the state; this is its picture. The
   bottom bar is what makes selection legible in a 24px sliver, and it is
   --text rather than --accent so a selected wild card is not one gold blob. */
.tt-card[data-sel="true"] { border-color: var(--text); }
.tt-card[data-sel="true"]::after {
  content: ""; position: absolute; inset: auto 0 0 0; height: 3px; background: var(--text);
}

.tt-card-back {
  --tt-back-ink: color-mix(in oklab, var(--accent) 34%, transparent);
  background-color: var(--surface-2);
  background-image:
    repeating-linear-gradient( 45deg, var(--tt-back-ink) 0 1px, transparent 1px 6px),
    repeating-linear-gradient(-45deg, var(--tt-back-ink) 0 1px, transparent 1px 6px);
  box-shadow: inset 0 0 0 3px var(--surface), inset 0 0 0 4px var(--line-soft),
              var(--tt-card-shadow);
}
/* Empty is the honest state: nothing is drawn in it. */
.tt-card-slot { background: none; border-style: dashed; box-shadow: none; }

/* Windows High Contrast drops box-shadows and forces colour, taking the wild
   bar and the selected bar with them. Restore both with properties it keeps. */
@media (forced-colors: active) {
  .tt-card { border-color: CanvasText; }
  .tt-pip  { fill: CanvasText; }
  .tt-card[data-wild="true"] { outline: 2px dashed CanvasText; outline-offset: -4px; }
  .tt-card[data-sel="true"]  { outline: 3px solid Highlight;   outline-offset: 1px; }
}

/* ── piles ────────────────────────────────────────────────────────────── */
.tt-piles { display: flex; gap: 1.2rem; align-items: flex-start; }
.tt-pile {
  display: flex; flex-direction: column; align-items: center; gap: .4rem;
  font: inherit; cursor: pointer; padding: .5rem; border-radius: var(--radius-sm);
  background: transparent; border: 1px solid transparent; color: var(--text);
}
.tt-pile:hover:not([aria-disabled="true"]) { border-color: var(--accent-dim); background: var(--surface); }
/* aria-disabled, not disabled: a disabled control is skipped in several
   screen-reader browse modes, which would take the deck count with it. */
.tt-pile[aria-disabled="true"] { cursor: default; opacity: .62; }
.tt-pile-label { font-size: .76rem; color: var(--muted); font-variant-numeric: tabular-nums; }

/* Depth, from the count the server actually sends. Ghost layers are siblings
   painted before the top card — never z-index:-1, which depends on the
   ancestor's background being opaque. */
.tt-pile-stack { position: relative; width: var(--tt-card-w); aspect-ratio: 5 / 7; }
.tt-pile-stack > * { position: absolute; inset-block-start: 0; inset-inline-start: 0; }
.tt-ghost {
  width: var(--tt-card-w); aspect-ratio: 5 / 7;
  border: 1px solid var(--line); border-radius: calc(var(--tt-card-w) * .12);
  background: var(--surface-2);
}
.tt-ghost[data-i="1"] { translate: 2px 2px; }
.tt-ghost[data-i="2"] { translate: 4px 4px; }

/* ── the hand ─────────────────────────────────────────────────────────── */
.tt-groups { display: flex; flex-wrap: wrap; gap: .4rem; }
.tt-groups:empty { display: none; }
.tt-group {
  display: flex; align-items: center; gap: .25rem; padding: .28rem .35rem;
  border: 1px solid var(--accent-dim); border-radius: var(--radius-sm);
  background: color-mix(in oklab, var(--accent) 8%, transparent);
}
.tt-group .tt-slot { --tt-card-w: 30px; }

/* overflow-x forces overflow-y to auto, so the lift and the focus ring are
   paid for with padding rather than allowed to spill. */
.tt-fan-rail {
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-padding-inline: .5rem; scrollbar-width: none;
  padding-block: calc(var(--tt-lift) + 6px) 4px;
}
.tt-fan-rail::-webkit-scrollbar { display: none; }
.tt-fan { display: flex; align-items: flex-start; }

/* Fixed-height slots that shrink, each holding one absolutely-positioned face
   that stays full card width and overflows right. Slots never overlap, so
   hit-testing is unambiguous and a raised card cannot swallow its neighbour's
   strip — the failure that kills naive fans. Faces overlap freely, so it reads
   as a hand. Later slots come later in the DOM, so every card's LEFT strip is
   never covered; that is why the index and the wild bar both live there.
   The browser solves the widths at layout time: no JS reads a width, sets a
   position, or runs after paint. */
.tt-slot {
  position: relative; overflow: visible;
  flex: 0 1 calc(var(--tt-card-w) * 1.1);
  min-width: var(--tt-slot-min);
  height: calc(var(--tt-card-w) * 1.4);
  margin: 0; padding: 0; border: 0; background: none; appearance: none;
  font: inherit; color: inherit; text-align: start;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
button.tt-slot { cursor: pointer; }
/* The right-hand card is always whole; only its neighbours give ground. */
.tt-fan > .tt-slot:last-child { flex: 0 0 var(--tt-card-w); }

.tt-slot > .tt-card {
  position: absolute; inset-block-start: 0; inset-inline-start: 0;
  pointer-events: none;                  /* the slot owns hit-testing */
  transition: transform .12s ease, border-color .12s;
}
.tt-slot[data-sel="true"]     { z-index: 2; }
button.tt-slot:hover          { z-index: 3; }
button.tt-slot:focus-visible  { z-index: 4; outline: none; }
button.tt-slot:hover         > .tt-card,
button.tt-slot:focus-visible > .tt-card,
.tt-slot[data-sel="true"]    > .tt-card { transform: translateY(calc(-1 * var(--tt-lift))); }
button.tt-slot:hover > .tt-card { border-color: var(--accent-dim); }
/* Outline the FACE, not the 24px slot. */
button.tt-slot:focus-visible > .tt-card { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The card you drew this turn, findable after a re-sort drops it mid-fan.
   Carried by outline rather than box-shadow: the wild marker is an inset
   box-shadow on the same element, and two box-shadow declarations do not merge
   — the later one replaced the wild bar outright, so drawing a wild card hid
   the fact that it was wild. */
.tt-slot[data-fresh="true"] > .tt-card {
  outline: 2px solid var(--accent-dim); outline-offset: -1px;
}

.tt-sortbtn { font-variant-numeric: tabular-nums; }
.tt-actions { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; }
.tt-hint { margin: 0; }

/* ── scoreboard ───────────────────────────────────────────────────────── */
.tt-board { background: var(--surface); border: 1px solid var(--line);
            border-radius: var(--radius); overflow: hidden; }
.tt-board-head { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
                 padding: .65rem .75rem; }
.tt-board-head h3 { margin: 0; font-size: .92rem; font-weight: 650; }
/* Lowest total wins inverts every scoreboard instinct, so the rule is stated
   permanently and never collapsed away. */
.tt-board-rule {
  font-size: .66rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--text); padding: .12rem .5rem; border-radius: 999px;
  border: 1px solid var(--accent-dim);
  background: color-mix(in oklab, var(--accent) 14%, transparent);
}
.tt-board-toggle { margin-left: auto; }
.tt-board-empty { margin: 0; padding: 0 .75rem .75rem; font-size: .85rem; color: var(--muted); }

.tt-rank { list-style: none; margin: 0; padding: 0 .5rem .55rem;
           display: flex; flex-direction: column; gap: .1rem; }
.tt-rank-row { display: flex; align-items: baseline; gap: .55rem;
               padding: .3rem .45rem; border-radius: var(--radius-sm);
               border: 1px solid transparent; font-size: .88rem; }
.tt-rank-row[data-lead="true"] { border-color: var(--accent-dim);
  background: color-mix(in oklab, var(--accent) 9%, transparent); }
.tt-rank-row[data-me="true"] .tt-rank-name { font-weight: 680; }
.tt-rank-name { flex: 1; min-width: 0; overflow: hidden;
                text-overflow: ellipsis; white-space: nowrap; }
.tt-rank-total { font-family: var(--mono); font-variant-numeric: tabular-nums;
                 font-size: 1rem; font-weight: 700; min-width: 3.5ch; text-align: right; }
.tt-rank-gap { font-family: var(--mono); font-variant-numeric: tabular-nums;
               font-size: .76rem; color: var(--muted); min-width: 4.5ch; text-align: right; }
.tt-lead { color: var(--text); border-color: var(--accent-dim); }

.tt-board-scroll { overflow: auto; max-height: min(46vh, 19rem);
                   border-top: 1px solid var(--line-soft); overscroll-behavior: contain; }
.tt-grid { width: 100%; min-width: 18rem; table-layout: fixed;
           border-collapse: collapse; font-size: .8rem; }
.tt-grid th, .tt-grid td { padding: .26rem .3rem; text-align: right; }
.tt-grid th:first-child, .tt-grid td:first-child { width: 4.2rem; text-align: left; }
/* No `display` override on any table part: that is the standard way to lose
   table semantics in assistive tech, which is the whole reason a real <table>
   was chosen. */
.tt-grid thead th { position: sticky; top: 0; z-index: 1;
  background: var(--surface-2); border-bottom: 1px solid var(--line);
  font-size: .72rem; font-weight: 650; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tt-grid thead th[data-me="true"] { color: var(--text); font-weight: 750; }
.tt-grid tbody th { color: var(--muted); font-weight: 600; white-space: nowrap; }
.tt-grid-rn { font-family: var(--mono); color: var(--text); }
.tt-grid-wild {
  display: inline-block; margin-left: .28rem; min-width: 1.5em; text-align: center;
  font-family: var(--mono); font-size: .7rem; font-weight: 700; color: var(--text);
  padding: 0 .16rem; border: 1px solid var(--accent-dim); border-radius: 4px;
  background: color-mix(in oklab, var(--accent) 12%, transparent);
}
.tt-grid tbody td { font-family: var(--mono); font-variant-numeric: tabular-nums;
                    border-bottom: 1px solid var(--line-soft); }
/* Heat bands are the game's own constants: 50 is exactly one unmelded wild, 15
   an unmelded ace, so 25+ means real cards were held. The number stays --text,
   so the tint never costs contrast. */
.tt-grid td[data-heat="0"] { background: color-mix(in oklab, var(--accent) 11%, transparent); font-weight: 700; }
.tt-grid td[data-heat="1"] { background: color-mix(in oklab, var(--live) 8%, transparent); }
.tt-grid td[data-heat="2"] { background: color-mix(in oklab, var(--live) 18%, transparent); }
.tt-grid td[data-heat="3"] { background: color-mix(in oklab, var(--live) 30%, transparent); font-weight: 700; }
.tt-grid td[data-heat="none"] { color: var(--muted); }
.tt-grid td[data-out="true"] { box-shadow: inset 0 -2px 0 var(--accent); }
.tt-grid tfoot th, .tt-grid tfoot td {
  position: sticky; bottom: 0; z-index: 1;
  background: var(--surface-2); border-top: 1px solid var(--line);
  font-family: var(--mono); font-variant-numeric: tabular-nums; font-weight: 700; }
.tt-grid tfoot td[data-lead="true"] { color: var(--text); box-shadow: inset 0 -3px 0 var(--accent); }
.tt-board-legend { margin: 0; padding: .5rem .75rem .65rem; font-size: .74rem;
  line-height: 1.5; color: var(--muted); border-top: 1px solid var(--line-soft); }

/* ── the round-end reveal ─────────────────────────────────────────────────
   seatScored fires once per seat and is the one moment every hand is public.
   The server's melds, the server's leftovers, the server's numbers. */
.tt-reveal {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  padding: clamp(.5rem, 3vw, 2rem);
  background: color-mix(in oklab, var(--bg) 84%, transparent);
}
@supports (backdrop-filter: blur(3px)) { .tt-reveal { backdrop-filter: blur(3px); } }
.tt-reveal-panel {
  display: flex; flex-direction: column;
  width: min(880px, 100%); max-height: min(88vh, 900px);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.tt-reveal-head { flex: none; padding: .95rem 1.05rem .75rem;
                  border-bottom: 1px solid var(--line-soft); }
.tt-reveal-head h3 { margin: 0; font-size: 1.02rem; font-weight: 650; }
.tt-reveal-sub { margin: .3rem 0 0; font-size: .86rem; color: var(--muted); }
.tt-reveal-body { flex: 1 1 auto; min-height: 0; overflow-y: auto;
                  overscroll-behavior: contain; padding: .9rem 1.05rem; }
.tt-reveal-body:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.tt-reveal-grid { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem;
                  grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); }

.tt-rs { padding: .65rem .75rem .75rem; border-radius: var(--radius-sm);
         border: 1px solid var(--line-soft); background: var(--surface-2); }
.tt-rs[data-me="true"] { border-color: var(--accent-dim); }
.tt-rs-head { display: flex; align-items: baseline; gap: .5rem; margin-bottom: .45rem; }
.tt-rs-name { flex: 1; min-width: 0; font-weight: 650; font-size: .9rem;
              overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tt-rs-pts { font-family: var(--mono); font-weight: 700; font-size: 1.25rem; color: var(--live); }
.tt-rs-pts[data-zero="true"] { color: var(--accent); }
.tt-rs-total { font-family: var(--mono); font-size: .78rem; color: var(--muted); }

.tt-rs-label { display: block; margin: .5rem 0 .3rem;
  font-size: .68rem; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; }
/* Cost is carried by the container, the label and the number — never by
   tinting the cards, because red already means hearts. */
.tt-rs-cost { padding: .45rem .5rem; border-radius: var(--radius-sm);
  background: color-mix(in oklab, var(--live) 9%, transparent);
  border: 1px solid color-mix(in oklab, var(--live) 38%, var(--line)); }
.tt-rs-cost .tt-rs-label { color: var(--live); margin-top: 0; }
/* No opacity here. Dimming the whole block also dims the cards inside it, and
   card red measured below the 4.5:1 floor once multiplied through. The melds
   recede by being smaller and quieter-labelled instead. */
.tt-rs-melds { display: flex; gap: .4rem; flex-wrap: wrap; }
.tt-rs-melds .tt-rs-label { color: var(--muted); flex: 1 1 100%; }
.tt-cards { display: flex; gap: .16rem; flex-wrap: wrap; align-items: flex-start; }
/* NOT `position: static`. The corner index and the large pip are absolutely
   positioned inside the card, so removing the card's containing block sends
   them to the nearest positioned ancestor — which in the reveal is the fixed
   overlay itself. The card is already in normal flow here; `relative` costs it
   nothing and is what its children are measured against. */
.tt-meld { padding: .25rem; border: 1px solid var(--line); border-radius: var(--radius-sm);
           background: var(--surface); display: flex; gap: 2px; }
.tt-rs-none { margin: 0; font-size: .82rem; color: var(--muted); }
.tt-rs-pen { font-family: var(--mono); font-size: .68rem; color: var(--muted); }
.tt-rs-card { display: inline-flex; flex-direction: column; align-items: center; gap: .1rem; }

.tt-rs-reject { margin-top: .6rem; padding: .55rem .6rem; border-radius: var(--radius-sm);
  border: 1px solid color-mix(in oklab, var(--live) 40%, var(--line));
  background: color-mix(in oklab, var(--live) 8%, transparent); }
.tt-rs-reject h4 { margin: 0 0 .35rem; font-size: .82rem; font-weight: 650; color: var(--live); }
.tt-rs-reject .hint { margin-top: .4rem; }
.tt-rs-unknown { width: var(--tt-card-w); aspect-ratio: 5 / 7;
  border: 1px dashed var(--line); border-radius: calc(var(--tt-card-w) * .12); }

.tt-reveal-foot { flex: none; display: flex; align-items: center; gap: .8rem; flex-wrap: wrap;
  padding: .75rem 1.05rem; border-top: 1px solid var(--line-soft); }
.tt-reveal-next { flex: 1 1 12rem; min-width: 0; margin: 0; font-size: .84rem; color: var(--muted); }
.tt-reveal-foot .btn { margin-left: auto; }

/* ── motion ───────────────────────────────────────────────────────────────
   Motion tells you what changed. Nothing eases for feel, and every cue has a
   static carrier, so the surface is fully legible with motion off. */
/* Deliberately not a card-flip. rotateX(90deg) turns the glyph edge-on, which
   makes the one thing a player must not misread INVISIBLE for the length of the
   animation — and if the animation is ever delayed (a throttled background tab,
   a context that is not compositing) it stays that way. Every frame here is
   legible, which is the rule the rest of this file holds itself to. */
.tt-wildchip.is-new .tt-wildchip-rank { animation: tt-flip 320ms ease-out; }
@keyframes tt-flip {
  from { transform: scale(.72); opacity: .5; }
  60%  { transform: scale(1.08); opacity: 1; }
}
.tt-seat.is-turn { animation: tt-turn 600ms ease-out; }
@keyframes tt-turn {
  0%, 100% { background: transparent; }
  25% { background: color-mix(in oklab, var(--accent) 16%, transparent); }
}
.tt-pile .tt-card.is-drop { animation: tt-drop 190ms cubic-bezier(.2,.7,.3,1); }
@keyframes tt-drop { from { translate: 0 -14px; scale: 1.06; opacity: .4; } }
.tt-slot[data-fresh="true"] > .tt-card { animation: tt-in 170ms ease-out; }
@keyframes tt-in { from { opacity: 0; translate: 0 12px; scale: .96; } }
.tt-reveal { animation: tt-fade 180ms ease-out; }
.tt-reveal-panel { animation: tt-rise 200ms cubic-bezier(.2,.7,.3,1); }
@keyframes tt-fade { from { opacity: 0; } }
@keyframes tt-rise { from { opacity: 0; translate: 0 8px; } }

/* ── a short viewport: a phone turned sideways ─────────────────────────────
   At 812x375 the dock was 48% of the screen and the Discard and Go out buttons
   started BELOW the fold — you had to scroll before the sticky dock pinned
   itself, which defeats the one thing the dock is for.

   The fix is a column that fills the window: the topbar takes its own height,
   the page takes the rest, and the GAME'S SURFACE scrolls inside that. No magic
   topbar number — hard-coding the height of platform chrome is exactly the
   coupling this file avoids elsewhere — and nothing reaches into the game's own
   classes: #gameSurface is the platform's element, and what the game does inside
   it stays the game's business. Its sticky dock then pins to the bottom of the
   surface, which is the bottom of the screen.

   Keyed on height, not orientation: a short window on a laptop has exactly the
   same problem. */
@media (max-height: 560px) {
  /* height, not min-height, and the page stops scrolling as a whole. A flex
     column whose height is only a MINIMUM still grows with its content, so the
     children never get a remainder to fill — which is why the first attempt at
     this left the dock exactly where it was. */
  /* Both, because overflow only propagates from body to the viewport when html
     leaves it alone — and something in this tree does not. Stated on both rather
     than relying on the propagation rule. */
  html { height: 100%; overflow: hidden; }
  body { height: 100dvh; overflow: hidden; }
  /* The column is #app, NOT body. The shell is injected into a wrapper div, so
     body's only flex child is that wrapper — making body the column left every
     page unconstrained and changed nothing. */
  #app { display: flex; flex-direction: column; height: 100%; min-height: 0; }
  #gate { min-height: 0; overflow-y: auto; }
  .topbar, .banner, .boot { flex: none; }
  .page { padding-top: .7rem; padding-bottom: 0; }

  /* Each page takes the remainder and scrolls inside itself. */
  #pageHome, #pageProfile { flex: 1; min-height: 0; overflow-y: auto; }
  #pageTable { flex: 1; min-height: 0; display: flex; flex-direction: column; }
  #gameSurface { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }

  /* A smaller hand buys back the vertical room the dock needs. This is the one
     game-facing value here, and it is the knob the game itself exposes for it. */
  .tt { --tt-card-w: clamp(28px, 2.2vw + 20px, 36px); --tt-lift: 7px; }
  .tt-status { padding-bottom: .45rem; }
  .tt-log { max-height: 2.2rem; }
  .tt-hint { display: none; }
}

@media (pointer: coarse) { .tt-actions .btn { min-height: 44px; } }
@media (max-width: 560px) {
  .tt-log { max-height: 2.4rem; }
  .tt-reveal { padding: 0; }
  .tt-reveal-panel { width: 100%; height: 100%; max-height: 100%; border: 0; border-radius: 0; }
  .tt-reveal-grid { grid-template-columns: 1fr; }
}
/* NO FULL-BLEED HAND RAIL. It was here, pulling the rail out to 100vw to
   reclaim the platform's page gutter, and it did not work: .tt clips
   horizontally (it has to, or the rail's own overflow escapes the page), so the
   reclaimed strip was clipped straight back off. Measured at 375px: the rail
   reported 375px wide while .tt was 346px, and elementFromPoint at the leftmost
   card returned the page, not the card — the card was painted nowhere and
   could not be tapped. Living inside .tt costs about 2px per card at fourteen
   cards and every card stays hittable. */

/* ── responsive ────────────────────────────────────────────────────────── */
@media (max-width: 560px) {
  .who-name { display: none; }
  .table-actions { margin-left: 0; width: 100%; justify-content: flex-start; }
  .start-why { text-align: left; }
  .firstrun-form { flex: 1 1 100%; }
  .codejoin .field { flex: 1 1 100%; }
  .codejoin .btn { width: 100%; }
}
/* Enumerated targets, never a bare .btn: the game renders six .btn btn-sm
   controls of its own, and growing those eats its measured mobile budget. */
@media (pointer: coarse) {
  .row-action .btn, .table-actions .btn, .invite-code,
  .codejoin .btn, .firstrun-form .btn { min-height: 44px; }
  .who { min-height: 44px; padding-inline: .55rem; }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
  /* The clamp above leaves a zero-duration animation, which can still flash on
     a freshly-created node. These are the places that show. */
  .tt-reveal, .tt-reveal-panel { animation: none; }
  .row[data-new="true"], .row[data-started="true"],
  .seat[data-arrived="true"], .start-why.is-nudge { animation: none; }
}

/* ── Jingle Guesser ────────────────────────────────────────────────────────
 *
 * Prefixed `jg-`, alongside Three Thirteen's `tt-`. There is no shadow DOM and
 * no per-game stylesheet loading, so the prefix IS the isolation and a bare
 * element selector here would restyle the shell. */

.jg { display: flex; flex-direction: column; gap: .9rem; align-items: stretch; }

.jg-phase {
  margin: 0; text-align: center; font-weight: 650; font-size: 1.05rem;
  min-height: 1.6em;   /* reserved, so the layout does not jump each phase */
}
.jg-status { margin: 0; text-align: center; font-size: .82rem; color: var(--muted); }
.jg-small { display: block; font-size: .78rem; color: var(--muted); margin-bottom: .3rem; }

.jg-buzzer {
  font: inherit; font-weight: 800; font-size: 1.5rem; letter-spacing: .06em;
  padding: 1.4rem; min-height: 5.5rem; border-radius: var(--radius);
  color: var(--on-accent); background: var(--accent); border: 1px solid var(--accent);
  cursor: pointer;
}
.jg-buzzer:disabled { opacity: .4; cursor: not-allowed; }
.jg-buzzer:hover:not(:disabled) { filter: brightness(1.08); }

.jg-answer { display: flex; flex-direction: column; gap: .6rem; }
.jg-answer input[type="text"] {
  font: inherit; width: 100%; min-height: 44px; padding: .6rem .75rem;
  color: var(--text); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.jg-primary {
  font: inherit; font-weight: 650; min-height: 44px; padding: .6rem 1rem;
  color: var(--on-accent); background: var(--accent);
  border: 1px solid var(--accent); border-radius: var(--radius-sm); cursor: pointer;
}

/* The multiple-choice fallback. auto-fit keeps four options on one row on a
 * laptop and stacks them on a phone, where a flex row leaves a lone orphan. */
.jg-choices {
  display: grid; gap: .6rem;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
.jg-choice {
  font: inherit; font-weight: 600; text-align: left; overflow-wrap: anywhere;
  min-height: 3.5rem; padding: .75rem 1rem;
  color: var(--text); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--radius-sm); cursor: pointer;
}
.jg-choice:hover:not(:disabled) { border-color: var(--accent); }
.jg-choice:disabled { opacity: .45; cursor: not-allowed; }
/* Deliberately NO accent/selected variant. Exactly one of these is secretly
 * correct, so any per-option emphasis is a tell. All four stay identical. */
