/*
 * Shared design tokens (docs/ARCHITECTURE.md "Seams reserved for the roadmap"; docs/MIGRATION.md
 * step 14). Every game page links this file first, then base.css, then (a shell game) shell.css,
 * then its own theme.css. Only custom properties belong in this file (test/tokens.test.ts holds it
 * to that).
 *
 * The vocabulary is gin's palette, the look the roadmap restyles Fidice towards, so gin's values
 * are canonical and gin's theme.css declares none of these names (single source). Fidice's
 * theme.css, linked last, redeclares every one of them onto its own palette (its seven of the
 * same name, and --bg --card-2 --text --danger aliased onto --mist --cream-2 --ink --red), so the
 * shared names resolve to fidice's own look until the restyle drops those overrides.
 * CONTRACT.md "Tokens" tables the canonical value beside each override. The computed-style
 * goldens (test/fixtures/styles/<game>.<WxH>.json) pin every declared custom property and its
 * :root value; a name added here is a note in tools/parity/computed-styles.ts --check until the
 * goldens are re-recorded, a changed value a difference. Gin's layout tokens (--card-w --mini-w
 * --pile-w --tiny-w) are gin-only and stay in its theme.css. The second block is the shell's
 * roles, read by shell.css alone (docs/design/dry-round-2.md G1).
 */
:root {
  --bg: #0f2318;
  --card: #163526;
  --card-2: #1c4030;
  --accent: #4ade80;
  --accent-dark: #22c55e;
  --gold: #fbbf24;
  --text: #f2fdf6;
  --muted: #9cc9ac;
  --danger: #f87171;
  /* The call to action (docs/ARCHITECTURE.md "Calls to action"): the fill and the label of every
     button that opens, joins or starts a game (.btn-go, styled in each theme). A light olive-lime,
     apart from the accent green so the start button reads apart from Take and Discard; fidice and
     backgammon redeclare both onto their palettes. */
  --go: #c2d06d;
  --go-text: #10281a;
  --radius: 18px;
  --felt: radial-gradient(ellipse at 50% 30%, #1f5a3a 0%, #123a26 55%, #0b2418 100%);
  /* The shell's roles (web/shared/styles/shell.css; docs/design/dry-round-2.md G1): where the two
     shell themes gave one shell rule two values that differ by palette, radius or face, the rule
     reads one of these and each theme supplies its own. Gin's values here (its theme.css declares
     none of them: single source, as with the palette); backgammon's and fidice's :root redeclare
     every one (fidice's on its own palette; its page links shell.css since
     docs/design/fidice-shell-adoption.md M1). CONTRACT.md "Shell tokens" tables the canonical value
     beside each override. */
  --font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-display: ui-monospace, Menlo, monospace;
  --surface-shell: var(--card);
  --surface-bar: rgba(0, 0, 0, 0.22);
  --fill: var(--card-2);
  --fill-hover: rgba(255, 255, 255, 0.07);
  --ink-on-fill: var(--text);
  --ink-on-bar: var(--muted);
  --ink-hover: var(--text);
  --emphasis: var(--gold);
  --shadow-shell: 0 4px 14px rgba(0, 0, 0, 0.25);
  --radius-control: 14px;
  --radius-inner: 12px;
  --radius-tab: 10px;
  /* The pass-and-play curtain's scrim (`.overlay.curtain`, shell.css). Opaque by default: a hand
     face up beneath the curtain (uno, flip7) must not read through it to the next seat, so a theme
     without a curtain rule of its own gets a wall, not a wash. Gin's felt's darkest stop (the value
     gin's curtain painted inline before the token). A theme whose table may stay readable beneath (briscola's hand paints
     backs, backgammon's position is public) redeclares it translucent. */
  --curtain-scrim: #0b2418;
}
/*
 * Shared primitives (docs/MIGRATION.md step 14): the rules both game themes carried with the same
 * computed result, moved here once and linked by each page after tokens.css and before its own
 * theme.css, so a theme rule of equal specificity still wins. Only what was identical in both
 * themes belongs here; a primitive the games style differently (buttons, inputs, labels, toasts,
 * overlays) stays in each theme.css until a restyle makes them agree. The computed-style goldens
 * (test/fixtures/styles) are the oracle for every move.
 */
* {
  box-sizing: border-box;
}
html,
body {
  margin: 0;
}
.hidden {
  display: none !important;
}
/*
 * The shell's stylesheet (docs/design/dry-round-2.md G1; docs/design/shared-shell.md P13): the
 * rules gin's and backgammon's theme.css carried twice for the screens the shared shell paints
 * (the page, the home screen's boxes, buttons, fields and tabs, the waiting rooms, the overlays and
 * sheets, the curtain, the toast), moved here once. A shell page links tokens.css, base.css, this
 * file, then its theme.css, so a theme rule of equal specificity still wins; fidice does not link
 * it before its restyle (its theme reuses thirty of these names with its own values).
 *
 * Token-only, by construction: a declaration is here because both themes computed the same value
 * from it, or because the two values differ by a role a token names (tokens.css declares the shell
 * tokens with gin's values, backgammon's :root redeclares every one; CONTRACT.md "Shell tokens").
 * The declarations where the two type scales differ (font sizes and weights, letter-spacing, a
 * padding or a margin) stay in each theme under the same selector, as does what one game alone
 * declares (gin's .card-box hairline and .sheet border, backgammon's `font-family: inherit` on every
 * control and its panel shadows), so every computed value on every selector of the four shell
 * goldens (test/fixtures/styles/{gin-rummy,backgammon}.*.json) is what it was before the move;
 * one type scale is G5, with the Fidice restyle. The rules keep the themes' order, and a rule a
 * theme keeps under one of these selectors follows all of them, so a later rule of equal
 * specificity still wins where it did (the submenu's hovered entry against its active one).
 */
* {
  -webkit-tap-highlight-color: transparent;
}
/* A tap on a control is a tap: `manipulation` leaves the pan and the pinch to the browser and takes
   double-tap zoom, so a quick second tap is delivered at once instead of held for a zoom. The
   pages' viewport meta allows a zoom (2026-09-29: `maximum-scale=1.0, user-scalable=no` dropped,
   a WCAG 1.4.4 failure iOS ignored anyway), so this is what keeps a tap-heavy screen from zooming;
   each game's theme.css says the same of its table's tap targets, and `none` on a dragged card or
   checker still wins there (the intersection along the ancestors is what the browser allows). */
.btn,
.icon-btn,
.tab-btn,
.tab-submenu button,
.mode-btn,
.chip,
input,
select,
textarea,
summary {
  touch-action: manipulation;
}
/* A surface the pointer-drag kernel listens on (web/shared/edge/drag.ts `bindDrag` marks each of
   its `surfaces`: gin's hand and table melds, backgammon's board, briscola's hand, hive's board and
   trays). A long press there never selects text, and no selection is left standing for the next
   press to turn into a native drag (the kernel's `preventDefault` on a picked press is the other
   half; gin's layoff spec on the Linux runners). The children (the cards, checkers and tiles the
   kernel picks) read `none` through `auto`; text a player may want to copy (the room code, the
   rules, the names) sits outside every surface. */
.drag-surface {
  -webkit-user-select: none;
  user-select: none;
}
html,
body {
  padding: 0;
  min-height: 100%;
  color: var(--text);
  font-family: var(--font-body);
  overscroll-behavior-y: contain;
}
#app {
  max-width: 480px;
  margin: 0 auto;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
/* The column is a bucket rule (docs/design/layout-buckets.md §3; the boot writes the bucket on
   `<body data-layout>`, web/shared/edge/screen.ts `applyLayout` over web/shared/lib/layout.ts):
   `phone-upright` keeps the 480px column above; every other bucket (a phone sideways, a tablet,
   a desktop window) reads the theme's `--column-wide`, so a game that lays its screens out per
   bucket names `none` there (`:root { --column-wide: none }`, or under one bucket alone:
   `body[data-layout="phone-sideways"] { --column-wide: none }`) and `#app` takes the full width
   the gutters or the frame leave; a game that has not adopted keeps its column everywhere, since
   a phone's column stretched across a laptop is worse than a phone's column. Read with a
   fallback and declared nowhere, as `--gutter` is (test/tokens.test.ts). `:where()` keeps #app's
   own specificity, so a theme's own `#app { max-width }` (fidice's `none`) still wins. */
:where(body[data-layout]:not([data-layout='phone-upright'])) #app {
  max-width: var(--column-wide, 480px);
}
h1 {
  text-align: center;
}
.subtitle {
  text-align: center;
  color: var(--muted);
}
/* Every box of the shell: the Play panel's cards, the resume box, the waiting rooms, the endgame. */
.card-box {
  background: var(--surface-shell);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 12px;
  box-shadow: var(--shadow-shell);
}
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: none;
  border-radius: var(--radius-control);
  font-weight: 700;
  cursor: pointer;
  font-size: 1rem;
  padding: 14px 18px;
  min-height: 54px;
  transition:
    transform 0.08s ease,
    filter 0.15s ease;
  user-select: none;
  color: var(--text);
}
.btn:active {
  transform: scale(0.96);
  filter: brightness(0.95);
}
.btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  transform: none;
}
/* Each theme paints the primary's fill and label; the size is the shell's. */
.btn-primary {
  width: 100%;
  font-size: 1.1rem;
}
/* The call to action (docs/ARCHITECTURE.md "Calls to action"; the owner, 2026-09-24: "all the
   'start game' buttons should be green and should stand out well"): every button that opens,
   joins or starts a game, and only those, in the shared --go and --go-text at .btn-primary's type
   size. Each theme adds its own hover, ring or edge. A theme's .btn-primary fill follows this file,
   so the two classes are never combined: a button is one or the other. */
.btn-go {
  background: var(--go);
  color: var(--go-text);
  font-size: 1.1rem;
}
.btn-secondary {
  background: var(--fill);
}
.btn-ghost {
  background: transparent;
  color: var(--muted);
}
.btn-block {
  width: 100%;
}
.btn-sm {
  padding: 10px 12px;
  min-height: 44px;
}
.btn small {
  opacity: 0.8;
}
input[type='text'],
input[type='number'],
select,
textarea {
  width: 100%;
  border-radius: var(--radius-control);
  color: var(--text);
  outline: none;
}
input::placeholder {
  color: var(--muted);
  font-weight: 400;
}
/* A prefilled default (`data-default`, ui/home.ts `fillInputs`: the game's name, shown because
   nothing is remembered) reads as a suggestion, in the placeholder's ink and italic; the first tap
   clears it and drops the mark, and a typed or remembered name is upright in --text. */
input[data-default='1'] {
  color: var(--muted);
  font-style: italic;
}
label {
  font-size: 0.78rem;
  color: var(--muted);
  font-weight: 700;
  text-transform: uppercase;
}
.row {
  display: flex;
  gap: 10px;
  align-items: center;
}
.row + .row {
  margin-top: 10px;
}
/* The topbar's two button rows (web/shared/markup/topbar.ts): the icon buttons closer than a form row's fields. */
.row.tight {
  gap: 6px;
}
.grow {
  flex: 1;
}
.icon-btn {
  background: var(--fill);
  border: none;
  color: var(--text);
  border-radius: var(--radius-inner);
  width: 44px;
  height: 44px;
  font-size: 1.1rem;
  flex-shrink: 0;
}
.code-input {
  text-align: center;
  letter-spacing: 0.4em;
  font-size: 1.5rem !important;
  text-transform: uppercase;
  font-family: var(--font-display);
}
.room-code {
  font-family: var(--font-display);
  font-size: 3rem;
  text-align: center;
  color: var(--emphasis);
}
.empty-note {
  color: var(--muted);
  text-align: center;
}
/* The waiting rooms' status line (shellPaint.ts `paintWaiting` toggles it). */
.pulse {
  animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.45;
  }
}
/* At the table the page never scrolls (shellPaint.ts `paintScreen` sets `fixed-screen`); each
   theme's short-viewport fallback lifts it, after this file, where its floor sizes cannot fit. */
body.fixed-screen,
body.fixed-screen #app {
  height: 100dvh;
  max-height: 100dvh;
  min-height: 0;
  overflow: hidden;
}
/* At the table a pull past an edge is neither a refresh nor a rubber band, and a long press on a
   checker or a card brings up no callout (the property inherits, so the body's covers the table). */
body.fixed-screen {
  overscroll-behavior: none;
  -webkit-touch-callout: none;
}

/* ---------- the screen frame (docs/design/screen-frame.md) ----------
   A game that opts in (page.ts `frame: true`, so the composed `<body>` carries `data-frame`;
   backgammon does) gets a band along the viewport's edge with a hairline on its inner side, fixed
   and inert, and #app padded so nothing it holds touches the band. The owner (2026-09-28): "The
   application should just be able to put the border in without considering anything else." Three
   tokens dress it (the theme sets them on :root, after this file): `--frame-band` the band's width,
   `--frame-color` its colour, `--frame-hairline` the hairline's colour (transparent for none); a
   fourth, `--frame-gap`, is the air between the hairline and the content (4px unless the theme
   says). It paints at z-index 0, under every positioned thing (flyers 40, overlays 50, the gate
   90, the toast 100). Its corners follow the screen's: under viewport-fit=cover the band reaches the glass, whose
   corners are round on every notched phone, and no CSS or JS API says by how much
   (screen-frame.md §2). The boot writes the four `--frame-corner-*` on the root (screen.ts
   `applyFrame`: the device catalogue's radius at each corner the page provably reaches, 0 at each a
   browser bar owns, re-read as the bar shows and hides); until it has, and on a page without it,
   the fallback below stands: the largest of the top and side insets, which tracks the radius on a
   notched iPhone within a few px and is 0 wherever the page's corners are the browser's (an upright
   tab, an Android tab, the SE, a desktop). The four insets are declared here so the boot can read
   them substituted off the root's computed style (a `--custom` property carries its `env()`
   resolved) and a measurement can override them inline (tools/shell-emulate.ts `seamScript`). */
:root {
  --frame-band: 6px;
  --frame-color: var(--accent);
  --frame-hairline: transparent;
  --frame-hairline-w: 1px;
  --frame-gap: 4px;
  --frame-inset-top: env(safe-area-inset-top, 0px);
  --frame-inset-right: env(safe-area-inset-right, 0px);
  --frame-inset-bottom: env(safe-area-inset-bottom, 0px);
  --frame-inset-left: env(safe-area-inset-left, 0px);
  --frame-corner: max(var(--frame-inset-top), var(--frame-inset-left), var(--frame-inset-right));
  --frame-corner-tl: var(--frame-corner);
  --frame-corner-tr: var(--frame-corner);
  --frame-corner-br: var(--frame-corner);
  --frame-corner-bl: var(--frame-corner);
}
body[data-frame]::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  border: var(--frame-band) solid var(--frame-color);
  /* The band's outer edge; the inset hairline follows the padding box's smaller radius. */
  border-radius: var(--frame-corner-tl) var(--frame-corner-tr) var(--frame-corner-br)
    var(--frame-corner-bl);
  box-shadow: inset 0 0 0 var(--frame-hairline-w) var(--frame-hairline);
}
/* The clearance: the band, the hairline and the gap, or the safe-area inset where that is more
   (the notch, the home indicator). `:where()` keeps this at #app's own specificity, so a theme's
   later `#app { padding }` (backgammon's sideways table: one symmetric `--edge` for both sides and
   its own vertical budget) still wins. */
:where(body[data-frame]) #app {
  --frame-clear: calc(var(--frame-band) + var(--frame-hairline-w) + var(--frame-gap));
  padding: max(var(--frame-clear), var(--frame-inset-top))
    max(var(--frame-clear), var(--frame-inset-right))
    max(var(--frame-clear), var(--frame-inset-bottom))
    max(var(--frame-clear), var(--frame-inset-left));
}
/* The unframed page's clearance (docs/design/shared-shell.md §6.7; the space audit's `gutter` rule,
   docs/design/space-audit.md §5): the theme's gutter, or the safe-area inset where that is more,
   so nothing sits under the notch or the home indicator in a tab, installed or fullscreen. The
   gutter is the theme's `--gutter` on `:root` (gin's and briscola's 12px; a side of its own as
   `--gutter-<side>`, gin's 16px foot), 0 where a theme names none (fidice, whose header and main
   pad themselves; the sandbox with its frame off), so without an inset nothing moves and the
   goldens stand. Read with fallbacks and declared nowhere: this file declares no property but the
   frame's (test/tokens.test.ts), and the audit reads the same `--gutter` off `#app`, where it
   inherits (tools/space-audit/judge.ts `gutterToken`). The insets are the four `--frame-inset-*` above:
   `env()` on a phone, the seam's inline values in a measurement, live on every page that links
   this file with nothing for the boot to write (it writes the corners alone, under `data-frame`).
   Top and bottom are #app's, where the column always spans the screen. The sides are the body's,
   below: the column is 480px centred in a sideways phone's 812-956px, and a side inset (47-62px
   at the notch, both sides on iOS) padded into the column would take 70-100px of it for a notch it
   never reaches; on the body the column centres in the room the notch leaves and keeps its
   gutter, and a full-bleed page (fidice) clears the notch with it. `:where()` keeps #app's own
   specificity, as the framed rule does, so a theme's later `#app { padding }` still wins. */
:where(body:not([data-frame])) #app {
  padding: max(var(--gutter-top, var(--gutter, 0px)), var(--frame-inset-top))
    var(--gutter-right, var(--gutter, 0px))
    max(var(--gutter-bottom, var(--gutter, 0px)), var(--frame-inset-bottom))
    var(--gutter-left, var(--gutter, 0px));
}
body:not([data-frame]) {
  padding-right: var(--frame-inset-right);
  padding-left: var(--frame-inset-left);
}

/* ---------- flights, overlays and sheets ---------- */
/* A clone in flight (web/shared/edge/motion.ts `launchClone`, the class the caller passes; CONTRACT.md's
   shell row): fixed on the body, inert, and UNDER the overlays. Nothing that flies outranks a sheet:
   the curtain is opaque so the seat that just played is hidden, and a flight through it would leak
   the card; on UNO the last play of a game wins it, so the tile glides while the result sheet opens
   (dry-review-2026-10.md §5.1). The stacking order, bottom up: the frame band 0, the tab submenu
   30, flyers 40, overlays 50, the gate 90, the toast 100. Each theme adds the glide itself (its
   transition, backgammon's lift) and nothing of the stacking. `body >` because the clone is a
   copy of a table element and keeps its class: the theme's own rule for that element (briscola's
   `.card { position: relative }`, backgammon's `.checker { position: absolute }`) is linked after
   this sheet and would win a plain `.flyer` at equal specificity, taking the clone back into the
   body's flow; the clone is the body's child by `launchClone`'s design, so the selector says so. */
body > .flyer {
  position: fixed;
  pointer-events: none;
  z-index: 40;
}
/* Each theme paints the overlay's scrim and the sheet's edge (gin a gold border, backgammon a shadow) and radius. */
.overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
  padding: 16px;
}
/* The pass-and-play curtain is the one overlay with a shell scrim, and it is opaque unless a theme
   says otherwise (`--curtain-scrim`, tokens.css): the seat that just played may hold a face-up hand
   beneath it, and a theme that forgot to paint a scrim showed that hand to the next seat (uno,
   2026-10-02). Briscola and backgammon redeclare the token translucent on their :root; gin's
   curtain, which painted this same felt inline (page.ts `curtainStyle`), now carries the class and
   reads the default. */
.overlay.curtain {
  background: var(--curtain-scrim);
}
.sheet {
  background: var(--card);
  padding: 22px 18px;
  max-width: 420px;
  width: 100%;
  max-height: 88dvh;
  overflow-y: auto;
}
.sheet-title {
  font-size: 1.35rem;
  text-align: center;
  margin: 4px 0 2px;
}
.sheet-sub {
  color: var(--muted);
  text-align: center;
  margin-bottom: 12px;
}
/* The turn gate (docs/design/backgammon-landscape.md §5D; web/shared/ui/shell.ts gateOpen,
   shellPaint.ts paintGate; the markup is web/shared/markup/shell.ts gateMarkup, carried by a page
   whose game plays sideways): over the flyers (40) and the overlays (50), under the toast (100).
   The theme paints its scrim (backgammon: the curtain's deep aegean, denser still). No media query
   shows it: the App holds the orientation. The glyph is one small stroke in the accent,
   markup-carried (no TS toggles it; CONTRACT.md's shell row): 36px, so the quarter turn's arc and
   its arrowhead (about 4px of leg) read as a turn and not as a phone outline alone. */
#turnGate {
  z-index: 90;
}
.turn-glyph {
  display: flex;
  justify-content: center;
  margin-bottom: 6px;
}
.turn-glyph svg {
  width: 36px;
  height: 36px;
  fill: none;
  stroke: var(--accent);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.rules-list {
  list-style: none;
  padding: 0;
  margin: 12px 0 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.rules-list li {
  border-radius: var(--radius-inner);
}
.rules-list li strong {
  color: var(--emphasis);
}
/* Grouped rules (web/shared/ui/glossary.ts `RuleGroup`, hive first; the owner, 2026-10-02: the
   bugs' moves and the other rules "in separate columns ... if not 2 columns on thinner desktop then
   at least 2 sections"): the slot's `<li class="rules-group">`s each carry a small heading and
   their own list. The slot is a grid: one column, and as many 300px columns as fit where the slot
   is wide, which is two on the Rules tab once #app's cap lifts at 900px and in the rules sheet,
   widened there for the same reason. Both lifts need :has() (the slot is the shell's, the groups a
   game's choice): without it the groups stack, the phone layout. A rule with art (`rule-card`) is a
   panel: the inner fill, the inner radius, 8px of padding, the art tile 40px at the left and the
   text beside it. */
.rules-group h3 {
  margin: 0 0 6px;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.rules-group .rules-list {
  margin: 0;
}
.rule-card {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px;
  background: var(--fill);
}
.rule-art {
  flex: 0 0 40px;
  width: 40px;
  height: 46px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.rule-art svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.rule-copy {
  flex: 1 1 auto;
  min-width: 0;
}
@supports selector(:has(a)) {
  .rules-list:has(> .rules-group) {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
    gap: 16px;
    align-items: start;
  }
  @media (min-width: 900px) {
    #app:has(> #homeScreen:not(.hidden) #rulesPanel:not(.hidden) .rules-group) {
      max-width: 820px;
    }
    .sheet:has(.rules-group) {
      max-width: 760px;
    }
  }
}
/* Glossary links (docs/design/glossary-links.md; web/shared/ui/glossary.ts writes them): jargon in
   the About copy and in a rule that names another rule is a link to that rule, a dotted hairline
   in the accent and nothing louder. The rule it lands on flashes once (web/shared/edge/glossary.ts
   holds the class for 1.2 s): the `rule-flash` keyframes are each theme's, a wash of its accent. */
.jargon {
  color: var(--accent);
  text-decoration: underline dotted;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.rule-flash {
  animation: rule-flash 1.2s ease-out;
}
/* ---------- the result sheet's score rows (web/shared/markup/shell.ts resultMarkup; docs/design/shell-hoist.md row G) ---------- */
/* `#rsScore` in its three shapes (web/shared/markup/shell.ts ResultSheet.score): a `score-list`,
   one `.score-row` per seat or side, its name and its number apart, on the sheet's second surface
   (uno's cards left, briscola's points, backgammon's match games on its endgame); a `score-line`,
   one bold line in the accent (backgammon's match score); a `result-note`, one muted paragraph
   (hive's how it ended). The rows are scoped to the list: fidice's ladder has `.score-row`s of its
   own look. Uno and briscola spelled the rows alike (uno's at 0.95rem, now the shared 0.92); a theme
   adds its marks (`.score-row.winner`, `.score-row .who`). */
.score-line {
  text-align: center;
  font-weight: 700;
  color: var(--accent);
  margin: 0 0 14px;
}
.result-note {
  margin: 6px 0 14px;
  color: var(--muted);
}
.score-list {
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.score-list .score-row {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 9px;
  background: var(--card-2);
  font-size: 0.92rem;
}
.sheet .score-list + .btn-block {
  margin-top: 14px;
}

/* ---------- the waiting rooms' seat list (web/shared/ui/shellPaint.ts paintWaiting; docs/design/n-seat-sessions.md §7; shell-hoist.md row I) ---------- */
/* One class-free row per seat under the room's status, the host first, each carrying data-seat,
   data-connected and, on this device's own, data-you. Quiet by design: a dot in the accent before
   the label, an empty seat and its dot muted, my own row in the heavier weight. Uno's spelling, on
   the tokens; briscola had literal olive and sand dots and cream text. */
.seat-list {
  list-style: none;
  margin: 8px auto 2px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-width: 260px;
  text-align: left;
  font-size: 0.92rem;
}
.seat-list li {
  display: flex;
  align-items: center;
  gap: 8px;
}
.seat-list li::before {
  content: '';
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent-dark);
  flex-shrink: 0;
}
.seat-list li[data-connected='false'] {
  color: var(--muted);
}
.seat-list li[data-connected='false']::before {
  background: var(--muted);
}
.seat-list li[data-you] {
  font-weight: 600;
}

/* ---------- the history panel (web/shared/ui/history.ts) ---------- */
/* One <details> per event, newest last (docs/design/briscola-sound-history.md §6): the summary
   line is the row, a small marker its only chrome, and a click unfolds the detail as a two-column
   list. Every selector here needs the details element, so gin's and backgammon's own rows (a div
   per line, each theme's rule) are untouched until they adopt the panel. data-value is the shell's
   weight word for a row (`big`, `huge`; a game maps its own classes onto them, briscola's value
   classes first): the accent is the theme's --emphasis and nothing louder (the owner, 2026-09-23:
   recognisable at a glance, otherwise quiet). The .who span keeps --muted whatever the weight. */
details.history-row {
  border-bottom: 1px solid var(--fill);
}
details.history-row:last-of-type {
  border-bottom: none;
}
details.history-row > summary {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 8px 2px;
  cursor: pointer;
  list-style: none;
  font-size: 0.92rem;
  line-height: 1.35;
}
details.history-row > summary::-webkit-details-marker {
  display: none;
}
details.history-row > summary::before {
  content: '\25B8';
  color: var(--muted);
  font-size: 0.8em;
  flex-shrink: 0;
  transition: transform 0.12s ease;
}
details.history-row[open] > summary::before {
  transform: rotate(90deg);
}
details.history-row > summary .who {
  color: var(--muted);
  flex-shrink: 0;
}
details.history-row[data-value='big'] > summary,
details.history-row[data-value='huge'] > summary {
  color: var(--emphasis);
}
details.history-row[data-value='huge'] > summary {
  font-weight: 700;
}
details.history-row > .history-detail {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 12px;
  margin: 0 0 10px 22px;
  font-size: 0.85rem;
}
details.history-row > .history-detail dt {
  color: var(--muted);
  font-weight: 700;
}
details.history-row > .history-detail dd {
  margin: 0;
}
/* ---------- the finished games under the history list (web/shared/ui/recentGames.ts) ---------- */
/* One line per game, newest first (the owner's game history, 2026-09-25): the local time muted,
   the names, the game's score in the display face, and W, L or D at the end, the mark alone
   carrying the outcome's colour (the accent for a win, the danger red for a loss, muted for a
   draw). The slot collapses when it lists nothing. Token-only, as the rest of this sheet. */
.recent-games:empty {
  display: none;
}
.recent-games {
  margin-top: 14px;
  border-top: 1px solid var(--fill);
  padding-top: 10px;
}
.recent-games-title {
  margin: 0 0 6px;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.recent-games-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.recent-game {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 6px 2px;
  border-bottom: 1px solid var(--fill);
  font-size: 0.85rem;
  line-height: 1.35;
}
.recent-game:last-child {
  border-bottom: none;
}
.recent-game-when {
  color: var(--muted);
  flex-shrink: 0;
}
.recent-game-players {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.recent-game-score {
  font-family: var(--font-display);
  flex-shrink: 0;
}
.recent-game-outcome {
  font-weight: 700;
  min-width: 1.2em;
  text-align: center;
  flex-shrink: 0;
  color: var(--muted);
}
.recent-game[data-outcome='win'] .recent-game-outcome {
  color: var(--accent);
}
.recent-game[data-outcome='loss'] .recent-game-outcome {
  color: var(--danger);
}
/* The About tab's prose (each game's about.ts). */
.card-box.prose p {
  margin: 0 0 12px;
  line-height: 1.5;
}
.card-box.prose p:last-child {
  margin-bottom: 0;
}
/* The toast (shellPaint.ts `showToast`): fixed over the page's foot; each theme paints its fill, label, shadow and width. */
#toast {
  position: fixed;
  left: 50%;
  bottom: calc(24px + var(--frame-inset-bottom));
  transform: translateX(-50%) translateY(20px);
  font-weight: 700;
  padding: 12px 16px;
  border-radius: var(--radius-control);
  opacity: 0;
  pointer-events: none;
  transition: all 0.2s;
  z-index: 100;
  text-align: center;
}
#toast.show {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
/* The red toast (shellPaint.ts `showToast` with the `error` kind; shell.ts `errorToast`): a refusal
   the player must read, over whatever fill the theme paints the plain one. Dark ink on the danger
   red (the shared `--danger`, every theme's own red): a theme may name `--toast-error-ink`. */
#toast.error {
  background: var(--danger);
  color: var(--toast-error-ink, #2a0b0b);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}

/* ---------- top tabs and the mode switch (home.ts) ---------- */
.tabbar {
  display: flex;
  gap: 6px;
  margin: 4px 0 16px;
  background: var(--surface-bar);
  border-radius: var(--radius-control);
  padding: 4px;
}
.tab-wrap {
  position: relative;
  flex: 1.2;
  display: flex;
}
.tab-btn {
  flex: 1;
  background: transparent;
  border: none;
  color: var(--ink-on-bar);
  font-weight: 700;
  padding: 11px 6px;
  border-radius: var(--radius-tab);
  white-space: nowrap;
}
.tab-btn.active {
  background: var(--fill);
  color: var(--ink-on-fill);
}
/* The Play tab has a submenu; the caret says so (hover on a desktop, a long press on a phone). */
.tab-wrap .tab-btn::after {
  content: ' \25BE';
  opacity: 0.55;
  font-size: 0.8em;
}
.tab-panel.hidden {
  display: none !important;
}
/* The submenu is a fold of the tab bar under the Play tab: the shell's box surface, its buttons as
   tab buttons. Its ::before bridges the 6px gap up to the tab, so a pointer crossing the gap keeps
   `.tab-wrap:hover` (without it the menu hid, and its pointer-events: none then kept it hidden,
   the moment the pointer left the tab). On the Play tab itself the mode row below is the menu,
   so a hover shows nothing. */
.tab-submenu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 180px;
  z-index: 30;
  background: var(--surface-shell);
  border-radius: var(--radius-inner);
  padding: 4px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(-4px);
  transition:
    opacity 0.12s,
    transform 0.12s;
}
.tab-submenu::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -8px;
  height: 8px;
}
.tab-submenu.force-open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateY(0);
}
/* The hovered entry precedes the active one below, as in both themes, so an active entry under the
   pointer keeps its fill. */
@media (hover: hover) {
  .tab-wrap:hover .tab-btn:not(.active) + .tab-submenu {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transform: translateY(0);
  }
  .tab-submenu button:hover {
    background: var(--fill-hover);
    color: var(--ink-hover);
  }
}
.tab-submenu button {
  background: transparent;
  border: none;
  color: var(--ink-on-bar);
  text-align: left;
  padding: 11px 12px;
  border-radius: var(--radius-tab);
  font-weight: 700;
  white-space: nowrap;
}
.tab-submenu button.active {
  background: var(--fill);
  color: var(--ink-on-fill);
}
/* The play mode is the tab bar's second row, on the bar's surface, its two modes as smaller tab
   buttons; each theme squares the bar's bottom off above it and draws the hairline between. */
.mode-switch {
  display: flex;
  gap: 6px;
  margin: 0 0 16px;
  padding: 4px;
  background: var(--surface-bar);
  border-radius: 0 0 var(--radius-control) var(--radius-control);
}
.mode-btn {
  flex: 1;
  background: transparent;
  border: none;
  color: var(--ink-on-bar);
  font-weight: 700;
  padding: 9px 6px;
  border-radius: var(--radius-tab);
  white-space: nowrap;
}
.mode-btn.active {
  background: var(--fill);
  color: var(--ink-on-fill);
}
/* A finger's target (Apple HIG, Material; the audit's `targets` rule, tools/space-audit/judge.ts
   `TARGET_MIN`): `.icon-btn` and `.btn-sm` stand at 44px always; the tab bar's and the mode
   switch's buttons, 37-42px by their type alone, reach it on a touch screen. A fine pointer keeps
   the type's height, so the goldens (a fine pointer) record what they did. */
@media (any-pointer: coarse) {
  .tab-btn,
  .mode-btn {
    min-height: 44px;
  }
}

::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* ---------- the far seat's flip (docs/design/backgammon-landscape.md §6 item 7; docs/design/shared-shell.md "Playing sideways") ---------- */
/* A phone lying flat between two players is read upside down by the one across the table: while
   the shell says so (web/shared/ui/shell.ts `flipped`: pass and play, the setting on, at the table,
   seat 1 looking, whether the curtain is up for them or their view is shown), the boot paints
   `data-flip="1"` on the body (shellPaint.ts `paintFlip`) and the body makes a half turn. At the
   table the body is the viewport (`fixed-screen` above: 100dvh, no scroll), so the turn maps it
   onto itself and everything the body holds turns with it: #app, every fixed overlay (the curtain,
   the sheets, the leave confirm), #toast (the theme's sideways anchor at the top edge lands at the
   reader's top), the flyers and the drag ghost (their kernels reflect the rects they place by,
   web/shared/edge/dom.ts `bodySpace`); pointer hit-testing follows the transform. An attribute,
   not a class: the class contract (CONTRACT.md) has no row, and the composed page carries none
   (only `data-plays`, below, is markup). */
body[data-flip='1'] {
  rotate: 180deg;
}

/* ---------- the home and the waiting rooms sideways (docs/design/backgammon-landscape.md §5; docs/design/shared-shell.md "Playing sideways") ---------- */
/* A phone held sideways on the home screen of a game that plays sideways (`body[data-plays="landscape"]`,
   the attribute the composed page carries when its page.ts says `plays: 'landscape'`; gin's and
   briscola's pages carry none and keep the upright column), under the board's own predicate
   (web/shared/edge/media.ts LANDSCAPE_PHONE: any-pointer: coarse and a landscape under 500px tall; a
   fine pointer, and so the 390x844 golden, never matches): two columns, so Open a table, Sit down
   and Start sit above a 304px fold with no scroll. The Play tab and the two mode panels lay their
   children into the grid themselves (display: contents), the masthead, the tab bar and the mode
   switch down the left, the cards on the right in sub-columns: online the name, the host card and
   the join card side by side (every shell page's online panel has these three cards), the name's
   the narrowest and the join card's the widest, so four letters of a code fit at their tracking
   (2026-09-28: the first letter of ABCD was cut off at 667 and 780px); pass the phone lays its
   cards out too, but their number is the game's (backgammon two, gin three, briscola one), so
   each game's theme places them (backgammon's theme.css: the players with Start under them and
   the options beside). A sub-column is 120-190px wide, so each panel's rows wrap (a select, a name
   or the code takes a line of its own) and the code loses some of its tracking. The tab bar keeps
   its own bottom edge (the switch is in the other column) and the switch its full radii. The two
   waiting rooms lay out in two columns as well (the host: the title, the code and its share
   button left; the status, Start the match and Cancel right, the buttons at the foot of the
   column; the guest: the title and the status left, Back right), so Start the match sits above a
   304px fold once the opponent has joined (2026-09-28: it sat at y 420 in 390). The table and the
   endgame are not touched: #app's cap lifts for these three screens alone, through :has(). The
   whole block sits under @supports on that selector: without :has() the cap could not lift and
   the grids would squeeze into 480px (three online cards of 53px), so a browser without it keeps
   the portrait single column sideways and scrolls, as before; @supports selector() itself predates
   :has() in every engine. The nested query at the end is the same layout on a 667px-wide phone
   (an SE sideways): a narrower left column, the online cards in two sub-columns (the host card,
   the tallest, alone on the left; the name over the join card on the right, so Open a table clears
   a 375px fold), and a smaller, tighter code. */
@supports selector(:has(a)) {
  @media (any-pointer: coarse) and (orientation: landscape) and (max-height: 500px) {
    body[data-plays='landscape']
      #app:has(
        > #homeScreen:not(.hidden),
        > #hostWaitScreen:not(.hidden),
        > #guestWaitScreen:not(.hidden)
      ) {
      max-width: 820px;
    }
    body[data-plays='landscape'] #homeScreen {
      display: grid;
      grid-template-columns: minmax(0, 250px) minmax(0, 1fr);
      grid-template-rows: auto auto auto auto 1fr;
      column-gap: 14px;
      align-items: start;
    }
    /* The masthead is each game's block (its class is the game's too: backgammon's theme places it at 1 / 1). */
    body[data-plays='landscape'] #homeScreen > #resumeBox {
      grid-area: 2 / 1;
    }
    /* The bar's shadow, in the theme's ink, is the theme's (backgammon's theme.css). */
    body[data-plays='landscape'] #homeScreen > #topTabbar {
      grid-area: 3 / 1;
      margin: 0 0 10px;
      border-radius: var(--radius-control);
    }
    body[data-plays='landscape'] #playPanel {
      display: contents;
    }
    body[data-plays='landscape'] #playPanel > #playModeSwitch {
      grid-area: 4 / 1;
      border-top: none;
      border-radius: var(--radius-control);
    }
    body[data-plays='landscape'] #onlineModeContent,
    body[data-plays='landscape'] #localModeContent {
      grid-area: 1 / 2 / -1 / 3;
      display: grid;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
      column-gap: 12px;
      row-gap: 12px;
      align-items: start;
    }
    /* Upright, a card's bottom margin spaces the column; in the grid the row gap does, and the
       margin under the tallest card (the host card, 364px at 844x390) was the 10px the document
       scrolled sideways (2026-09-28: #app 12 + 376 + 12 = 400 in 390). */
    body[data-plays='landscape'] #onlineModeContent > .card-box,
    body[data-plays='landscape'] #localModeContent > .card-box {
      margin-bottom: 0;
    }
    body[data-plays='landscape'] #onlineModeContent .row,
    body[data-plays='landscape'] #localModeContent .row {
      flex-wrap: wrap;
    }
    /* .grow is flex: 1 (a zero basis never overflows a line): a full-line basis makes each wrap. */
    body[data-plays='landscape'] #onlineModeContent .row > *,
    body[data-plays='landscape'] #localModeContent .row > * {
      flex-basis: 100%;
    }
    body[data-plays='landscape'] #onlineModeContent {
      grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.1fr) minmax(0, 1.1fr);
    }
    body[data-plays='landscape'] #onlineModeContent > .card-box:nth-child(1) {
      grid-area: 1 / 1;
    }
    body[data-plays='landscape'] #onlineModeContent > .card-box:nth-child(2) {
      grid-area: 1 / 2;
    }
    body[data-plays='landscape'] #onlineModeContent > .card-box:nth-child(3) {
      grid-area: 1 / 3;
    }
    body[data-plays='landscape'] #onlineModeContent .code-input {
      letter-spacing: 0.3em;
    }
    body[data-plays='landscape'] #rulesPanel,
    body[data-plays='landscape'] #aboutPanel {
      grid-area: 1 / 2 / -1 / 3;
    }
    /* The host's waiting room: rows 1-2 are the title and its line, the code card spans the rest of
       the left column; the status card spans rows 1-3 on the right, the two buttons take rows 4-5,
       and the 1fr third row pushes them to the foot. Start the match is display: none until the
       guest joins, so it takes no row until then. */
    body[data-plays='landscape'] #hostWaitScreen {
      display: grid;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
      grid-template-rows: auto auto 1fr auto auto;
      column-gap: 14px;
      align-items: start;
    }
    body[data-plays='landscape'] #hostWaitScreen > h1 {
      grid-area: 1 / 1;
      margin: 6px 0 0;
    }
    body[data-plays='landscape'] #hostWaitScreen > .subtitle {
      grid-area: 2 / 1;
    }
    body[data-plays='landscape'] #hostWaitScreen > .card-box:nth-child(3) {
      grid-area: 3 / 1 / 6 / 2;
      margin-bottom: 0;
    }
    body[data-plays='landscape'] #hostWaitScreen > .card-box:nth-child(4) {
      grid-area: 1 / 2 / 4 / 3;
    }
    body[data-plays='landscape'] #hostWaitScreen > #startGameBtn {
      grid-area: 4 / 2;
    }
    body[data-plays='landscape'] #hostWaitScreen > #cancelHostBtn {
      grid-area: 5 / 2;
    }
    /* The guest's: the title and the status card down the left, Back at the foot of the right. */
    body[data-plays='landscape'] #guestWaitScreen {
      display: grid;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
      grid-template-rows: auto 1fr;
      column-gap: 14px;
      align-items: start;
    }
    body[data-plays='landscape'] #guestWaitScreen > h1 {
      grid-area: 1 / 1;
      margin: 6px 0 0;
    }
    body[data-plays='landscape'] #guestWaitScreen > .card-box {
      grid-area: 2 / 1;
      margin-bottom: 0;
    }
    body[data-plays='landscape'] #guestWaitScreen > #cancelGuestBtn {
      grid-area: 1 / 2 / 3 / 3;
      align-self: end;
    }
    @media (max-width: 719px) {
      body[data-plays='landscape'] #homeScreen {
        grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
      }
      /* Two sub-columns: three of 100px wrapped every label and clipped the selects. */
      body[data-plays='landscape'] #onlineModeContent {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        grid-template-rows: auto auto;
      }
      body[data-plays='landscape'] #onlineModeContent > .card-box:nth-child(1) {
        grid-area: 1 / 2;
      }
      body[data-plays='landscape'] #onlineModeContent > .card-box:nth-child(2) {
        grid-area: 1 / 1 / 3 / 2;
      }
      body[data-plays='landscape'] #onlineModeContent > .card-box:nth-child(3) {
        grid-area: 2 / 2;
      }
      /* .code-input above holds its size with !important; the join card takes four wide letters
         (WWWW) at this size and tracking. */
      body[data-plays='landscape'] #onlineModeContent .code-input {
        font-size: 1.1rem !important;
        letter-spacing: 0.12em;
      }
    }
  }
}
/* The number stepper (web/shared/markup/stepper.ts, web/shared/ui/stepper.ts; the owner,
   2026-10-02: "a number with - and + buttons on the side so you can increase and decrease"): the
   label over a row of −, the count and +, centred in its card, each button a 44px target in the
   theme's fill, the count in the display face between them; a button at its bound is disabled and
   fades as .btn does. */
.stepper {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.stepper-label {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.stepper-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
}
.stepper-btn {
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  border: none;
  border-radius: var(--radius-inner);
  background: var(--fill);
  color: var(--ink-on-fill);
  font-size: 1.4rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
}
.stepper-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* The pass-and-play seat names (web/shared/markup/seatNames.ts, web/shared/ui/seatNames.ts; the
   owner, 2026-10-02: "more than 4 players should paint properly"): one input per seat the stepper
   allows, two to a row, the seats past the count hidden so the grid holds no empty cells. */
.seat-names {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-top: 10px;
}
.seat-names .hidden {
  display: none;
}

/* The table's names strip (web/shared/markup/topbar.ts topbarHtml; docs/design/dry-review-2026-10.md
   §7 row 14): the two names and the other seat's dot on one line between the topbar's buttons,
   clipped rather than wrapped. A theme adds its weight and size (uno), its centring (hive). */
.names-strip {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
}
/* The other seat's connection dot (topbar.ts `#oppDot`, gin's `#connDot`; web/shared/ui/shellPaint.ts
   `connDotClass` toggles `on`/`off`): a 9px disc, muted until the peer answers, red once it drops. A
   theme recolours it (backgammon's olive and sand, briscola's moss and parchment, gin's accent, and
   gin's 10px with its halo). */
.conn-dot {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--muted);
  flex-shrink: 0;
}
.conn-dot.on {
  background: var(--accent-dark);
}
.conn-dot.off {
  background: var(--danger);
}
.stepper-num {
  min-width: 2ch;
  text-align: center;
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--text);
}
