/* peer2peergames: one stylesheet, mobile first, no framework.
 *
 * Tokens on :root for the light palette, redefined for dark. The board is
 * walnut and the cards ivory in both, so those are not tokens that change.
 */
:root {
  --ground: #d9d5c9;
  --surface: #efebe2;
  --surface-2: #e4dfd3;
  --ink: #221d18;
  --muted: #6d655a;
  --line: #c4bdae;
  --accent: #7a4b22;
  --accent-ink: #fbf7ee;
  --danger: #b3261e;
  --walnut: #5a3a24;
  --walnut-deep: #3d2616;
  --hole: #241509;
  --brass: #cfa64f;
  --peg-red: #c23b2c;
  --peg-blue: #2f4c8a;
  /* Seats 3 and 4. Chosen to stay apart from red and blue under deuteranopia
     and protanopia, and never relied on alone: see .swatch below. */
  --peg-green: #1f7a4d;
  --peg-amber: #d59a00;
  --card: #fbf7ee;
  --card-ink: #1e1a17;
  --card-red: #b8322a;
  --card-edge: rgba(30, 20, 10, .22);
  --back: #2f4c8a;
  --back-line: rgba(251, 247, 238, .55);
  --shadow: rgba(30, 20, 10, .22);
  --display: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
  --body: system-ui, -apple-system, "Segoe UI", sans-serif;
}
@media (prefers-color-scheme: dark) {
  :root {
    --ground: #1b1815;
    --surface: #26211c;
    --surface-2: #2f2923;
    --ink: #ece5d8;
    --muted: #a79d8f;
    --line: #3d362e;
    --accent: #d3aa55;
    --accent-ink: #1b1815;
    --shadow: rgba(0, 0, 0, .5);
  }
}

/* ---- hidden means hidden ---------------------------------------------------
 *
 * THE HIDDEN ATTRIBUTE ONLY WORKS UNTIL SOMETHING IN HERE SETS A DISPLAY.
 * `[hidden] { display: none }` is a USER AGENT rule, and any author rule with
 * a display beats it whatever its specificity, so `.push-prompt { display:
 * flex }` made an element JavaScript had carefully hidden visible on every
 * page. The notification prompt sat on every game page with its permission
 * long since granted, and pressing the button did nothing at all, because the
 * click handler is only attached in the state that was meant to show it.
 *
 * This file had already learned the lesson at least ten times - .mar-claim,
 * four .needs-script rules, .hex-swap, the four #go- buttons, .ct-tile,
 * .ct-picker, .mz-ready, the darts loupe - each one a separate rule written
 * after the same bug. One rule here retires the whole class of it, and the
 * ones already written do no harm.
 *
 * Anything that wants to be visible while carrying the attribute has to say
 * so with its own !important, which is a thing worth having to write down. */
[hidden] { display: none !important; }

/* ---- the frame ------------------------------------------------------------- */
body { font: 16px/1.45 var(--body); background: var(--ground); color: var(--ink);
       max-width: 62rem; margin: 0 auto; padding: 0 1rem 3rem; }
h1, h2, h3 { font-family: var(--display); font-weight: 400; text-wrap: balance; }
h2 { font-size: 1.7rem; margin: 1.2rem 0 .4rem; }
h3 { font-size: 1.15rem; margin: 1.6rem 0 .5rem; }
.lede { max-width: 60ch; color: var(--muted); }
.quiet { color: var(--muted); }
a { color: var(--accent); }

header.site { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1rem;
              border-bottom: 1px solid var(--line); padding: .8rem 0; }
header.site .brand { font-family: var(--display); font-size: 1.25rem; text-decoration: none; color: var(--ink); }
header.site .brand-mark { color: var(--accent); }
header.site nav { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem .9rem; margin-inline-start: auto; }
header.site nav a { text-decoration: none; color: var(--ink); }
header.site nav a.cta { color: var(--accent); font-weight: 600; }
header.site nav form { margin: 0; }
button.link { border: 0; background: none; color: var(--accent); padding: 0; cursor: pointer; font: inherit; }

footer.site { display: flex; flex-wrap: wrap; gap: .4rem 1rem; margin-top: 3rem; padding-top: 1rem;
              border-top: 1px solid var(--line); font-size: .9rem; }

.button, button.button { border: 1px solid var(--line); background: var(--surface-2); color: var(--ink);
  padding: .5rem .9rem; border-radius: 8px; cursor: pointer; font: inherit; font-weight: 600; text-decoration: none;
  display: inline-block; }
.button.primary, button.primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
.button.small, button.small { padding: .3rem .6rem; font-size: .9rem; }
.button.danger, button.danger { color: var(--danger); }
.button:disabled, button:disabled { opacity: .45; cursor: default; }
.button:focus-visible, button:focus-visible, .card:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.notice { padding: .6rem .8rem; border-radius: 8px; background: var(--surface); border: 1px solid var(--line); }
.notice.bad { border-color: var(--danger); }
form.inline { display: inline; }
form.row { display: flex; flex-wrap: wrap; gap: .8rem; align-items: flex-end; }
form.row label { display: flex; flex-direction: column; gap: .2rem; }
select, input { font: inherit; padding: .4rem; border-radius: 6px; border: 1px solid var(--line); background: var(--surface); color: var(--ink); }
input:focus-visible, select:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }

/* ---- forms: sign in, create an account, the account page ------------------------- */
form.stack { display: grid; gap: .9rem; max-width: 24rem; }
form.stack label, form.row label { display: flex; flex-direction: column; gap: .3rem; font-size: .85rem; font-weight: 600; color: var(--muted); }
form.stack input:not([type="checkbox"]), form.stack select { width: 100%; box-sizing: border-box; font-size: 1rem; padding: .55rem .7rem; color: var(--ink); }
form.stack .between { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; }
form.stack .between a { font-weight: 400; }
form.stack .hint { font-weight: 400; font-size: .85rem; color: var(--muted); margin: 0; }
form.stack label.check, label.check { flex-direction: row; align-items: center; gap: .6rem; font-weight: 400; font-size: 1rem; color: var(--ink); }
form.stack label.check input, label.check input { width: 1.15rem; height: 1.15rem; margin: 0; accent-color: var(--accent); flex: 0 0 auto; }
form.stack > button { justify-self: start; background: var(--accent); color: var(--accent-ink); border: 1px solid transparent;
                      padding: .6rem 1.1rem; border-radius: 8px; font-weight: 600; }
form.stack > button.bad { background: none; color: var(--danger); border-color: var(--danger); }
.cap { text-transform: capitalize; }

.auth { max-width: 26rem; margin: 2rem auto 0; padding: 1.6rem 1.5rem 1.4rem; background: var(--surface); border: 1px solid var(--line);
        border-radius: 12px; box-shadow: 0 6px 20px var(--shadow); }
.auth h2 { margin: 0 0 .3rem; font-size: 1.8rem; }
.auth .lede { margin: 0 0 1.2rem; }
.auth .notice { margin: 0 0 1rem; }
.auth form.stack { max-width: none; }
.auth form.stack > button { justify-self: stretch; width: 100%; }
.auth .divider { display: flex; align-items: center; gap: .8rem; margin: 1.2rem 0; color: var(--muted); font-size: .85rem; }
.auth .divider::before, .auth .divider::after { content: ""; flex: 1; border-top: 1px solid var(--line); }
.auth .oauth { display: grid; gap: .5rem; }
.auth .oauth .button { text-align: center; }
.auth .alt { margin: 1.2rem 0 0; text-align: center; color: var(--muted); }
.auth .aside { margin: .8rem 0 0; text-align: center; font-size: .85rem; color: var(--muted); }
.auth .aside a { word-break: break-all; }

/* ---- welcome, lists ------------------------------------------------------------ */
.hero { padding: 1.5rem 0 .5rem; }
.hero h2 { font-size: 2rem; max-width: 22ch; }
.actions { display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0; }
.steps { padding-inline-start: 1.2rem; max-width: 60ch; display: grid; gap: .5rem; }
.columns { display: grid; gap: 0 2rem; grid-template-columns: 1fr; }
@media (min-width: 720px) { .columns { grid-template-columns: 1fr 1fr; } }
ul.games, ul.challenges { list-style: none; padding: 0; margin: 0; display: grid; gap: .4rem; }
ul.games li, ul.challenges li { padding: .5rem .7rem; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; }
ul.challenges li { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; }
ul.challenges .who { font-weight: 600; }
ol.log { font-variant-numeric: tabular-nums; }
.invite-link a { word-break: break-all; }

/* ---- the roster, filters and tables (plan phase 12) ------------------------------ */
/* The game grid (plan_p2p_home_and_seo phase 01). It replaced .roster-list, a
   flex row per game, which was right for one game and unreadable at nine.
   Tiles are a fixed minimum width rather than a fixed count, so the same rule
   gives four across on a desktop and one on a phone with no breakpoint. */
.game-grid { list-style: none; padding: 0; margin: 0; display: grid; gap: 1rem;
             grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); }
/* Three rows: the name, the blurb, the buttons. The blurb's row is the elastic
   one, so the buttons sit on the floor of every tile and a row of tiles has its
   actions in a line however long the blurbs are. */
.game-tile { display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto 1fr auto;
             gap: .2rem .8rem; padding: .9rem 1rem 1rem;
             background: var(--surface); border: 1px solid var(--line); border-radius: 10px; }
.game-tile .game-icon { grid-row: 1 / span 2; width: 3rem; height: 3rem; align-self: start; }
.game-tile h4 { margin: .1rem 0 0; font-size: 1.1rem; }
.game-tile .blurb { margin: .15rem 0 0; color: var(--muted); font-size: .88rem; }
.game-tile .tile-actions { grid-column: 1 / -1; margin: .8rem 0 0;
                           display: flex; flex-wrap: wrap; gap: .5rem .9rem; align-items: center; }
/* a game that is registered but has no page yet: no Play button to press */
.game-tile.soon { opacity: .82; }
.game-tile .tag { font-size: .8rem; padding: .25rem .6rem; border-radius: 999px;
                  background: var(--surface-2); border: 1px dashed var(--line); color: var(--muted); }

/* the same icons label the lobby's filter chips, so the roster reads alike wherever it is */
.chips .chip-icon { width: 1.15rem; height: 1.15rem; margin-inline-end: .35rem; flex: none; }
/* A browser's forced dark theme (Chrome for Android's "apply dark theme to
   sites", Samsung Internet's equivalent) inverts the lightness of icon-sized
   inline SVGs and leaves large ones alone, so the tiles went pastel on a phone
   while the 14em icon on the rules page did not. The icons draw their own
   ground and are right in both themes; `only` is Chrome's documented
   per-element opt-out. Never put it on :root, that kills the site's dark theme. */
.game-icon, .chip-icon { color-scheme: only light; }

.chips, .tabs { display: flex; flex-wrap: wrap; gap: .4rem; margin: .4rem 0 .8rem; }
.chips a, .tabs a { text-decoration: none; padding: .25rem .7rem; border-radius: 999px; border: 1px solid var(--line); color: var(--ink); background: var(--surface); font-size: .9rem; }
.chips a { display: inline-flex; align-items: center; }
.chips a[aria-current], .tabs a[aria-current] { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
.board-table { border-collapse: collapse; width: 100%; font-variant-numeric: tabular-nums; }
.board-table th, .board-table td { text-align: start; padding: .4rem .6rem; border-bottom: 1px solid var(--line); }
.board-table th { font-size: .75rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.record { display: grid; gap: .3rem; margin: .4rem 0 1rem; }
.record div { display: flex; gap: .6rem; }
.record dt { color: var(--muted); min-width: 9rem; }
.record dd { margin: 0; }
.record-game h3 { margin-top: 1rem; }
.move-list { display: flex; flex-wrap: wrap; gap: .5rem; min-height: 2.4rem; margin-bottom: .6rem; }

/* ---- tic-tac-toe ------------------------------------------------------------------------ */
.ttt-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; padding: 6px; margin: 0 0 .8rem;
            background: var(--walnut); border-radius: 10px; width: min(100%, 354px); box-sizing: border-box;
            box-shadow: inset 0 2px 8px rgba(0,0,0,.35), 0 3px 10px var(--shadow); }
.ttt-grid .cell { aspect-ratio: 1; border: 0; border-radius: 6px; background: var(--card); color: var(--card-ink);
                  font-family: var(--display); font-size: clamp(2rem, 8vw, 3.6rem); line-height: 1; cursor: pointer;
                  transition: background .15s ease, transform .15s ease; }
.ttt-grid .cell:not(:disabled):hover { transform: translateY(-2px); }
.ttt-grid .cell:disabled { cursor: default; opacity: 1; }          /* a taken square is not a greyed control */
.ttt-grid .cell:disabled:empty { opacity: .7; }
.ttt-grid .cell.x { color: var(--peg-red); }
.ttt-grid .cell.o { color: var(--peg-blue); }
.ttt-grid .cell.win { background: var(--brass); }
.ttt-grid .cell.hinted { outline: 3px dashed var(--brass); outline-offset: -4px; }
.ttt-grid .cell:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; }
/* ---- checkers ---------------------------------------------------------------------------- */
/* The board is walnut and the discs are the peg colours, none of which move with the colour
   scheme, so the board looks the same in both. Squares are buttons: a square that cannot be
   picked up is disabled, and a disabled square is not a greyed control. */
.checkers { margin: 0 0 .8rem; }
/* Sized by the page, never by the squares: a board declared max-content wide makes the
   game grid's column that wide too (a grid takes an item's max-content as its minimum)
   and a phone scrolls sideways. So the board is as wide as it may be, up to a limit,
   and the squares divide it; a square's height follows its width. A landscape phone
   is short rather than narrow, so there the height sets the limit instead. */
.checkers-board { display: grid; grid-template-rows: repeat(8, auto); gap: 0; padding: 6px; margin: 0 auto .2rem;
                  background: var(--walnut); border-radius: 10px; width: min(100%, 460px); box-sizing: border-box;
                  box-shadow: inset 0 2px 8px rgba(0,0,0,.35), 0 3px 10px var(--shadow);
                  touch-action: none; }
.checkers-board .rank { display: grid; grid-template-columns: repeat(8, 1fr); }
.checkers-board .square { aspect-ratio: 1; border: 0; padding: 0; position: relative; display: block; }
.checkers-board .square.light { background: var(--card); }
.checkers-board .square.dark { background: var(--walnut-deep); cursor: default; }
.checkers-board button.square { cursor: default; }
.checkers-board button.square:not(:disabled) { cursor: pointer; }
.checkers-board button.square:disabled { opacity: 1; }             /* a square is not a greyed control */
.checkers-board .num { position: absolute; left: 3px; top: 2px; font-size: .6rem; line-height: 1;
                       color: var(--card); opacity: .32; font-family: var(--body); }
.checkers-board .disc { position: absolute; inset: 12%; border-radius: 50%; display: none;
                        box-shadow: inset 0 -2px 4px rgba(0,0,0,.35), 0 2px 3px var(--shadow);
                        transition: transform .18s ease; }
.checkers-board .man .disc, .checkers-board .king .disc { display: block; }
.checkers-board .black .disc { background: var(--peg-red); }
.checkers-board .white .disc { background: var(--peg-blue); }
.checkers-board .king .disc::after { content: ''; position: absolute; inset: 26%; border-radius: 50%;
                                     border: 2px solid var(--brass); }
.checkers-board .square.selected .disc { transform: scale(1.08); box-shadow: 0 0 0 3px var(--brass), 0 2px 3px var(--shadow); }
.checkers-board .square.dragging .disc { opacity: .35; }
.checkers-board .square.can-land::before { content: ''; position: absolute; inset: 32%; border-radius: 50%;
                                           background: var(--brass); opacity: .55; }
.checkers-board .square.would-take .disc { outline: 3px solid var(--danger); outline-offset: -3px; }
.checkers-board .square.hinted { outline: 3px dashed var(--brass); outline-offset: -4px; }
.checkers-board .square.last-move::after { content: ''; position: absolute; inset: 0; pointer-events: none;
                                           box-shadow: inset 0 0 0 3px var(--brass); opacity: .5; }
.checkers-board .square:focus-visible { outline: 3px solid var(--accent); outline-offset: -3px; z-index: 1; }
.checkers .files { display: grid; grid-template-columns: repeat(8, 1fr); gap: 0;
                   width: min(100%, 460px); box-sizing: border-box; margin: 0 auto .6rem; padding: 0 6px; text-align: center; }
.checkers .files span { font-size: .75rem; }
@media (max-width: 400px) { .checkers-board .num { font-size: .5rem; left: 2px; top: 1px; } }
@media (orientation: landscape) and (max-height: 520px) {
    .checkers-board, .checkers .files { width: min(100%, 460px, calc(100vh - 6rem)); }
}
/* the piece the drag carries: appended to the board, positioned by the client */
.checkers-drag { position: fixed; z-index: 20; pointer-events: none; border-radius: 50%;
                 box-shadow: inset 0 -2px 4px rgba(0,0,0,.35), 0 4px 8px var(--shadow); }
.checkers-choice, .board-choice { display: flex; flex-wrap: wrap; gap: .4rem; margin: .3rem 0; align-items: center; }

/* ---- reversi ----------------------------------------------------------------------------- */
/* The checkers board with the drag, the kings and the light squares taken out.
   Reversi's board is UNCHECKERED, which is the visible difference: every square
   is the same green and the grid lines are what divide them. */
.reversi-board { display: grid; grid-template-rows: repeat(8, auto); gap: 0; padding: 6px; margin: 0 auto .2rem;
                 background: var(--felt-line, #1a4f35); border-radius: 10px; width: min(100%, 460px); box-sizing: border-box;
                 box-shadow: inset 0 2px 8px rgba(0,0,0,.35), 0 3px 10px var(--shadow); }
.reversi-board .rank { display: grid; grid-template-columns: repeat(8, 1fr); }
/* The 1px gap between squares IS the grid line: the board colour shows through. */
.reversi-board .square { aspect-ratio: 1; border: 0; padding: 0; position: relative; display: block;
                         background: var(--felt, #237a4f); outline-offset: -3px;
                         margin: 0 1px 1px 0; cursor: default; }
.reversi-board button.square:not(:disabled) { cursor: pointer; }
.reversi-board button.square:disabled { opacity: 1; }      /* a square is not a greyed control */

.reversi-board .disc { position: absolute; inset: 12%; border-radius: 50%; display: none;
                       box-shadow: inset 0 -2px 4px rgba(0,0,0,.35), 0 2px 3px var(--shadow);
                       transition: transform .22s ease, background .22s ease; }
.reversi-board .disc-cell .disc { display: block; }

/* A DISC IS NOT TOLD APART BY COLOUR ALONE. Dark and light differ in lightness,
   in border and in the inset highlight, so the board survives a monochrome
   screen and colour-blindness both. */
.reversi-board .dark .disc  { background: #24242a; border: 1px solid #0d0d11; }
.reversi-board .light .disc { background: #f4f1e8; border: 1px solid #b8b2a4;
                              box-shadow: inset 0 2px 4px rgba(255,255,255,.7), 0 2px 3px var(--shadow); }

/* A playable square is a translucent dot, which is checkers' .can-land marker
   doing the same job: saying where a move is legal without drawing a disc. */
.reversi-board .square.playable::before { content: ''; position: absolute; inset: 34%; border-radius: 50%;
                                          background: var(--brass); opacity: .5; }
.reversi-board .square.playable:hover::before { opacity: .8; }
.reversi-board .square.hinted { outline: 3px dashed var(--brass); }
.reversi-board .square.last-move::after { content: ''; position: absolute; inset: 0; pointer-events: none;
                                          box-shadow: inset 0 0 0 3px var(--brass); opacity: .55; }
.reversi-board .square:focus-visible { outline: 3px solid var(--accent); z-index: 1; }

/* THE TURN. A disc that changes hands rolls over rather than blinking, so a
   move that turns six discs reads as one action. The transform is on the
   transition the disc already had. */
.reversi-board .square.turning .disc { transform: rotateY(180deg); }
@media (prefers-reduced-motion: reduce) { .reversi-board .disc { transition: none !important; } }

.reversi .files { display: grid; grid-template-columns: repeat(8, 1fr); gap: 0;
                  width: min(100%, 460px); box-sizing: border-box; margin: 0 auto .6rem; padding: 0 6px; text-align: center; }
.reversi .files span { font-size: .75rem; }
.reversi-count { display: flex; align-items: center; gap: .4rem; justify-content: center; margin: .2rem 0 .6rem; }
.reversi-count .swatch { width: .9rem; height: .9rem; border-radius: 50%; display: inline-block; }
.reversi-count .swatch.dark { background: #24242a; border: 1px solid #0d0d11; }
.reversi-count .swatch.light { background: #f4f1e8; border: 1px solid #b8b2a4; }
.reversi-moves { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; margin-top: .6rem; }
@media (orientation: landscape) and (max-height: 520px) {
    .reversi-board, .reversi .files { width: min(100%, 460px, calc(100vh - 6rem)); }
}

/* ---- chess ------------------------------------------------------------------------------- */
/* The same walnut and the same tokens as checkers, so the game looks like the site and not
   like a chess program. Every square is a button; the pieces are the inline SVG symbols in
   the template, filled and outlined by the colour class on the square. */
/* The squares are tan and brown, not the cream and near-black the PIECES are: a board
   whose light square is the white piece's own colour turns half the set into outlines. */
.chess { margin: 0 0 .8rem; --sq-light: #d7bd97; --sq-dark: #7a5232; --piece-light: #fbf7ee; --piece-dark: #241a12; }
.chess-symbols { position: absolute; width: 0; height: 0; overflow: hidden; }
.chess-frame { width: min(100%, 500px); margin: 0 auto .6rem; }
@media (orientation: landscape) and (max-height: 520px) { .chess-frame { width: min(100%, 500px, calc(100vh - 6rem)); } }
.chess-board { display: grid; grid-template-rows: repeat(8, auto); gap: 0; padding: 6px 6px 6px 0;
               background: var(--walnut); border-radius: 10px; box-sizing: border-box;
               box-shadow: inset 0 2px 8px rgba(0,0,0,.35), 0 3px 10px var(--shadow); touch-action: none; }
.chess-board .rank { display: grid; grid-template-columns: 1.1rem repeat(8, 1fr); align-items: center; }
.chess-board .rank-label, .chess .files .rank-label { font-size: .7rem; color: var(--brass); text-align: center; font-family: var(--body); }
.chess .files { display: grid; grid-template-columns: 1.1rem repeat(8, 1fr); gap: 0; margin: .2rem 0 0; padding: 0 6px 0 0; text-align: center; }
.chess .files span { font-size: .75rem; }
.chess-board .square { aspect-ratio: 1; border: 0; padding: 0; position: relative; display: block; min-width: 0; }
.chess-board .square.light { background: var(--sq-light); }
.chess-board .square.dark { background: var(--sq-dark); }
.chess-board button.square { cursor: default; }
.chess-board button.square:not(:disabled) { cursor: pointer; }
.chess-board button.square:disabled { opacity: 1; }                /* a square is not a greyed control */
.chess-board .piece { position: absolute; inset: 6%; display: block; pointer-events: none; }
.chess-board .piece svg, .chess-drag svg { display: block; width: 100%; height: 100%; stroke-width: 1.6; stroke-linejoin: round; }
.chess-board .white .piece svg, .chess-drag.white svg { fill: var(--piece-light); stroke: var(--piece-dark); }
.chess-board .black .piece svg, .chess-drag.black svg { fill: var(--piece-dark); stroke: var(--piece-light); }
.chess-drag { --piece-light: #fbf7ee; --piece-dark: #241a12; }
.chess-board .square.selected .piece { transform: scale(1.1); filter: drop-shadow(0 0 3px var(--brass)); }
.chess-board .square.dragging .piece { opacity: .35; }
.chess-board .square.can-land::before { content: ''; position: absolute; inset: 36%; border-radius: 50%;
                                        background: rgba(30, 20, 10, .45); }
.chess-board .square.can-land.pawn::before, .chess-board .square.can-land.knight::before, .chess-board .square.can-land.bishop::before,
.chess-board .square.can-land.rook::before, .chess-board .square.can-land.queen::before, .chess-board .square.can-land.king::before {
    inset: 0; border-radius: 0; background: none; box-shadow: inset 0 0 0 4px var(--brass); }
.chess-board .square.would-take { box-shadow: inset 0 0 0 3px var(--danger); }
.chess-board .square.in-check { background: radial-gradient(circle, var(--danger) 0, var(--danger) 35%, transparent 75%); }
.chess-board .square.hinted { outline: 3px dashed var(--brass); outline-offset: -4px; }
.chess-board .square.last-move::after { content: ''; position: absolute; inset: 0; pointer-events: none;
                                        box-shadow: inset 0 0 0 3px var(--brass); opacity: .5; }
.chess-board .square:focus-visible { outline: 3px solid var(--accent); outline-offset: -3px; z-index: 1; }
@media (max-width: 400px) { .chess-board .rank { grid-template-columns: .8rem repeat(8, 1fr); } .chess .files { grid-template-columns: .8rem repeat(8, 1fr); } }
/* the piece the drag carries: appended to the body, positioned by the client */
.chess-drag { position: fixed; z-index: 20; pointer-events: none; }
.draw-controls { display: flex; flex-wrap: wrap; gap: .4rem; margin: .3rem 0; }

/* ---- connect four ------------------------------------------------------------------------ */
/* Sized the same way as the checkers board: the page's width up to a limit, seven equal
   columns, a cell as tall as it is wide. */
.c4-board { display: grid; grid-template-columns: repeat(7, 1fr); gap: 0; padding: 1%; margin: 0 0 .8rem;
            background: var(--peg-blue); border-radius: 12px; width: min(100%, 488px); box-sizing: border-box;
            box-shadow: inset 0 2px 8px rgba(0,0,0,.35), 0 3px 10px var(--shadow); }
@media (orientation: landscape) and (max-height: 520px) { .c4-board { width: min(100%, 488px, calc((100vh - 6rem) * 7 / 6)); } }
/* The space between cells is a margin on the cell, as a share of the column's WIDTH
   (a percentage margin always resolves against the width, top and bottom included).
   A percentage row gap would resolve against the column's height, which is not known
   until the cells are placed, so the gap was added after the board was sized and the
   bottom row hung below the blue. */
.c4-col { display: grid; gap: 0; padding: 0; border: 0; background: none; cursor: pointer; border-radius: 6px; min-width: 0; }
.c4-col:disabled { cursor: default; }
.c4-col:not(:disabled):hover .c4-cell:not(.r):not(.y) { background: rgba(255,255,255,.35); }
.c4-col:focus-visible { outline: 3px solid var(--brass); outline-offset: 2px; }
.c4-col.hinted { outline: 3px dashed var(--brass); outline-offset: 2px; }
.c4-cell { display: block; aspect-ratio: 1; margin: 6%; border-radius: 50%; background: var(--ground);
           box-shadow: inset 0 2px 4px rgba(0,0,0,.35); transition: background .15s ease; }
.c4-cell.r { background: radial-gradient(circle at 35% 35%, #e0584a, var(--peg-red) 60%, #8f2419); }
.c4-cell.y { background: radial-gradient(circle at 35% 35%, #ffe680, #e6b422 60%, #a67c00); }
.c4-cell.win { box-shadow: inset 0 0 0 4px var(--card), inset 0 2px 4px rgba(0,0,0,.35); }

/* ---- rock, paper, scissors -------------------------------------------------------------- */
.rps-choices { display: flex; flex-wrap: wrap; gap: .6rem; margin: 0 0 .8rem; }
.rps-choice { font-family: var(--display); font-size: 1.3rem; padding: .9rem 1.4rem; min-width: 7.5rem; }
.rps-choice.chosen { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
.rounds { padding-inline-start: 1.2rem; display: grid; gap: .25rem; }

.badge { font-family: var(--display); font-size: 1.1rem; padding: 0 .35rem; border-radius: 4px; background: var(--surface-2); }
/* a seat the clock ran out on: the player's name stays, this says who is
   moving it. Quiet on purpose, because it is a note and not a score. */
.bot-seat { font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; padding: 0 .3rem;
            border: 1px solid var(--line); border-radius: 4px; color: var(--muted); }

/* ---- the game page ---------------------------------------------------------------- */
.game { display: grid; gap: .9rem; }
.game-head { display: grid; gap: .3rem; padding-top: 1em; }
.scores { display: flex; flex-wrap: wrap; gap: .5rem; }
.score { display: inline-flex; align-items: center; gap: .5rem; padding: .35rem .8rem .35rem .5rem; background: var(--surface);
         border: 1px solid var(--line); border-radius: 999px; font-variant-numeric: tabular-nums; }
.score b { font-family: var(--display); font-weight: 400; font-size: 1.3rem; min-width: 2ch; text-align: end; }
.score .name { color: var(--muted); }
.score .dealer { font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--accent); font-weight: 600; }
/* A seat's swatch. The COLOUR IS NEVER THE ONLY DIFFERENCE: each seat has its
 * own shape and carries its own number, because four colours that a
 * colour-blind player can reliably separate do not exist. At three or four
 * seats the marker is how somebody finds their own score on a crowded page.
 *
 * Red and blue keep their shapes, so every two-seat game looks exactly as it
 * did. */
.swatch {
  width: 16px; height: 16px; display: inline-block;
  font-size: 10px; line-height: 16px; text-align: center;
  color: #fff; font-weight: 700;
}
.swatch.red { background: var(--peg-red); border-radius: 50%; }
.swatch.blue { background: var(--peg-blue); border-radius: 2px; }
.swatch.green { background: var(--peg-green); border-radius: 50% 2px 50% 2px; }
.swatch.amber { background: var(--peg-amber); border-radius: 2px 50% 2px 50%; color: #3a2a00; }
/* THE META LINE CARRIES A COUNTDOWN ON ITS RIGHT (plan: the owner, 20 Sep
   2026). Flex with the countdown pushed over by margin-left:auto rather than
   space-between, because everything before it is INLINE TEXT with middots
   between the parts: space-between would spread those parts across the line.
   One flex item that pushes itself right leaves the sentence alone. */
.meta { margin: 0; font-size: .9rem; display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 .35rem; }
.meta-countdown {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--ink);
}
/* the last minute. A colour AND a weight, because a colour alone says nothing
   to a reader who cannot see it and nothing on a monochrome screen. */
.meta-countdown[data-urgent] { color: var(--peg-red); }
@media (prefers-reduced-motion: reduce) {
  .meta-countdown { transition: none !important; animation: none !important; }
}

.board { background: linear-gradient(170deg, #6a4529, var(--walnut) 40%, var(--walnut-deep)); border-radius: 10px;
         padding: .6rem .8rem; box-shadow: inset 0 2px 8px rgba(0,0,0,.35), 0 3px 10px var(--shadow); overflow-x: auto; }
.board svg { display: block; width: 100%; min-width: 480px; height: auto; }
.board .holes circle { fill: var(--hole); }
.board .holes .finish { stroke: var(--brass); stroke-width: 1.2; }
.board .labels text { font-family: var(--display); font-size: 6px; fill: var(--brass); }
.board .peg { transition: transform .55s cubic-bezier(.4, 0, .2, 1); }
.board .peg circle, .board .peg rect { stroke: rgba(0,0,0,.45); stroke-width: .6; }
.board .peg.p1 circle { fill: var(--peg-red); }
.board .peg.p2 rect { fill: var(--peg-blue); }
.board .peg .shine { fill: rgba(255,255,255,.55); stroke: none; }
/* SECOND TIME ROUND. The board is two rows walked twice, so hole 7 and hole
   67 are one hole and the position alone is short by sixty. A peg on its
   second lap wears a brass ring; the header carries the number itself, and
   the svg's aria-label reads both scores out, so this is the sighted
   reader's half of the same fact. */
.board .peg[data-lap="2"] circle, .board .peg[data-lap="2"] rect { stroke: var(--brass); stroke-width: 1.3; }
.board .peg[data-lap="2"] .shine { stroke: none; }
@media (prefers-reduced-motion: reduce) { .board .peg, .card, .checkers-board .disc, .chess-board .piece, .billiards .layer, .minigolf .layer { transition: none !important; } }

.status { font-size: 1.1rem; min-height: 1.5em; margin: 0; }

.table { display: grid; gap: .8rem; padding: .8rem; border-radius: 10px; background: var(--surface-2); border: 1px dashed var(--line); }
@media (min-width: 640px) { .table { grid-template-columns: 1fr auto; align-items: start; } }
.seq { display: flex; flex-wrap: wrap; gap: .4rem; align-items: flex-start; min-height: 72px; }
.seq .who { font-size: .7rem; color: var(--muted); text-align: center; display: grid; gap: 2px; justify-items: center; }
.side { display: flex; flex-wrap: wrap; gap: .8rem; align-items: flex-start; }
.count { font-family: var(--display); font-size: 2rem; line-height: 1; min-width: 3ch; text-align: center; font-variant-numeric: tabular-nums; }
.count small { display: block; font-family: var(--body); font-size: .65rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.starterbox, .cribbox, .oppbox { display: grid; gap: .2rem; justify-items: center; }
.label { font-size: .65rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.pile { display: inline-grid; place-items: center; width: 46px; aspect-ratio: 5/7; border-radius: 5px; font-family: var(--display); font-size: 1.2rem;
         background: repeating-linear-gradient(45deg, transparent 0 4px, var(--back-line) 4px 5px), repeating-linear-gradient(-45deg, transparent 0 4px, var(--back-line) 4px 5px), var(--back);
         color: var(--card); box-shadow: inset 0 0 0 3px var(--card); }

/* ---- cards ----------------------------------------------------------------------------- */
.hand .cards { display: flex; flex-wrap: wrap; gap: .5rem; padding-top: 16px; min-height: 108px; }
.card { --w: 66px; display: inline-block; width: var(--w); aspect-ratio: 5 / 7; font-size: calc(var(--w) * .21); position: relative; padding: 0;
        border-radius: calc(var(--w) * .09); background: var(--card); color: var(--card-ink); border: 1px solid var(--card-edge);
        box-shadow: 0 1px 2px var(--shadow); font-family: var(--display); cursor: pointer; flex: 0 0 auto;
        transition: transform .18s ease, box-shadow .18s ease; }
.card.red { color: var(--card-red); }
.card.sm { --w: 46px; }
.card.xs { --w: 34px; }   /* the app's xs (plan_cards/04): a rival's suit in one row, the rest of Rummy's pile; 'lg' 96 lands with the settings */
.card.lg { --w: 96px; }   /* the app's lg (plan_cards/05): a tablet pane, "A TABLET IS NOT A BIG PHONE" */
.card .idx { position: absolute; top: .3em; left: .38em; line-height: 1; text-align: center; }
.card .idx small { display: block; font-size: .8em; font-family: var(--body); }
.card .idx.b { top: auto; left: auto; bottom: .3em; right: .38em; transform: rotate(180deg); }
.card .pip { position: absolute; inset: 0; display: grid; place-items: center; font-size: 1.8em; }
.card:not(:disabled):hover { transform: translateY(-4px); }
.card[aria-pressed="true"] { transform: translateY(-14px); box-shadow: 0 8px 16px var(--shadow); outline: 2px solid var(--accent); outline-offset: 2px; }
.card:disabled { cursor: default; }
.card:disabled:not(.back):not(.static) { opacity: .42; }
.card.back { background: repeating-linear-gradient(45deg, transparent 0 4px, var(--back-line) 4px 5px), repeating-linear-gradient(-45deg, transparent 0 4px, var(--back-line) 4px 5px), var(--back);
             border-color: rgba(0,0,0,.3); box-shadow: inset 0 0 0 3px var(--card), 0 1px 2px var(--shadow); }
.card.hi { outline: 2px solid var(--brass); outline-offset: 1px; }
.card.new { animation: deal .35s ease-out; }
@keyframes deal { from { transform: translateY(8px); opacity: 0; } to { transform: none; opacity: 1; } }

.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; min-height: 2.4rem; padding-top:1em;}
.emotes { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.toast { margin: 0; padding: .3rem .7rem; border-radius: 999px; background: var(--surface); border: 1px solid var(--line); }

/* In-game chat. The log scrolls rather than growing the page, because a long
   conversation must not push the board off screen on a phone. */
.chat { margin: .8rem 0; }
.chat-log { list-style: none; margin: 0 0 .5rem; padding: .4rem .6rem; max-height: 11rem; overflow-y: auto;
            background: var(--surface); border: 1px solid var(--line); border-radius: 8px; font-size: .94rem; }
.chat-log:empty { display: none; }
.chat-log li { margin: .15rem 0; overflow-wrap: anywhere; }
.chat-who { font-family: var(--display); }
/* The report button sits out of the way until the line is hovered or the
   button is focused: it must be reachable by keyboard, and it must not be the
   loudest thing in a friendly conversation. */
.chat-report { opacity: 0; transition: opacity .15s; }
.chat-log li:hover .chat-report, .chat-report:focus-within { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .chat-report { transition: none; } }
.chat-say { display: flex; gap: .4rem; }
.chat-say input { flex: 1 1 auto; min-width: 0; }
.chat-off { margin: .6rem 0; }
.hand-ends details { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: .4rem .7rem; margin: .3rem 0; }
.hand-ends summary { cursor: pointer; font-family: var(--display); }
.hand-ends ul { margin: .3rem 0 .2rem; padding-inline-start: 1.1rem; font-size: .92rem; }
/* a card in a line of text: the count says what scored, these say from what */
.pip-cards { display: inline-flex; flex-wrap: wrap; gap: .2rem; vertical-align: -.15em; }
.pip-card { display: inline-flex; align-items: baseline; gap: .05em; padding: .05em .3em;
            border: 1px solid var(--line); border-radius: 4px; background: var(--card);
            font-family: var(--display); font-size: .88em; line-height: 1.35; white-space: nowrap; color: black; }
.pip-card.red { color: var(--card-red); }
.pip-card small { font-size: 1.05em; }
.pip-card.cut { border-color: var(--brass); }

/* ---- the count: cribbage's scoring phase -------------------------------------------------
   Three panels in the order a real board counts them - the non-dealer's hand,
   the dealer's hand, then the crib - each with its four cards and the cut.
   The server renders the whole thing revealed (games/cribbage.tmpl) so it
   reads with scripting off; cribbage.js hides the rows again and deals them
   back one at a time, lighting the cards each combination is made of.
   Logical properties throughout: this is page content and it turns in Arabic,
   unlike the board itself. */
.tally { display: grid; gap: .8rem; padding: .9rem 1rem; margin: .8rem 0; border-radius: 10px;
         background: var(--surface); border: 1px solid var(--line); }
.tally.live { border-color: var(--brass); }
.tally-head { display: flex; flex-wrap: wrap; gap: .6rem; align-items: baseline; justify-content: space-between; }
.tally-head h2 { margin: 0; font-size: 1.25rem; }
.tally-parts { display: grid; gap: .8rem; }
@media (min-width: 900px) { .tally-parts { grid-template-columns: repeat(3, 1fr); align-items: start; } }
.tally-part { padding: .6rem .7rem; border-radius: 8px; background: var(--surface-2);
              border: 1px solid var(--line); transition: opacity .3s ease, border-color .3s ease; }
.tally-part.waiting { opacity: .42; }
.tally-part.active { border-color: var(--brass); }
.tally-part h3 { margin: 0 0 .45rem; font-size: 1rem; }
.tally-cards { display: flex; flex-wrap: wrap; gap: .35rem; }
/* the cut stands apart from the four: it belongs to all three panels */
.tally-cards .card.cut { margin-inline-start: .55rem; }
.tally .card { cursor: default; }
.tally .card:not(.lit):hover { transform: none; }
.tally-cards .card.lit { transform: translateY(-6px); outline: 2px solid var(--accent); outline-offset: 2px; }
.tally-lines { list-style: none; margin: .55rem 0 .3rem; padding: 0; display: grid; gap: .25rem; font-size: .92rem; }
.tally-lines li { display: flex; flex-wrap: wrap; align-items: baseline; gap: .35rem; }
.tally-lines li.now { animation: tally-in .3s ease-out; }
.tally-what { font-family: var(--display); }
.tally-pts { margin-inline-start: auto; font-family: var(--display); font-variant-numeric: tabular-nums; }
.tally-total { margin: .35rem 0 0; font-family: var(--display); font-size: 1.05rem; }
@keyframes tally-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
/* One frame with the peg transition off, while the count puts the pegs back
   to where it found them: a peg animating BACKWARDS down a cribbage board
   means something, and it is never true. */
.board.no-anim .peg { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .tally-part { transition: none; }
  .tally-lines li.now { animation: none; }
  .tally-cards .card.lit { transform: none; }
}
.game-actions { display: flex; flex-wrap: wrap; gap: .8rem; align-items: center; }
.game-actions p { margin: 0; }
/* ---- the referral banner, above the footer on every page --------------------------------- */
.promo { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem 1.2rem; margin: 2.5rem 0 0; padding: .9rem 1.1rem;
         background: var(--surface); border: 1px solid var(--line); border-radius: 10px; }
.promo-text { margin: 0; flex: 1 1 24ch; }
.promo-go { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: .4rem .8rem; }
.promo-mark { font-size: .7rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
/* BOTH MARKS ARE LINKS NOW, to /partners, and a class selector beats the bare
   `a` rule, so without this they keep the small-print grey and no underline:
   a link nobody can see is a link nobody follows, and the whole point of it
   is somebody coming back to find a product they saw mid-game. Underlined
   rather than accent-coloured, so the disclosure still reads as small print
   and does not start competing with the banner above it. */
a.promo-mark, a.partner-mark { text-decoration: underline; text-underline-offset: .2em;
                               text-decoration-color: var(--line); }
a.promo-mark:hover, a.partner-mark:hover { color: var(--ink); text-decoration-color: currentColor; }

/* ---- the modal: somebody joined your game ---------------------------------------------- */
dialog.modal { border: 1px solid var(--line); border-radius: 14px; background: var(--surface); color: var(--ink);
               padding: 2rem 2.2rem; max-width: min(34rem, 92vw); box-shadow: 0 16px 48px var(--shadow); }
dialog.modal::backdrop { background: rgba(20, 14, 8, .6); backdrop-filter: blur(2px); }
dialog.modal h2 { margin: 0 0 .5rem; font-size: 2rem; }
dialog.modal .lede { font-size: 1.15rem; margin: 0 0 1.4rem; }
dialog.modal .actions { margin: 0; }

/* ---- the bell, its panel, and the toast for somewhere else ------------------
 *
 * The panel is the first anchored thing on this site: absolutely positioned
 * inside a relative wrapper, which is why .bell-wrap has a position at all.
 * It relies on the global [hidden] rule at the top of this file, without
 * which its own display would hold it open for ever.
 *
 * On a narrow screen it stops being anchored and spans the header, because a
 * 20rem panel hanging off a bell is wider than a phone. */
.bell-wrap { position: relative; display: inline-flex; }
.bell { position: relative; display: inline-flex; align-items: center; color: var(--ink);
        text-decoration: none; padding: .2rem; }
.bell-badge { position: absolute; top: -.35rem; inset-inline-end: -.45rem; min-width: 1.15rem;
              padding: 0 .25rem; border-radius: 999px; background: var(--accent); color: var(--accent-ink);
              font-size: .7rem; line-height: 1.15rem; text-align: center; font-weight: 700; }
.bell-panel { position: absolute; inset-inline-end: 0; top: calc(100% + .5rem); z-index: 30;
              width: min(22rem, 85vw); max-height: min(70vh, 30rem); overflow-y: auto;
              padding: .6rem; background: var(--surface); color: var(--ink);
              border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 12px 32px var(--shadow); }
.bell-empty { margin: .3rem .4rem; color: var(--muted); }
.bell-list { list-style: none; margin: 0; padding: 0; }
.bell-list li { border-top: 1px solid var(--line); }
.bell-list li:first-child { border-top: 0; }
/* A row is a link and a cross side by side: the link takes the room and the
   cross keeps its own hit area, big enough for a thumb, because what it does
   is the last thing that should be pressed by accident. */
.bell-list li { display: flex; align-items: center; gap: .2rem; }
.bell-list a { flex: 1; display: block; padding: .5rem .4rem; text-decoration: none; color: var(--ink); }
.bell-cross { flex: 0 0 auto; padding: .45rem .55rem; font-size: 1.1rem; line-height: 1;
              color: var(--muted); }
.bell-cross:hover, .bell-cross:focus-visible { color: var(--danger); }
.bell-list a:hover, .bell-list a:focus-visible { background: var(--surface-2); }
/* Not a dot beside it: the whole line reads heavier, which survives being
   looked at by somebody who cannot see a colour difference. */
.bell-list li.fresh a { font-weight: 600; }
@media (max-width: 30rem) {
  .bell-panel { position: fixed; inset-inline: .5rem; width: auto; top: 3.6rem; }
}

/* The cross-game toast. Fixed under the header rather than in the flow: it
   has to be noticeable from any page, and no page has a place set aside for
   it. Centred, because it is not anchored to anything. */
.notice-stack { position: fixed; top: .6rem; inset-inline: 0; z-index: 40;
                display: flex; flex-direction: column; align-items: center; gap: .4rem;
                pointer-events: none; }
.notice-toast { display: flex; align-items: center; gap: .5rem; max-width: min(28rem, 92vw);
                padding: .5rem .7rem; background: var(--surface); color: var(--ink);
                border: 1px solid var(--line); border-radius: 10px;
                box-shadow: 0 8px 24px var(--shadow); pointer-events: auto; }
.notice-toast a { color: var(--ink); text-decoration: none; }
.notice-toast a:hover, .notice-toast a:focus-visible { text-decoration: underline; }
.notice-shut { font-size: 1.1rem; line-height: 1; color: var(--muted); }
/* The confirmation, which is the same box asking a question rather than
   announcing news: no heading, the question at reading size, and the two
   answers side by side and thumb-sized. Narrower, because a question that
   runs the full width of a laptop reads as a paragraph. */
dialog.modal.confirm { padding: 1.6rem 1.8rem; max-width: min(26rem, 92vw); }
dialog.modal.confirm .confirm-say { margin: 0 0 1.2rem; font-size: 1.15rem; }
dialog.modal.confirm .actions { display: flex; flex-wrap: wrap; gap: .6rem; }
dialog.modal.confirm .actions .button { flex: 1 1 8rem; }
.button.big { font-size: 1.15rem; padding: .8rem 1.4rem; }
.rematch-prompt { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; padding: .6rem .8rem;
                  background: var(--surface); border: 1px solid var(--line); border-radius: 8px; }
.push-prompt { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; padding: .6rem .8rem; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; }
.push-prompt p { margin: 0; }

/* ---- consent and advertising (plan phase 08) ---------------------------------------- */
.consent { position: sticky; bottom: 0; margin: 1rem -1rem 0; padding: .8rem 1rem; background: var(--surface); border-top: 1px solid var(--line);
           display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; align-items: center; box-shadow: 0 -3px 12px var(--shadow); }
.consent p { margin: 0; max-width: 60ch; }
.consent-choice { display: flex; gap: .5rem; }
/* ---- the affiliate slot ----------------------------------------------------
 *
 * One image of ours inside one link, plus the disclosure. There is no network
 * and no script, so nothing here has to reserve space for something that may
 * or may not arrive: the image carries its own width and height attributes,
 * the browser derives an aspect-ratio from them, and the box is exactly as
 * tall as the creative from the first paint.
 *
 * NO min-height. The old rule reserved 90px against an ad network refusing a
 * zero-height slot; here it would reserve 90px of nothing on every page that
 * has no creative to show.
 *
 * NAMED `partner` RATHER THAN `ad`. Generic ad-blocking filter lists match any
 * element classed `ad-slot` and any path containing /ads/, and these are our
 * own first-party images and labelled affiliate links, not third-party ad
 * requests. The disclosure below is exactly as prominent either way.
 */
.partner-slot { margin: 1rem 0; display: flex; flex-direction: column; gap: .35rem; align-items: center; }
.partner-slot-header { max-width: 728px; margin-inline: auto; }
/* `text-decoration: none` because this anchor now carries WORDS as well as a
   picture. It held only an image until the caption went in, so the browser's
   default underline had nothing to underline and the omission did not show. */
.partner-link { display: block; line-height: 0; border-radius: 8px; overflow: hidden;
                border: 1px solid var(--line); background: var(--surface);
                text-decoration: none; max-width: 300px; }
.partner-link:hover .partner-caption { color: var(--accent); }
.partner-link img { display: block; max-width: 100%; height: auto; margin: auto; }
/* The product's own name, under its picture and inside the same link. The
   anchor sets line-height 0 so an image leaves no gap beneath it, which would
   collapse this to nothing, so the caption sets its own back. */
.partner-caption { display: block; line-height: 1.3; padding: .45rem .6rem;
                   font-family: var(--display); font-size: .95rem; color: var(--ink); }
/* The blurb under the name, in the body face rather than the display one so
   the two read as a heading and a sentence rather than as one long title. */
.partner-caption .partner-blurb { display: block; margin-top: .15rem;
                                  font-family: var(--body); font-size: .8rem; color: var(--muted); }
.partner-mark { font-size: .7rem; letter-spacing: .06em; text-transform: uppercase;
                color: var(--muted); text-align: center; }
/* The text card: a creative with no artwork yet, so a link can go into
   config/ads.yml before its picture exists. */
.partner-card { line-height: inherit; padding: .9rem 1.1rem; border-radius: 10px;
                text-decoration: none; color: var(--ink); }
.partner-card .partner-title { display: block; font-family: var(--display); font-size: 1.15rem; }
.partner-card .partner-blurb { display: block; margin-top: .2rem; color: var(--muted); }

/* ---- /partners: everything the site links to, in one list ------------------
 *
 * A grid of cards rather than the slot layout: this is a catalogue somebody
 * came here to read, not an advert beside something else they were doing.
 */
/* `align-items: start` so a card is as tall as its own content. Without it a
   grid item stretches to the tallest in its row, and a creative with no
   artwork yet becomes a tall empty box beside one with a photo. */
.partner-list { list-style: none; padding: 0; margin: 1.5rem 0; align-items: start;
                display: grid; gap: 1rem; grid-template-columns: 1fr; }
@media (min-width: 620px)  { .partner-list { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1000px) { .partner-list { grid-template-columns: repeat(3, 1fr); } }
.partner-item-link { display: flex; flex-direction: column; gap: .6rem; height: 100%;
                     padding: .9rem; border: 1px solid var(--line); border-radius: 10px;
                     background: var(--surface); color: var(--ink); text-decoration: none; }
.partner-item-link:hover { border-color: var(--accent); }
.partner-item-link img { display: block; width: 100%; height: auto; border-radius: 6px; }
.partner-item-title { font-family: var(--display); font-weight: 400; font-size: 1.1rem; }
.partner-item-text { display: flex; flex-direction: column; gap: .25rem; }

/* ---- the below-board slot: three in a row on a desktop, one on a phone -----
 *
 * The server renders a page once and cannot know the screen it is going to,
 * so all three are in the markup and the narrow layout hides two. That is the
 * whole reason this is CSS and not a count decided in Perl: deciding it server
 * side would mean either guessing from the user agent, which is wrong often
 * enough to matter, or varying the response on screen width, which no cache
 * can key on.
 *
 * THE HIDDEN TWO COST A PHONE NOTHING TO SPEAK OF. Every banner carries
 * loading="lazy", and a lazy image inside a display:none box is never in the
 * viewport, so it is not fetched. If that attribute is ever removed from
 * _ads.tmpl, a phone starts downloading two pictures it will not show.
 *
 * `~` rather than nth-child: the slot's last element is the disclosure span,
 * so counting children would count that too. "A partner-link preceded by a
 * partner-link" is the second and third whatever else is in the box.
 */
@media (max-width: 759px) {
  .partner-slot-below_board .partner-link ~ .partner-link { display: none; }
}
@media (min-width: 760px) {
  .partner-slot-below_board { flex-direction: row; flex-wrap: wrap;
                              justify-content: center; align-items: flex-start; gap: .75rem; }
  /* the disclosure sits under the row rather than beside the last banner */
  .partner-slot-below_board .partner-mark { flex-basis: 100%; }
  .partner-slot-below_board .partner-link { flex: 0 1 auto; min-width: 0; }
}
.with-sidebar { display: grid; gap: 1.5rem; grid-template-columns: 1fr; }
@media (min-width: 1100px) { .with-sidebar:has(.sidebar) { grid-template-columns: minmax(0, 1fr) 300px; } .sidebar { position: sticky; top: 1rem; align-self: start; } }
.card.hinted { outline: 2px dashed var(--brass); outline-offset: 2px; }
/* THE BACK'S COLOUR IS A TOKEN OVERRIDE (plan_cards/05). The weave is written once,
   on .card.back and .pile above, and both read --back, so a colour is one line and
   the pile is themed for free. classic is :root's --back and has no rule. */
.theme-crimson { --back: #8a2f2a; }
.theme-sage    { --back: #4f6b4a; }
.theme-plum    { --back: #5b3a6e; }
.theme-slate   { --back: #3a4a5c; }
.theme-gold    { --back: #9c7227; }
.theme-ink     { --back: #2a2522; }
aside.sidebar {
    overflow: hidden;
}
/* ---- ludo ---------------------------------------------------------------------------- */
/* The cross board: fifteen by fifteen, so the arms, the yards and the middle
   all fall out of the grid rather than being positioned by hand. */
.ludo { display: grid; gap: .8rem; justify-items: center; }
.ludo-board { display: grid; grid-template-rows: repeat(15, 1fr);
              background: var(--surface-2); border: 1px solid var(--line); border-radius: 8px;
              padding: 4px; width: min(90vw, 30rem); }
.ludo-row { display: grid; grid-template-columns: repeat(15, 1fr); }
.ludo-board .cell { aspect-ratio: 1; position: relative; display: grid; place-items: center;
                    border: 1px solid transparent; }
.ludo-board .cell.track { background: var(--card); border-color: var(--line); }
.ludo-board .cell.blank { background: transparent; }
.ludo-board .cell.finish { background: var(--brass); border-radius: 4px; }
.ludo-board .cell.home.red, .ludo-board .cell.yard.red, .ludo-board .cell.entry.red { background: var(--peg-red); }
.ludo-board .cell.home.blue, .ludo-board .cell.yard.blue, .ludo-board .cell.entry.blue { background: var(--peg-blue); }
.ludo-board .cell.home.green, .ludo-board .cell.yard.green, .ludo-board .cell.entry.green { background: #3f7d4f; }
.ludo-board .cell.home.yellow, .ludo-board .cell.yard.yellow, .ludo-board .cell.entry.yellow { background: #c8992f; }
.ludo-board .cell.yard { border-radius: 50%; opacity: .35; }
.ludo-board .cell.can-land::before { content: ''; position: absolute; inset: 18%; border-radius: 50%;
                                     border: 2px dashed var(--brass); }
/* A token carries its NUMBER as well as its colour: four seats need four
   colours, and colour alone is not a distinction everybody can see. */
.ludo-board .token { position: absolute; inset: 6%; border-radius: 50%; border: 1px solid var(--card-edge);
                     font: 600 clamp(7px, 1.4vw, 11px)/1 var(--body); color: var(--accent-ink);
                     display: grid; place-items: center; padding: 0; cursor: default; }
.ludo-board .token.red { background: var(--peg-red); }
.ludo-board .token.blue { background: var(--peg-blue); }
.ludo-board .token.green { background: #3f7d4f; }
.ludo-board .token.yellow { background: #c8992f; color: var(--card-ink); }
.ludo-board .token.playable { cursor: pointer; box-shadow: 0 0 0 2px var(--brass); }
.ludo-board .token.playable:hover { transform: scale(1.12); }
.ludo-board .token.hinted { outline: 2px dashed var(--brass); outline-offset: 2px; }
.ludo-board .token:disabled { opacity: 1; }
.ludo-board .token:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; z-index: 2; }

.ludo-seats { list-style: none; display: flex; flex-wrap: wrap; gap: .4rem .9rem; padding: 0; margin: 0; }
.ludo-seats li { display: flex; align-items: center; gap: .4rem; padding: .2rem .5rem; border-radius: 999px;
                 border: 1px solid transparent; font-size: .9rem; }
.ludo-seats li.on-turn { border-color: var(--brass); background: var(--surface); }
.ludo-seats .dot { width: .7rem; height: .7rem; border-radius: 50%; }
.ludo-seats .dot.red { background: var(--peg-red); } .ludo-seats .dot.blue { background: var(--peg-blue); }
.ludo-seats .dot.green { background: #3f7d4f; }     .ludo-seats .dot.yellow { background: #c8992f; }
.ludo-seats .tally { color: var(--muted); }

/* The die is shown, not thrown: the roll came from the seed with the state. */
.ludo-side { text-align: center; }
.die-wrap { display: flex; align-items: center; gap: .6rem; justify-content: center; margin: 0 0 .3rem; }
.die { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2px; place-items: center;
       width: 2.4rem; height: 2.4rem; padding: .3rem; border-radius: 8px;
       background: var(--card); border: 1px solid var(--line); color: var(--muted); }
.die i { width: .38rem; height: .38rem; border-radius: 50%; background: var(--card-ink); }
.die.idle { color: var(--muted); font-size: 1.4rem; }
.die.rolled { animation: die-in .25s ease-out; }
@keyframes die-in { from { transform: rotate(-12deg) scale(.85); } to { transform: none; } }
.ludo .small { font-size: .8rem; max-width: 42ch; margin: 0 auto; }
@media (prefers-reduced-motion: reduce) { .die.rolled { animation: none; } .ludo-board .token { transition: none; } }

/* ---- backgammon ---------------------------------------------------------------------- */
/* The real board: four quadrants split by the bar, trays outside, points as
   triangles. The viewer's home board is always the bottom right, whichever
   seat they hold, so neither player has to do arithmetic to read it.
   The checkers are WHITE and BLACK, which is what the badges say and what a
   backgammon set looks like. */
.bg { display: grid; gap: .6rem; justify-items: center; }
.bg-board { width: min(96vw, 44rem); background: var(--walnut-deep);
            border: 3px solid var(--walnut); border-radius: 8px; padding: 4px;
            display: grid; gap: 0; }
.bg-half { display: grid; grid-template-columns: 6fr auto 6fr auto; gap: 3px; }
.bg-quad { display: grid; grid-template-columns: repeat(6, 1fr); gap: 1px;
           background: var(--walnut); border-radius: 2px; }
.bg-quad.home { box-shadow: inset 0 0 0 2px var(--brass); }

/* A point is a triangle with the checkers stacked from its base: down from
   the top row, up from the bottom, which is how they sit on a board. The
   number sits on the OUTER edge, away from the checkers and away from the
   bar, which is where a real board prints it. */
.bg-board .pt { position: relative; height: 8.5rem; cursor: default;
                display: flex; justify-content: center; }
.bg-half.top .pt    { align-items: flex-start; padding-top: .85rem; }
.bg-half.bottom .pt { align-items: flex-end;  padding-bottom: .85rem; }
.bg-board .tri { position: absolute; left: 0; right: 0; }
.bg-half.top .tri    { top: .85rem; bottom: 0; clip-path: polygon(0 0, 100% 0, 50% 100%); }
.bg-half.bottom .tri { top: 0; bottom: .85rem; clip-path: polygon(50% 0, 100% 100%, 0 100%); }
.bg-board .tri { background: #efe6d2; }
.bg-board .pt.dark .tri { background: #6b4a2f; }
.bg-board .pn { position: absolute; left: 0; right: 0; text-align: center;
                font: 600 .6rem/.85rem var(--body); color: var(--muted); }
.bg-half.top .pn    { top: 0; }
.bg-half.bottom .pn { bottom: 0; }

/* Checkers overlap slightly, as a real stack does, so five fit in a point. */
.bg-board .men { position: relative; display: flex; flex-direction: column;
                 align-items: center; z-index: 1; }
.bg-half.bottom .men { flex-direction: column-reverse; }
.bg-board .man { width: 1.3rem; height: 1.3rem; border-radius: 50%; display: block;
                 box-shadow: inset 0 0 0 1px rgba(0,0,0,.35), 0 1px 1px var(--shadow); }
.bg-board .man + .man { margin-top: -.12rem; }
.bg-half.bottom .man + .man { margin-top: 0; margin-bottom: -.12rem; }
.bg-board .man.white { background: #f4efe2; }
.bg-board .man.black { background: #2b2622; }
.bg-board .more { font: 700 .62rem/1 var(--body); color: var(--ink);
                  background: var(--brass); border-radius: 999px; padding: 1px 5px; margin-top: 1px; }

/* what may be picked, and where it may go: the server decided both */
.bg-board .pt.can-from, .bg-board .pt.can-to { cursor: pointer; }
.bg-board .pt.can-from .tri { filter: brightness(1.12); }
.bg-board .pt.can-from::after, .bg-board .pt.can-to::after {
    content: ''; position: absolute; left: 50%; transform: translateX(-50%);
    width: .5rem; height: .5rem; border-radius: 50%; background: var(--brass); }
.bg-half.top .pt.can-from::after, .bg-half.top .pt.can-to::after { bottom: .3rem; }
.bg-half.bottom .pt.can-from::after, .bg-half.bottom .pt.can-to::after { top: .3rem; }
.bg-board .pt.can-to .tri { filter: brightness(1.25); box-shadow: inset 0 0 0 2px var(--accent); }
.bg-board .pt.picked .tri { box-shadow: inset 0 0 0 3px var(--accent); filter: brightness(1.3); }
.bg-board .pt:focus-visible { outline: 3px solid var(--accent); outline-offset: -3px; z-index: 2; }

.bg-bar { width: 2rem; background: var(--walnut-deep); border-radius: 2px;
          display: flex; align-items: center; justify-content: center; }
.bg-tray { width: 1.9rem; background: var(--walnut-deep); border-radius: 2px;
           display: grid; place-items: center; box-shadow: inset 0 0 0 1px var(--walnut); }
.bg-tray.mine { box-shadow: inset 0 0 0 2px var(--brass); }
/* bearing off is a move to the tray, so the tray lights up like a point does */
.bg-tray.can-to { cursor: pointer; background: var(--walnut);
                  box-shadow: inset 0 0 0 3px var(--accent); filter: brightness(1.15); }
.bg-tray:focus-visible { outline: 3px solid var(--accent); outline-offset: -3px; }
.tray-count { font: 700 .85rem/1 var(--display); color: var(--accent-ink); }

.bg-under { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; align-items: center; justify-content: center; }
.bg-pips { margin: 0; font-size: .9rem; }
.bg-dice { display: flex; gap: .4rem; align-items: center; }
.bg-dice .die { display: grid; grid-template: repeat(3, 1fr) / repeat(3, 1fr);
                width: 2.1rem; height: 2.1rem; padding: .22rem; gap: 1px;
                border-radius: 7px; background: #f4efe2;
                border: 1px solid var(--line); box-shadow: 0 1px 3px var(--shadow); }
.bg-dice .pip { width: .34rem; height: .34rem; border-radius: 50%; background: #2b2622;
                align-self: center; justify-self: center; }
.bg-dice .pip.r1 { grid-row: 1 } .bg-dice .pip.r2 { grid-row: 2 } .bg-dice .pip.r3 { grid-row: 3 }
.bg-dice .pip.c1 { grid-column: 1 } .bg-dice .pip.c2 { grid-column: 2 } .bg-dice .pip.c3 { grid-column: 3 }
/* a double is worth seeing at a glance: four dice, all the same */
.bg-dice.doubles .die { box-shadow: 0 0 0 2px var(--brass), 0 1px 3px var(--shadow); }
.bg-building { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center;
               padding: .4rem .7rem; background: var(--surface); border: 1px solid var(--brass);
               border-radius: 8px; }
.bg-building b { font-family: var(--display); }
@media (max-width: 34rem) {
  .bg-board .pt { height: 6rem; }
  .bg-board .man { width: .95rem; height: .95rem; }
  .bg-bar, .bg-tray { width: 1.4rem; }
}

/* ---- hangman ------------------------------------------------------------------------- */
/* Two gallows, one word. Yours is large and theirs is smaller beside it: their
   figure is public because the miss count is, and their word is a row of blocks
   saying HOW MANY letters they have found and never which. That asymmetry is the
   whole tension of the game and it has to read at a glance. */

/* a general utility, introduced here because nothing needed it before: text for
   a screen reader that the drawing carries for everybody else */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
           overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

.hm { display: grid; gap: 1rem; justify-items: center; }
.hm-bar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1rem; justify-content: center; }
.hm-category { margin: 0; color: var(--muted); font-size: .95rem; }
.hm-category b { color: var(--ink); }

.hm-pips { list-style: none; display: flex; gap: .35rem; padding: 0; margin: 0; }
.hm-pips .pip { width: .8rem; height: .8rem; border-radius: 50%;
                border: 1px solid var(--line); background: transparent; }
.hm-pips .pip.current { border-color: var(--accent); box-shadow: 0 0 0 2px var(--surface-2); }
.hm-pips .pip.won { background: var(--accent); border-color: var(--accent); }
.hm-pips .pip.lost { background: var(--muted); border-color: var(--muted); }
/* a drawn round is a half, and it looks like one */
.hm-pips .pip.drawn { background: linear-gradient(90deg, var(--accent) 50%, transparent 50%); }

.hm-boards { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem 1.5rem;
             align-items: start; width: min(96vw, 34rem); }
.hm-board { display: grid; justify-items: center; gap: .4rem; }
.hm-who { margin: 0; font: 600 .95rem/1 var(--body); color: var(--muted); }
.hm-board.mine .hm-who { color: var(--ink); }

.hm-figure { width: 100%; max-width: 11rem; height: auto; }
.hm-figure.small { max-width: 7.5rem; opacity: .85; }
.hm-figure .frame { stroke: var(--line); stroke-width: 5; }
.hm-figure .limbs { stroke: var(--ink); stroke-width: 4; }
/* every limb is normalised to pathLength 100, so one dash rule draws all six
   however long each really is */
.hm-figure .limb { stroke-dasharray: 100; stroke-dashoffset: 100; }
.hm-figure .limb.on { stroke-dashoffset: 0; transition: stroke-dashoffset .35s ease-out; }

.hm-lives { margin: 0; font-size: .85rem; color: var(--muted); }
.hm-lives.danger { color: var(--danger); font-weight: 600; }

.hm-slots { list-style: none; display: flex; flex-wrap: wrap; gap: .3rem;
            padding: 0; margin: .2rem 0 0; justify-content: center; }
.hm-slots .slot { width: 1.5rem; height: 1.9rem; display: grid; place-items: center;
                  border-bottom: 2px solid var(--line);
                  font: 600 1.1rem/1 var(--display); color: var(--ink); }
.hm-slots .slot.shown { border-bottom-color: var(--accent); }
.hm-slots .slot.just { animation: hm-flip .3s ease-out; }
@keyframes hm-flip { from { transform: rotateX(-90deg); opacity: 0; } to { transform: none; opacity: 1; } }
/* theirs is a count, so it is blocks and never letters */
.hm-slots.blocks .slot { width: .9rem; height: .9rem; border: 1px solid var(--line);
                         border-radius: 2px; background: transparent; }
.hm-slots.blocks .slot.shown { background: var(--accent); border-color: var(--accent); }

.hm-keyboard { display: grid; gap: .3rem; justify-items: center; width: min(96vw, 32rem); }
.hm-keyrow { display: flex; gap: .3rem; justify-content: center; flex-wrap: wrap; }
.hm-key { min-width: 2.1rem; min-height: 2.6rem; padding: 0 .3rem; border-radius: 6px;
          border: 1px solid var(--line); background: var(--card); color: var(--card-ink);
          font: 600 1rem/1 var(--body); text-transform: uppercase; cursor: pointer; }
.hm-key:hover:not(:disabled) { border-color: var(--accent); }
.hm-key:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; }
.hm-key.hit { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
/* A MISS IS STRUCK THROUGH as well as dimmed: a colour is not a state. */
.hm-key.miss { background: transparent; color: var(--muted); text-decoration: line-through;
               text-decoration-thickness: 2px; opacity: .7; }
.hm-key:disabled { cursor: default; }
.hm-key.hinted { outline: 2px dashed var(--brass); outline-offset: 2px; }
/* you have guessed and they have not: the board says so without a sentence */
.hm.waiting .hm-keyboard { opacity: .55; }

.hm-solve { display: grid; gap: .4rem; justify-items: center; }
.hm-solve-box { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center;
                justify-content: center; max-width: 34rem; }
.hm-solve-box label { flex: 1 0 100%; text-align: center; font-size: .85rem; color: var(--muted); }
.hm-solve-box input { font: 600 1rem/1 var(--body); text-transform: uppercase; letter-spacing: .1em;
                      padding: .45rem .6rem; border-radius: 6px; border: 1px solid var(--line);
                      background: var(--card); color: var(--card-ink); }
.hm .small { font-size: .8rem; max-width: 46ch; margin: 0 auto; text-align: center; }

@media (max-width: 30rem) {
    .hm-boards { grid-template-columns: 1fr; }
    .hm-board.theirs { border-top: 1px solid var(--line); padding-top: .8rem; }
    .hm-key { min-width: 1.9rem; min-height: 2.9rem; }
}
@media (prefers-reduced-motion: reduce) {
    .hm-figure .limb.on { transition: none; }
    .hm-slots .slot.just { animation: none; }
}

/* ---- dominoes ------------------------------------------------------------------ */
/* Real tiles, drawn in CSS from two numbers, so the board renders with
   JavaScript off and the client only has to rebuild the same markup.
   The pips are background gradients rather than elements: a 6-6 is 12 dots and
   a table is thirty tiles, and that is 360 spans nobody needs. */

.dominoes .dom-arms { overflow: auto; padding: .5rem 0; max-height: 28rem; }

/* The main line runs across, and the SPINNER sits in it like any other tile so
   the line stays straight and unbroken.
   
   Its two arms are positioned OUT of the flow, away from it. They were flex
   children of a centred column before, which meant an arm growing on one side
   shoved the spinner off the line: with five tiles below and none above, the
   line ran through the middle of the lower arm instead of through the spinner.
   Out of the flow, the spinner cannot move however long either arm gets. */
.dominoes .dom-line { display: flex; gap: .25rem; align-items: center; justify-content: center;
  white-space: nowrap; min-height: 26rem; }
.dom-spinner { position: relative; display: inline-flex; flex: 0 0 auto; }
.dom-arm { position: absolute; left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; gap: .25rem; align-items: center; }
.dom-arm.up { bottom: calc(100% + .25rem); }
.dom-arm.down { top: calc(100% + .25rem); }
.dom-arm:empty { display: none; }

/* A tile lies LENGTHWAYS along its run: wide on the main line, tall on an arm.
   A double lies CROSSWISE, which is the whole reason a spinner has arms, so it
   is the other way round in both places. */
.dom-tile {
  --pip: currentColor;
  display: inline-flex; flex: 0 0 auto;
  background: var(--card); color: var(--card-ink);
  border: 1px solid var(--card-edge); border-radius: 6px;
  box-shadow: 0 1px 2px var(--shadow);
  overflow: hidden;
}
.dom-half { width: 1.7rem; height: 1.7rem; position: relative; }
.dom-half + .dom-half { border-left: 1px solid var(--card-edge); }

/* On the main line: lengthways along it, doubles across it. */
.dom-line > .dom-tile { flex-direction: row; }
.dom-line > .dom-tile.double { flex-direction: column; }
.dom-line > .dom-tile.double .dom-half + .dom-half { border-left: 0; border-top: 1px solid var(--card-edge); }

/* The spinner is always crosswise: that is what makes it a spinner. It is also
   marked, because it is the only double whose two ends can be played on and a
   player needs to see which one it is. */
.dom-tile.spinner { flex-direction: column; box-shadow: 0 0 0 2px var(--brass), 0 1px 2px var(--shadow); }
.dom-tile.spinner .dom-half + .dom-half { border-left: 0; border-top: 1px solid var(--card-edge); }

/* On an arm: down the arm, doubles across it. */
.dom-arm .dom-tile { flex-direction: column; }
.dom-arm .dom-tile .dom-half + .dom-half { border-left: 0; border-top: 1px solid var(--card-edge); }
.dom-arm .dom-tile.double { flex-direction: row; }
.dom-arm .dom-tile.double .dom-half + .dom-half { border-left: 1px solid var(--card-edge); border-top: 0; }

/* In the hand: held upright. */
.dom-hand .dom-tile { flex-direction: column; }
.dom-hand .dom-tile .dom-half + .dom-half { border-left: 0; border-top: 1px solid var(--card-edge); }

/* The pips. Three columns and three rows at 25/50/75, which is every layout a
   double six set needs. */
.dom-half::before {
  content: ""; position: absolute; inset: 0;
  background-repeat: no-repeat;
  background-size: 100% 100%;
}
.dom-half[data-pips="0"]::before { background-image: none; }
.dom-half[data-pips="1"]::before {
  background-image: radial-gradient(circle at 50% 50%, var(--pip) 1.6px, transparent 1.7px); }
.dom-half[data-pips="2"]::before {
  background-image:
    radial-gradient(circle at 27% 27%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 73% 73%, var(--pip) 1.6px, transparent 1.7px); }
.dom-half[data-pips="3"]::before {
  background-image:
    radial-gradient(circle at 27% 27%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 50% 50%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 73% 73%, var(--pip) 1.6px, transparent 1.7px); }
.dom-half[data-pips="4"]::before {
  background-image:
    radial-gradient(circle at 27% 27%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 73% 27%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 27% 73%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 73% 73%, var(--pip) 1.6px, transparent 1.7px); }
.dom-half[data-pips="5"]::before {
  background-image:
    radial-gradient(circle at 27% 27%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 73% 27%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 50% 50%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 27% 73%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 73% 73%, var(--pip) 1.6px, transparent 1.7px); }
.dom-half[data-pips="6"]::before {
  background-image:
    radial-gradient(circle at 27% 25%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 73% 25%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 27% 50%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 73% 50%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 27% 75%, var(--pip) 1.6px, transparent 1.7px),
    radial-gradient(circle at 73% 75%, var(--pip) 1.6px, transparent 1.7px); }

/* The hand. A tile you cannot play is dimmed rather than hidden: knowing what
   you are holding and cannot use is most of the game. */
.dominoes .dom-hand { display: flex; flex-wrap: wrap; gap: .4rem; margin: 1rem 0; padding: 0; }
.dom-hand button.dom-tile { cursor: default; padding: 0; }
.dom-hand button.dom-tile.playable { cursor: pointer; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
.dom-hand button.dom-tile:disabled { opacity: .5; }
.dominoes .dom-ends { margin: .6rem 0; }
.dominoes .dom-empty { color: var(--muted); }

/* ---- goofspiel ------------------------------------------------------------------ */
/* Real cards, so this reuses the .card markup cribbage uses rather than a
   second card look. Only the layout and the "already spent" state are new. */
.goof { padding: 1rem; }
.goof-prize { text-align: center; margin-bottom: 1rem; }
.goof-prize .card { cursor: default; }
.goof-label { margin: 1.2rem 0 .4rem; font-size: .85rem; }
.goof-row { display: flex; flex-wrap: wrap; gap: .4rem; }
.goof-row.hand { padding-top: 16px; }

/* The prize is a span in a plain block rather than a button in a flex row, and
   a span is inline: width and aspect-ratio do not apply to it, so the card
   collapsed to the size of its text. .card now sets display: inline-block for
   that reason. The row of theirs never showed the fault because a flex
   container blockifies its children. */

/* A rank they have spent is struck through and faded, NOT removed. The gap is
   the information: the whole game is knowing what is left in the other hand,
   and a row that reflows every round is much harder to read at a glance. */
.card.spent { opacity: .3; }
.card.spent .pip { text-decoration: line-through; }
.card.static { cursor: default; }

.goof-history { width: 100%; border-collapse: collapse; margin-top: 1.4rem; font-size: .9rem; }
.goof-history th, .goof-history td { text-align: left; padding: .25rem .5rem; border-bottom: 1px solid var(--line); }
.goof-history caption { text-align: left; padding-bottom: .3rem; }
.goof-history tr.won td { color: var(--accent); font-weight: 600; }
.goof-history tr.tied td { color: var(--muted); font-style: italic; }

/* ---- warship ----------------------------------------------------------------------- */
/* Two grids, the same size, told apart by more than position: yours shows your
   ships, theirs shows only the squares you have fired at. A square you have not
   fired at is water because the state does not mention it, not because the page
   is hiding something. */

.bs { display: grid; gap: 1rem; justify-items: center; }
.bs-grids { display: grid; gap: 1.2rem; grid-template-columns: 1fr; width: min(96vw, 46rem); }
@media (min-width: 46rem) { .bs-grids { grid-template-columns: 1fr 1fr; } }

.bs-side { display: grid; gap: .4rem; justify-items: center; }
.bs-who { margin: 0; font: 600 .95rem/1 var(--body); color: var(--muted); }
.bs-side.mine .bs-who { color: var(--ink); }

.bs-grid { display: grid; gap: 1px; background: var(--line); border: 1px solid var(--line);
           border-radius: 6px; padding: 1px; width: 100%; }
.bs-labels, .bs-row { display: grid; grid-template-columns: 1.3rem repeat(10, 1fr); gap: 1px; }
.bs-labels span, .bs-rownum { display: grid; place-items: center; font: 600 .7rem/1 var(--body);
                              color: var(--muted); background: var(--surface); }

.bs-cell { aspect-ratio: 1; border: 0; padding: 0; background: var(--surface-2); cursor: default;
           position: relative; }
.bs-grid:not(.own) .bs-cell:not(:disabled) { cursor: crosshair; }
.bs-grid:not(.own) .bs-cell:not(:disabled):hover { background: var(--line); }
.bs-cell:focus-visible { outline: 3px solid var(--accent); outline-offset: -2px; z-index: 2; }

/* your own ships, and the shots that have landed on them */
.bs-cell.ship { background: var(--muted); }
.bs-cell.sunk { background: var(--walnut-deep); }

/* A HIT IS A SHAPE AS WELL AS A COLOUR: a cross for a hit, a dot for a miss, so
   the board reads without relying on either. */
.bs-cell.hit::after, .bs-cell.miss::after, .bs-cell.sunk::after {
    content: ''; position: absolute; inset: 22%; }
.bs-cell.hit { background: var(--danger); }
.bs-cell.hit::after, .bs-cell.sunk::after {
    background: linear-gradient(45deg, transparent 43%, var(--accent-ink) 43% 57%, transparent 57%),
                linear-gradient(-45deg, transparent 43%, var(--accent-ink) 43% 57%, transparent 57%); }
.bs-cell.miss::after { border-radius: 50%; background: var(--muted); inset: 36%; }
.bs-cell.hinted { box-shadow: inset 0 0 0 2px var(--brass); }

/* placing */
.bs-cell.preview { background: var(--accent); }
.bs-cell.refused { background: var(--danger); opacity: .55; }

.bs-fleet { list-style: none; display: flex; flex-wrap: wrap; gap: .3rem .6rem; padding: 0; margin: 0;
            font-size: .78rem; color: var(--muted); }
.bs-tally.sunk { text-decoration: line-through; text-decoration-thickness: 2px; opacity: .6; }

.bs-setup { display: grid; gap: .6rem; justify-items: center; width: min(96vw, 40rem); }
.bs-tray { list-style: none; display: flex; flex-wrap: wrap; gap: .4rem; padding: 0; margin: 0;
           justify-content: center; }
.bs-ship-button { display: grid; gap: .2rem; justify-items: center; padding: .35rem .5rem;
                  border: 1px solid var(--line); border-radius: 6px; background: var(--card);
                  color: var(--card-ink); font: 600 .75rem/1 var(--body); cursor: pointer; }
.bs-ship.picked .bs-ship-button { border-color: var(--accent); box-shadow: 0 0 0 2px var(--surface-2); }
.bs-ship.done .bs-ship-button { opacity: .45; text-decoration: line-through; }
.bs-ship-pips { display: flex; gap: 2px; }
.bs-ship-pips i { width: .5rem; height: .5rem; background: var(--muted); border-radius: 1px; }
.bs-setup-actions { display: flex; flex-wrap: wrap; gap: .4rem; justify-content: center; align-items: center; }
.bs .small { font-size: .8rem; max-width: 48ch; margin: 0 auto; text-align: center; }

@media (prefers-reduced-motion: reduce) {
    .bs-cell { transition: none; }
}

svg.game-icon.big {
    max-width: 14em;
}

/* ---- the rules pages (plan_p2p_home_and_seo phase 03) ----------------------------
   Restored: this block went missing when the stylesheet was rewritten, which left
   /how-to-play on browser defaults. The icon's SIZE is deliberately not set here,
   because `svg.game-icon.big` above owns it; this only stops flexbox stretching it. */
.how-to-head { display: flex; flex-wrap: wrap; gap: 1rem 1.4rem; align-items: flex-start;
               margin: 1rem 0 1.6rem; }
.how-to-head > div { flex: 1 1 22ch; }
.how-to-head h2 { margin: 0 0 .3rem; }
.how-to-head .lede { margin: 0; }
.how-to-head .actions { margin: .9rem 0 0; }
.game-icon.big { flex: none; height: auto; align-self: flex-start; }
.rule-list { max-width: 62ch; display: grid; gap: .6rem; padding-inline-start: 1.3rem; }
.rule-list li { padding-inline-start: .2rem; }
.how-to .note { max-width: 62ch; padding: .7rem .9rem; border-inline-start: 3px solid var(--brass);
                background: var(--surface); border-radius: 0 8px 8px 0; color: var(--muted); }
.playing-here { margin-top: 2rem; }
.facts { max-width: 62ch; list-style: none; padding: 0; display: grid; gap: .7rem; }
.facts li { padding-inline-start: 1rem; border-inline-start: 2px solid var(--line); }
.how-to-nav { display: flex; flex-wrap: wrap; gap: .6rem 1.4rem; margin-top: 2.4rem;
              padding-top: 1rem; border-top: 1px solid var(--line); font-size: .95rem; }
.how-to-nav a { text-decoration: none; }

/* the vocabulary block: a term and what it means, two columns until it is tight */
.glossary { margin-top: 2rem; }
.terms { max-width: 62ch; margin: 0; display: grid; grid-template-columns: max-content 1fr;
         gap: .45rem 1rem; }
.terms dt { font-weight: 600; }
.terms dd { margin: 0; color: var(--muted); }
@media (max-width: 34rem) {
  .terms { grid-template-columns: 1fr; gap: .1rem; }
  .terms dd { margin-bottom: .6rem; }
}

/* ---- bulls and cows ------------------------------------------------------------------- */
/* Two histories, both public, and one number each that is not. Knowing what the
   other player has tried and how close they came is the game, so neither list is
   hidden: the only secret on the page is their code, and it is question marks
   until the end. */

.bc { display: grid; gap: 1rem; justify-items: center; }
.bc-columns { display: grid; gap: 1rem 1.5rem; grid-template-columns: 1fr; width: min(96vw, 40rem); }
@media (min-width: 40rem) { .bc-columns { grid-template-columns: 1fr 1fr; } }

.bc-side { display: grid; gap: .4rem; justify-items: center; align-content: start; }
.bc-who { margin: 0; font: 600 .95rem/1 var(--body); color: var(--muted); }
.bc-side.mine .bc-who { color: var(--ink); }

.bc-code { list-style: none; display: flex; gap: .25rem; padding: 0; margin: 0; }
.bc-digit { width: 1.9rem; height: 2.3rem; display: grid; place-items: center;
            border: 1px solid var(--line); border-radius: 5px; background: var(--card);
            color: var(--card-ink); font: 600 1.1rem/1 var(--display); }
.bc-digit.unknown { color: var(--muted); background: var(--surface-2); }
.bc-digit.empty { background: var(--surface-2); }
.bc-code.entry .bc-digit { border-color: var(--accent); }

.bc-rows { list-style: none; display: grid; gap: .2rem; padding: 0; margin: .3rem 0 0;
           width: 100%; }
.bc-row { display: flex; gap: .5rem; align-items: baseline; justify-content: center;
          font-size: .85rem; }
.bc-row.got { font-weight: 600; }
.bc-guess { display: flex; gap: .15rem; }
.bc-guess b { width: 1.2rem; text-align: center; font: 600 .95rem/1.4 var(--display); }
/* THE COUNTS READ WITHOUT COLOUR: they are labelled and numbered, not two
   shades of peg, because two shades of peg is the version nobody can use. */
.bc-pegs { color: var(--muted); }
.bc-pegs .bulls { color: var(--ink); font-weight: 600; }
.bc-row.got .bc-pegs .bulls { color: var(--accent); }

.bc-entry { display: grid; gap: .5rem; justify-items: center; }
.bc-keypad { display: grid; grid-template-columns: repeat(6, 1fr); gap: .3rem;
             width: min(96vw, 24rem); }
.bc-key { min-height: 2.6rem; border: 1px solid var(--line); border-radius: 6px;
          background: var(--card); color: var(--card-ink);
          font: 600 1rem/1 var(--body); cursor: pointer; }
.bc-key:hover:not(:disabled) { border-color: var(--accent); }
.bc-key:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; }
.bc-key.wide { grid-column: span 3; }
.bc-key.primary:not(:disabled) { background: var(--accent); border-color: var(--accent);
                                 color: var(--accent-ink); }
/* a digit already in the entry is struck as well as dimmed */
.bc-key.spent { text-decoration: line-through; opacity: .45; }
.bc-key:disabled { cursor: default; opacity: .55; }

.bc-setup { display: grid; gap: .5rem; justify-items: center; max-width: 34rem; text-align: center; }
.bc-setup-say { margin: 0; font-size: .88rem; color: var(--muted); }
.bc-setup-actions { display: flex; gap: .4rem; flex-wrap: wrap; justify-content: center; }
.bc .small { font-size: .8rem; max-width: 48ch; margin: 0 auto; text-align: center; }

@media (prefers-reduced-motion: reduce) { .bc-digit, .bc-key { transition: none; } }

/* ---- gin rummy (plan_gin_rummy phase 07) ------------------------------------------ */
.gin-table { display: flex; flex-wrap: wrap; gap: 1.2rem 2rem; align-items: center;
             padding: .8rem 1rem; background: var(--surface); border: 1px solid var(--line);
             border-radius: 10px; margin: .6rem 0 1rem; }
.gin-table > div { display: flex; flex-direction: column; align-items: center; gap: .2rem; }
.gin-table b { font: 700 1.3rem/1 var(--display); }
/* NO `.card` RULE HERE. Real cards already exist, at the top of this file:
   66px wide, 5/7, a corner index top and bottom and a pip in the middle, with
   .sm and .back and the hover lift. Cribbage has used them since the
   beginning and goofspiel reuses them, saying so.

   The first draft of this block redefined `.card` with a flat rounded
   rectangle, which not only looked worse but landed LATER in the file and so
   overrode the real one everywhere: cribbage and goofspiel were being damaged
   by a stylesheet block belonging to a game neither of them knows about. */
.card.empty { opacity: .35; }
/* A meld is boxed, because the grouping is the information. */
.gin-hand { display: flex; flex-wrap: wrap; gap: .5rem .9rem; align-items: center; margin: .4rem 0; }
.gin-hand { padding-top: 16px; }                 /* room for the hover lift */
.gin-meld { display: flex; gap: .35rem; padding: .4rem; border: 2px solid var(--brass);
            border-radius: 10px; background: var(--surface-2); }
.gin-loose { display: flex; flex-wrap: wrap; gap: .35rem; }
/* Loose cards are the ones costing you points, so they are marked rather than
   left to be counted: a dashed edge and the value in the corner. */
.card.loose { box-shadow: inset 0 0 0 1px var(--line), 0 1px 2px var(--shadow); }
.gin-table .card { cursor: default; }
.gin-count { margin: .6rem 0; }
.gin-count .good { color: var(--accent); font-weight: 600; }
@media (max-width: 34rem) {
  .gin-hand .card, .gin-table .card { --w: 48px; }
  .gin-table { gap: .6rem 1.2rem; }
}

/* ---- dots and boxes ---------------------------------------------------------------------- */
/* Eleven by eleven tracks for five by five boxes: a thin track for every row and column of
   dots and lines, a wide one for every row and column of boxes. The board is square, so the
   wide tracks are square too. Sized the way the connect four board is: the page's width up
   to a limit, never max-content (which makes the grid column that wide and a phone scrolls). */
.dab-board { --dab-dot: 10px; --dab-reach: 14px; container-type: inline-size; overflow: hidden; display: grid;
             grid-template-columns: repeat(5, var(--dab-dot) 1fr) var(--dab-dot);
             grid-template-rows: repeat(5, var(--dab-dot) 1fr) var(--dab-dot);
             aspect-ratio: 1; width: min(100%, 520px); box-sizing: border-box; padding: 4px;
             margin: 0 0 .6rem; background: var(--surface); border-radius: 10px;
             box-shadow: 0 3px 10px var(--shadow); }
@media (orientation: landscape) and (max-height: 520px) { .dab-board { width: min(100%, 520px, calc(100vh - 6rem)); } }
.dab-dot { display: block; background: var(--ink); border-radius: 50%; transform: scale(1.3); z-index: 2; }
/* A line is a button the size of its thin track, DRAWN by ::after and TAPPED through ::before,
   which reaches --dab-reach into the boxes on either side so a thumb has something to hit.
   Two reaches meet along every box diagonal, and each is clipped to a hexagon whose taper
   runs from the BOX CORNER to the box centre, so the two triangles meet exactly on the
   diagonal and the nearer side wins. (It first tapered to the line's own midline, five
   pixels outside the box, and t/77 found a wedge near every corner that neither covered.)

        h:   ________________          v:   /\
            /                \              |  |
            \________________/              |  |
                                            \/

   The boxes take no pointer events, so a tap inside one reaches whichever reach covers it.
   A disabled (drawn) line has no reach at all, so it never swallows a tap meant for a
   neighbour. */
.dab-line { position: relative; display: block; border: 0; padding: 0; margin: 0; background: transparent;
            cursor: pointer; min-width: 0; min-height: 0; z-index: 1; }
.dab-line:disabled { cursor: default; }
.dab-line::before { content: ''; position: absolute; z-index: 3; }
.dab-line.h::before { inset: calc(-1 * var(--dab-reach)) 0;
  clip-path: polygon(0 var(--dab-reach), 50% 0, 100% var(--dab-reach),
                     100% calc(100% - var(--dab-reach)), 50% 100%, 0 calc(100% - var(--dab-reach))); }
.dab-line.v::before { inset: 0 calc(-1 * var(--dab-reach));
  clip-path: polygon(var(--dab-reach) 0, 0 50%, var(--dab-reach) 100%,
                     calc(100% - var(--dab-reach)) 100%, 100% 50%, calc(100% - var(--dab-reach)) 0); }
.dab-line:disabled::before { display: none; }
.dab-line::after { content: ''; position: absolute; border-radius: 3px; background: transparent;
                   transition: background .25s ease; }
.dab-line.h::after { inset: 30% 0; border-top: 2px dotted var(--line); }
.dab-line.v::after { inset: 0 30%; border-left: 2px dotted var(--line); }
.dab-line.drawn::after { background: var(--ink); border: 0; }
.dab-line.h.drawn::after { inset: 20% 0; }
.dab-line.v.drawn::after { inset: 0 20%; }
.dab-line.last::after { background: var(--brass); }
.dab-line:not(:disabled):hover::after, .dab-line:focus-visible::after { background: var(--brass); border-color: transparent; }
.dab-line:focus-visible { outline: 0; }
.dab-line.hinted::after { background: var(--brass); border-color: transparent; box-shadow: 0 0 0 3px var(--card); }
/* A claimed box is its owner's colour AND its owner's seat number, because colour alone is not
   a distinction everybody can see; the number is the one initial that goes in a box here. */
.dab-box { position: relative; display: flex; align-items: center; justify-content: center;
           pointer-events: none; border-radius: 4px; transition: background .3s ease; }
.dab-box.red { background: color-mix(in srgb, var(--peg-red) 70%, transparent); }
.dab-box.blue { background: color-mix(in srgb, var(--peg-blue) 70%, transparent); }
.dab-box.last { box-shadow: inset 0 0 0 2px var(--brass); }
.dab-box i { font-style: normal; font-family: var(--display); font-size: clamp(.7rem, 3vw, 1.1rem);
             color: var(--card); opacity: .75; }
.dab-counts { margin: 0 0 .4rem; }
@media (prefers-reduced-motion: reduce) { .dab-line::after, .dab-box { transition: none !important; } }
/* THE REACH IS HALF A BOX where the browser can measure the board (container query
   units; every browser this site targets has had them since 2023), and 14px where it
   cannot. At half a box the two hexagons above and below a horizontal line, and left and
   right of a vertical one, are the four triangles of each box meeting at its centre, so a
   tap ANYWHERE in a box draws the side nearest to it. That is the whole of the phone
   case, and t/77-dab-gate.t measures it in a headless browser at phone width. The board
   clips its overflow so the outer lines' reach stops at the board's edge rather than
   catching taps on the status line above it. */
@supports (width: 1cqw) { .dab-board { --dab-reach: calc((100cqw - 6 * var(--dab-dot)) / 10); } }

/* ---- schnapsen and sixty-six ------------------------------------------------------
   NO `.card` RULE HERE, and the omission is the point. Real cards are defined at the
   top of this file and cribbage, goofspiel and gin rummy all use them. A block down
   here that redefined `.card` would land LATER in the cascade and override the real one
   everywhere, which is how a game-specific style once damaged two games it knew nothing
   about. Everything below is either a `.mar-` layout class of its own or a modifier ON
   a real card. */

.mar { display: grid; gap: .9rem; justify-items: center; }

.mar-table { display: flex; flex-wrap: wrap; gap: 1.2rem; align-items: flex-start;
             justify-content: center; }
.mar-table > div { display: grid; gap: .25rem; justify-items: center; min-width: 4.5rem; }
.mar-table .quiet { font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; }
.mar-table b { font: 600 1.35rem/1 var(--display); }

.mar-suit { font-size: 1.5rem; line-height: 1; }
.mar-closed { font-size: .72rem; padding: .05rem .35rem; border-radius: 999px;
              background: var(--brass); color: var(--accent-ink); }

.mar-count { margin: 0; text-align: center; }
.mar-count b { font: 600 1.15rem/1 var(--display); }

.mar-hand { display: flex; flex-wrap: wrap; gap: .3rem; justify-content: center;
            padding-top: 16px; }

.mar-note { margin: 0; font-size: .85rem; color: var(--muted); text-align: center; }

.mar-declare { display: flex; flex-wrap: wrap; gap: .4rem; justify-content: center; }

/* The claim is separated from every other control on purpose. It ends the deal either
   way, and a claim that is wrong hands it to the opponent, so it is never somewhere a
   stray click lands. */
.mar-claim { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center;
             justify-content: center; padding: .5rem .7rem; border-radius: 8px;
             border: 1px dashed var(--line); }
.mar-claim .quiet { font-size: .8rem; max-width: 34ch; }
.mar-claim[hidden] { display: none; }

.mar-reveal { text-align: center; font-size: .9rem; color: var(--muted); }

/* A card the rules do not allow this turn. The strict follow-suit cascade refuses most
   of a hand in phase two, and a player who cannot see why wants the page, not a
   refusal. */
.mar-hand .card:disabled { opacity: .38; }

@media (max-width: 30rem) {
  .mar-table { gap: .7rem; }
  .mar-hand .card { --w: 52px; }
}

/* ---- pool and snooker -----------------------------------------------------------------
   The server draws the table as SVG (P2PGames::Game::Billiards::Page) and the client
   replaces it with a stack of three canvases sized by billiards.js: the table paints once,
   the balls every frame, the cue on top. Cloth, cushion and ball colours are attributes in
   the markup and the sprites; the rails, the pockets and the markings take the site's
   tokens. `.scripted` is set by the client the moment it has a table to draw, so a page
   with scripting off keeps the SVG and the ball list and never shows a dead control. */
.billiards { margin: 0 0 .8rem; }
.billiards .table-svg { display: block; width: min(100%, 720px); height: auto; margin: 0 auto .6rem;
                        border-radius: 10px; box-shadow: 0 3px 10px var(--shadow); }
.billiards .table-svg .rails { fill: var(--walnut); }
.billiards .table-svg .pocket { fill: var(--hole); }
.billiards .table-svg .mark { stroke: rgba(245,240,225,.5); stroke-width: 2; fill: none; }
.billiards .table-svg .spot { fill: rgba(245,240,225,.7); }
.billiards .table-svg .ball text { font-family: var(--body); font-weight: 700; fill: #17161a; }
.billiards .table-stack { display: none; position: relative; width: min(100%, 720px); aspect-ratio: 2 / 1;
                          margin: 0 auto .6rem; touch-action: none; border-radius: 10px; overflow: hidden;
                          background: var(--walnut); box-shadow: 0 10px 28px var(--shadow);
                          user-select: none; -webkit-user-select: none; }
.billiards .table-stack.portrait { width: min(100%, 520px); aspect-ratio: 1 / 2; }
.billiards .table-stack:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.billiards .layer { position: absolute; inset: 0; display: block; }
.billiards .layer-top { cursor: crosshair; }
.billiards.scripted .table-stack { display: block; }
.billiards.scripted .table-svg, .billiards.scripted .ball-list { display: none; }
.billiards .table-summary { margin: 0 0 .4rem; }
/* the landscape twin the other boards have: a phone on its side keeps the whole table in view */
@media (orientation: landscape) and (max-height: 520px) {
    .billiards .table-stack, .billiards .table-svg { width: min(100%, 720px, calc((100vh - 6rem) * 2)); }
}
.billiards .needs-script { display: none; }
.billiards.scripted .needs-script { display: inline-flex; }
.billiards.scripted .needs-script[hidden] { display: none; }
.billiards .options { display: flex; flex-wrap: wrap; gap: .4rem; }
.billiards .options:empty { display: none; }
.billiards .fine { flex: 1 1 160px; min-width: 120px; height: 38px; border: 1px solid var(--line); border-radius: 8px;
                   background: var(--surface); align-items: center; justify-content: center; color: var(--muted);
                   font-size: .85rem; cursor: ew-resize; touch-action: none; user-select: none; -webkit-user-select: none; }
.billiards .fine:focus-visible, .billiards .spin:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.billiards .power { align-items: center; gap: 6px; font-size: .9rem; }
.billiards .power input { width: 150px; accent-color: var(--accent); }
.billiards .spin { width: 56px; height: 56px; border-radius: 50%; cursor: pointer; touch-action: none; }
.billiards .guide { align-items: center; gap: 6px; font-size: .9rem; }
.billiards .guide input { accent-color: var(--accent); }
.billiards .billiards-controls[data-busy="1"] .needs-script { opacity: .55; }
/* a portrait phone: the table turned by the client, the page scrolls down and never sideways,
   the fine strip gone (the cloth itself is the fine control at that width), the row stuck to
   the bottom so the shoot button is under the thumb */
@media (max-width: 640px) and (orientation: portrait) {
    .billiards .fine { display: none !important; }
    .billiards .billiards-controls { position: sticky; bottom: 0; background: var(--ground); border-top: 1px solid var(--line);
                                     padding: 6px 0; z-index: 2; gap: 8px 12px; }
    .billiards .power input { width: 120px; }
    .billiards .spin { width: 48px; height: 48px; }
}

/* ---- minigolf ------------------------------------------------------------------ */
/* Billiards' shape: the server's SVG until the client sets .scripted, then the
   canvas stack; the scorecard is HTML and stays. The kerb, surface, blocks and
   cup are locals the client's painter reads off the stack, so both pictures take
   the same colours in both themes. */
.minigolf { margin: 0 0 .8rem; --kerb: #6b4a2e; --surface-green: #3f8f4a; --block: #8a5a3a; --hole: #241509; --gravel: #cbc4b0; }
:root:not([data-theme="light"]) .minigolf { --kerb: #4d3421; --surface-green: #2f6f3a; --gravel: #2b2622; }
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .minigolf { --kerb: #6b4a2e; --surface-green: #3f8f4a; --gravel: #cbc4b0; } }
:root[data-theme="dark"] .minigolf { --kerb: #4d3421; --surface-green: #2f6f3a; --gravel: #2b2622; }
.minigolf .hole-line { margin: 0 0 .4rem; font-weight: 600; }
.minigolf .course-svg { display: block; width: min(100%, 720px); height: auto; margin: 0 auto .6rem;
                        border-radius: 10px; box-shadow: 0 3px 10px var(--shadow); background: var(--gravel); }
.minigolf .course-svg .kerb { fill: none; stroke: var(--kerb); stroke-width: 180; stroke-linejoin: round; }
.minigolf .course-svg .surface { fill: var(--surface-green); }
.minigolf .course-svg .block { fill: var(--block); stroke: rgba(0,0,0,.35); stroke-width: 3; }
.minigolf .course-svg .tee { fill: rgba(0,0,0,.18); }
.minigolf .course-svg .cup { fill: var(--hole); }
.minigolf .course-svg .flag-pole { stroke: #f4f0e6; stroke-width: 6; }
.minigolf .course-svg .flag { fill: #c23b2c; }
.minigolf .course-svg .ball text { font-family: var(--body); font-weight: 700; }
.minigolf .course-stack { display: none; position: relative; width: min(100%, 720px); margin: 0 auto .6rem;
                          touch-action: none; border-radius: 10px; overflow: hidden; background: var(--gravel);
                          box-shadow: 0 10px 28px var(--shadow); user-select: none; -webkit-user-select: none; }
.minigolf .course-stack.portrait { width: min(100%, 520px); }
.minigolf .course-stack:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.minigolf .layer { position: absolute; inset: 0; display: block; }
.minigolf .layer-top { cursor: crosshair; }
.minigolf.scripted .course-stack { display: block; }
.minigolf.scripted .course-svg { display: none; }
.minigolf .course-summary { margin: 0 0 .4rem; }
.minigolf .needs-script { display: none; }
.minigolf.scripted .needs-script { display: inline-flex; }
.minigolf.scripted .needs-script[hidden] { display: none; }
.minigolf .fine { flex: 1 1 160px; min-width: 120px; height: 38px; border: 1px solid var(--line); border-radius: 8px;
                  background: var(--surface); align-items: center; justify-content: center; color: var(--muted);
                  font-size: .85rem; cursor: ew-resize; touch-action: none; user-select: none; -webkit-user-select: none; }
.minigolf .fine:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.minigolf .power { align-items: center; gap: 6px; font-size: .9rem; }
.minigolf .power input { width: 150px; accent-color: var(--accent); }
.minigolf .guide { align-items: center; gap: 6px; font-size: .9rem; }
.minigolf .guide input { accent-color: var(--accent); }
.minigolf .minigolf-controls[data-busy="1"] .needs-script { opacity: .55; }
.minigolf .scorecard-wrap { overflow-x: auto; margin-top: .8rem; }
.minigolf .scorecard { border-collapse: collapse; font-size: .9rem; min-width: 100%; }
.minigolf .scorecard caption { text-align: left; padding-bottom: .3rem; }
.minigolf .scorecard th, .minigolf .scorecard td { text-align: center; padding: .25rem .4rem; border-bottom: 1px solid var(--line); white-space: nowrap; }
.minigolf .scorecard th[scope="row"] { text-align: left; }
.minigolf .scorecard th.current { color: var(--accent); text-decoration: underline; }
.minigolf .scorecard tr.on-turn th[scope="row"] { color: var(--accent); }
.minigolf .scorecard tr.par td, .minigolf .scorecard tr.par th { color: var(--muted); font-size: .8rem; }
.minigolf .scorecard td.open { color: var(--muted); font-style: italic; }
.minigolf .scorecard td[data-total] { font-weight: 600; }
@media (max-width: 640px) and (orientation: portrait) {
    .minigolf .fine { display: none !important; }
    .minigolf .minigolf-controls { position: sticky; bottom: 0; background: var(--ground); border-top: 1px solid var(--line);
                                   padding: 6px 0; z-index: 2; gap: 8px 12px; }
    .minigolf .power input { width: 120px; }
}

/* ---- hearts ------------------------------------------------------------------ */
/* The other seats around, the trick in the middle, your hand below. The cards
   are the site's .card rules above; nothing here restyles a card. */
.hearts { padding: 1rem; }
.hearts-seats { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: .5rem; margin-bottom: .8rem; }
.hearts-seat { padding: .4rem .6rem; border: 1px solid var(--line); border-radius: 8px; font-size: .9rem; }
.hearts-seat.on-turn { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent) inset; }
.hearts-seat.waiting .hearts-name::after { content: ' \2026'; color: var(--muted); }
.hearts-trick { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .5rem; justify-items: center;
                min-height: calc(46px * 7 / 5 + 2.2rem); padding: .6rem; border-radius: 10px;
                background: linear-gradient(170deg, #6a4529, var(--walnut) 40%, var(--walnut-deep)); }
.hearts-slot { display: flex; flex-direction: column; align-items: center; gap: .25rem; min-width: 46px; }
.hearts-slot-name { font-size: .75rem; color: var(--card); }
.hearts-slot.led .hearts-slot-name { text-decoration: underline; }
.hearts-mine { margin: .8rem 0 .3rem; font-size: .85rem; }
.hearts-hand { display: flex; flex-wrap: wrap; gap: .4rem; padding-top: 16px; }
.hearts .controls { margin-top: .8rem; display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; }
.hearts-history-wrap { overflow-x: auto; margin-top: 1.4rem; }
.hearts-history { border-collapse: collapse; font-size: .9rem; min-width: 100%; }
.hearts-history th, .hearts-history td { text-align: right; padding: .25rem .5rem; border-bottom: 1px solid var(--line); white-space: nowrap; }
.hearts-history th:first-child, .hearts-history td:first-child { text-align: left; }
.hearts-history caption { text-align: left; padding-bottom: .3rem; }
.hearts-history td.moon { color: var(--accent); font-weight: 600; }
.hearts-history tfoot th { border-top: 2px solid var(--line); }
@media (max-width: 34rem) {
  .hearts { padding: .5rem; }
  .hearts-hand .card { --w: 44px; }
  .hearts-seats { gap: .3rem; }
  .hearts-seat { padding: .3rem .4rem; font-size: .8rem; }
  .hearts-trick { gap: .3rem; padding: .4rem; }
}

/* ---- typing race --------------------------------------------------------------------- */
/* The passage at the top, the box directly under it, the bars under the box:
   on a phone with the keyboard up, the current word and your own bar are in
   the half of the screen that is left, and the page never scrolls while
   racing. Colours are the site's tokens: typed words fade to muted, the
   current word is underlined in the accent, a miss flashes peg-red. */
.tr { display: grid; gap: .75rem; width: min(100%, 44rem); margin: 0 auto; }
.tr-passage { margin: 0; padding: .9rem 1rem; border-radius: 10px; background: var(--surface);
              font: 1.15rem/1.7 var(--display); color: var(--ink); overflow-wrap: anywhere; }
.tr-word { border-radius: 3px; scroll-margin: 2rem; transition: color .15s ease-out; }
.tr-word.typed { color: var(--muted); }
.tr-word.current { text-decoration: underline; text-decoration-color: var(--accent);
                   text-decoration-thickness: 2px; text-underline-offset: .15em; }
.tr-word.pending { background: var(--surface-2); }
.tr-word.wrong { color: var(--danger); }
.tr-word.miss { animation: tr-miss .3s ease-out; }
@keyframes tr-miss { from { background: var(--peg-red); color: var(--accent-ink); } to { background: transparent; } }
.tr-credit { margin: 0; font-size: .85rem; text-align: right; }

.tr-play { display: grid; gap: .5rem; grid-template-columns: 1fr; align-items: center; scroll-margin: 1rem; }
.tr-count { font: 600 1.4rem/1 var(--display); color: var(--accent); text-align: center; min-height: 1.4rem; }
.tr-count:empty { display: none; }
.tr-input { font: 1.2rem/1.4 var(--body); padding: .55rem .8rem; border-radius: 8px;
            border: 1px solid var(--line); background: var(--card); color: var(--card-ink); width: 100%; }
.tr-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.tr-input:disabled { background: var(--surface-2); color: var(--muted); }

.tr-bars { list-style: none; margin: 0; padding: 0; display: grid; gap: .4rem; }
.tr-bar { display: grid; grid-template-columns: minmax(6rem, 10rem) 1fr 4.5rem; gap: .5rem; align-items: center; }
.tr-bar.you .tr-name { font-weight: 600; }
.tr-who { display: flex; align-items: center; gap: .35rem; min-width: 0; }
.tr-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tr-track { display: block; height: .8rem; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.tr-fill { display: block; height: 100%; width: 0; border-radius: 999px; background: var(--brass);
           transition: width .25s ease-out; }
.tr-fill.seat-2 { background: var(--peg-blue); }
.tr-fill.seat-3 { background: var(--peg-green); }
.tr-fill.seat-4 { background: var(--peg-amber); }
.tr-bar[data-status="finished"] .tr-track { outline: 2px solid var(--accent); outline-offset: -2px; }
.tr-bar[data-status="dnf"] .tr-fill, .tr-bar[data-status="dq"] .tr-fill { background: var(--muted); }
.tr-wpm { font-size: .85rem; color: var(--muted); text-align: right; white-space: nowrap; }

.tr-result { width: 100%; border-collapse: collapse; margin-top: .5rem; }
.tr-result th, .tr-result td { text-align: left; padding: .35rem .5rem; border-bottom: 1px solid var(--line); }
.tr-result th { font-size: .75rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.tr-result tr.you td { font-weight: 600; }

@media (max-width: 30rem) {
  .tr-passage { font-size: 1.05rem; line-height: 1.6; padding: .7rem .8rem; }
  .tr-bar { grid-template-columns: minmax(4.5rem, 6rem) 1fr 3.5rem; }
}

/* ---- pong ---- */
/* The court is a canvas the client sizes; the wrapper carries the aspect ratio the
   client sets from the orientation (16:10 landscape, 10:16 portrait) so the page does
   not jump when the canvas first paints. touch-action: none, or the browser scrolls the
   page instead of moving the paddle. The note sits over the court for the sentences
   that need to be where the eye is (answer the sheet, reconnecting, resumed). */
.pong { margin: 0 0 .8rem; }
.pong-court-wrap { position: relative; width: min(100%, 720px); margin: 0 auto .6rem; aspect-ratio: 16 / 10;
                   background: #17161a; border-radius: 10px; overflow: hidden; }
.pong-court { display: block; width: 100%; height: 100%; touch-action: none; cursor: crosshair; user-select: none; }
.pong-note { position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%); margin: 0; padding: .6rem 1rem;
             text-align: center; color: #f4f0e6; background: rgba(23,22,26,.85); font-weight: 600; }
.pong-cannot { padding: .6rem .8rem; border-inline-start: 3px solid var(--danger); background: var(--surface-2); }
.pong-reading { margin: 0 0 .6rem; }
.pong-points { margin: 0; padding-inline-start: 1.2rem; font-size: .9rem; }
@media (orientation: portrait) {
  /* The whole court has to be on screen without scrolling, because dragging
     on it can never scroll the page: its width is derived from the viewport's
     height less the status line, and pong.js scrolls the court to the top of
     the viewport once at start when the page head would push it under the
     fold (the canvas draws the score itself). */
  .pong-court-wrap { width: min(100%, 480px, calc((100vh - 6rem) * 10 / 16)); aspect-ratio: 10 / 16; }
}


/* The ready lobby, retrofitted 25 September 2026. Lightlines' block word for
   word, because it is the same panel doing the same job, and a second set of
   sizes would be a second thing to keep in step. No colour of its own: the
   button is the shared .button.primary and this only places and sizes it,
   which is what keeps this block tokens-only. */
.pong-ready {
  display: flex;
  justify-content: center;
  margin: .8rem auto 0;
  max-width: 34rem;
}
.pong-ready-button {
  min-width: 12rem;
  padding: .7rem 1.4rem;
  font-size: 1.05rem;
}
.pong-ready-button[disabled] { opacity: .55; }
/* ---- blockfall ----------------------------------------------------------------- */
/* Two stacks. Yours is the hero, capped by the viewport's height so twenty square
   rows, the head, the status line and the key row fit a phone in portrait with
   nothing to scroll; the side column holds the hold, the queue and the other
   stack at a third of the scale, and grows it to half beside yours on a desk.
   Cells are aspect-ratio squares with a percentage margin, never a row gap
   (site.css's Four Connect note). Colours are the site's tokens by letter and
   never the Guideline's; the landing mark is a line under the piece's columns,
   not a ghost piece; garbage is hatched as well as coloured. */
.bf { display: grid; gap: .8rem; justify-items: center; }
.bf-game { display: grid; grid-template-columns: minmax(0, 1fr) 5.5rem; gap: .8rem; align-items: start;
           width: min(100%, 440px); }
.bf-main { display: grid; gap: .3rem; min-width: 0; }
.bf-side { display: grid; gap: .6rem; min-width: 0; }
.bf-board { display: grid; grid-template-columns: repeat(10, 1fr); gap: 0; padding: 1.5%;
            background: var(--walnut); border-radius: 8px; box-sizing: border-box; min-width: 0;
            box-shadow: inset 0 2px 8px rgba(0,0,0,.35), 0 3px 10px var(--shadow); touch-action: none;
            user-select: none; -webkit-user-select: none; }
.bf-board.mine { width: min(100%, 320px, calc((100dvh - 15rem) / 2)); }
.bf-board.theirs { width: 100%; }
.bf-board:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.bf-cell { display: block; aspect-ratio: 1; margin: 3%; border-radius: 2px; background: var(--walnut-deep);
           box-shadow: inset 0 1px 2px rgba(0,0,0,.4); position: relative; }
.bf-cell.I { background: var(--brass); }
.bf-cell.O { background: var(--card); }
.bf-cell.T { background: var(--peg-blue); }
.bf-cell.S { background: var(--peg-green); }
.bf-cell.Z { background: var(--peg-red); }
.bf-cell.J { background: var(--peg-amber); }
.bf-cell.L { background: var(--accent); }
.bf-cell.G { background: var(--surface-2);
             background-image: repeating-linear-gradient(45deg, transparent 0 3px, rgba(30,20,10,.35) 3px 5px); }
.bf-cell.falling { box-shadow: inset 0 0 0 1px rgba(255,255,255,.35), inset 0 1px 2px rgba(0,0,0,.4); }
.bf-cell.landing::after { content: ''; position: absolute; left: 10%; right: 10%; bottom: 0; height: 2px;
                          background: var(--brass); }
.bf-cell.hinted { outline: 2px dashed var(--brass); outline-offset: -2px; }
.bf-slot { display: grid; gap: .25rem; justify-items: center; }
.bf-label { font: 600 .75rem/1 var(--body); color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.bf-mini { display: grid; grid-template-columns: repeat(4, 1fr); width: 100%; max-width: 4.4rem; padding: 4%;
           background: var(--surface-2); border: 1px solid var(--line); border-radius: 6px; box-sizing: border-box; }
.bf-mcell { display: block; aspect-ratio: 1; margin: 4%; border-radius: 2px; }
.bf-mcell.I { background: var(--brass); }
.bf-mcell.O { background: var(--card); }
.bf-mcell.T { background: var(--peg-blue); }
.bf-mcell.S { background: var(--peg-green); }
.bf-mcell.Z { background: var(--peg-red); }
.bf-mcell.J { background: var(--peg-amber); }
.bf-mcell.L { background: var(--accent); }
.bf-incoming { display: flex; align-items: flex-end; gap: .4rem; min-height: 1.1rem; font-size: .75rem; color: var(--danger); }
/* The bar's height is a data attribute, not an inline style: the site's CSP is
   default-src 'self' with no style-src, so an inline style is refused. One rule
   a line up to twelve, and everything above reads as twelve. */
.bf-bar { display: block; width: .5rem; height: 0; background: var(--danger); border-radius: 2px;
          transition: height .15s ease; }
.bf-bar[data-lines="1"] { height: .35rem; }  .bf-bar[data-lines="2"] { height: .7rem; }
.bf-bar[data-lines="3"] { height: 1.05rem; } .bf-bar[data-lines="4"] { height: 1.4rem; }
.bf-bar[data-lines="5"] { height: 1.75rem; } .bf-bar[data-lines="6"] { height: 2.1rem; }
.bf-bar[data-lines="7"] { height: 2.45rem; } .bf-bar[data-lines="8"] { height: 2.8rem; }
.bf-bar[data-lines="9"] { height: 3.15rem; } .bf-bar[data-lines="10"] { height: 3.5rem; }
.bf-bar[data-lines="11"] { height: 3.85rem; } .bf-bar[data-lines="12"] { height: 4.2rem; }
.bf-count { margin: 0; }
.bf-keys { display: flex; flex-wrap: wrap; gap: .4rem; justify-content: center; }
.bf-key { min-width: 2.9rem; min-height: 2.9rem; padding: 0 .4rem; border-radius: 8px; border: 1px solid var(--line);
          background: var(--card); color: var(--card-ink); font: 700 1.2rem/1 var(--body); cursor: pointer;
          touch-action: manipulation; }
.bf-key:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; }
.bf-key:disabled { opacity: .45; cursor: default; }
.bf-noscript { padding: .6rem .8rem; border-left: 3px solid var(--danger); background: var(--surface-2); }
/* On a desk there is room for both stacks at one size: the other player's
   stack takes the third column at the same width as yours, with the hold and
   the queue between them (the owner's note, 17 Sep 2026: the phone was right
   and the desk was wasting its space). */
@media (min-width: 46rem) {
    .bf-game { grid-template-columns: minmax(0, 1fr) 5.5rem minmax(0, 1fr); grid-auto-rows: auto;
               width: min(100%, 760px); align-items: start; }
    /* the side column dissolves into the game grid, so its three slots can be
       placed by column: the hold and the queue in the middle, the other stack
       in the third column at the same size as yours */
    .bf-side { display: contents; }
    .bf-main { grid-column: 1; grid-row: 1 / span 3; }
    .bf-slot { grid-column: 2; }
    .bf-theirs-slot { grid-column: 3; grid-row: 1 / span 3; justify-items: stretch; }
    .bf-theirs-slot .bf-label { justify-self: center; }
    .bf-board.mine, .bf-board.theirs { width: min(100%, 320px); }
    .bf-board.theirs { justify-self: center; }
}
@media (orientation: landscape) and (max-height: 520px) {
    .bf-board.mine { width: min(100%, calc((100dvh - 7rem) / 2)); }
}
@media (prefers-reduced-motion: reduce) { .bf-bar { transition: none; } }

/* ---- Yacht -------------------------------------------------------------
   A SCORECARD, NOT A CASINO TABLE. docs/product.md bans the appearance and
   plan_p2p_new_games/shortlist.md ruled out Liar's Dice on that ground, so
   there is no baize here and nothing is green: the card is ruled like every
   other table on the site and the dice sit on the page's own surface.

   The tightest table on the site: twelve rows by five columns at four seats
   on a 375px phone. The category column is what gives, and it gives by
   abbreviating in CSS rather than by shortening the label, so the full text
   stays in the DOM for a screen reader and for the rules page. */
.yacht { display: grid; gap: .8rem; justify-items: center; }

.yacht-dice { display: flex; flex-wrap: wrap; gap: .5rem; justify-content: center; }
/* NAMESPACED, and it has to be. `.die` and `.die i` are LUDO's, forty lines
   up: `.die i` paints every pip dark, which lit the blanks as well as the
   spots, and `.die` is a three-column grid, which squeezed this whole face
   into one third of the button. Both applied here silently because the names
   were generic. Every class this game draws with starts `yacht-`. */
.yacht-die { width: 3rem; height: 3rem; padding: .35rem; border-radius: 10px; cursor: pointer;
       box-sizing: border-box; display: inline-block; vertical-align: middle;
       border: 1px solid var(--card-edge); background: var(--card); color: var(--card-ink);
       touch-action: manipulation; }
.yacht-die:disabled { opacity: .5; cursor: default; }
.yacht-die:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
/* A held die is marked by a ring AND by lifting it off the row, never by
   colour alone: site.css line 23 is the rule and a hold is the one thing a
   player must be able to read at a glance. */
.yacht-die.held { border-color: var(--brass); box-shadow: 0 0 0 2px var(--brass); transform: translateY(-.2rem); }
.yacht-die .yacht-pips { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr);
             width: 100%; height: 100%; }
.yacht-die .yacht-pips i { display: block; border-radius: 50%; margin: auto; width: .55rem; height: .55rem; }
/* --card-ink and NOT --pip. `--pip` looks like a site token and is not one:
   it is declared on `.dom-tile` alone, forty lines up, so a pip out here
   resolved to nothing at all and every die came out blank. --card-ink is
   declared on :root and is what Ludo's dice use. */
.yacht-die .yacht-pips i.on { background: var(--card-ink); }

.yacht-roll { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; justify-content: center; }
.yacht-help { margin: 0; text-align: center; }
.yacht-note { margin: 0; text-align: center; }

.yacht-card { border-collapse: collapse; width: min(100%, 34rem); font-variant-numeric: tabular-nums; }
.yacht-card caption { caption-side: top; padding-bottom: .4rem; }
.yacht-card th, .yacht-card td { border: 1px solid var(--line); padding: .3rem .45rem; text-align: right; }
.yacht-card thead th { text-align: center; font-weight: 600; white-space: nowrap; }
.yacht-card tbody th[scope="row"], .yacht-card tfoot th { text-align: left; font-weight: 500; }
.yacht-card tbody tr.lower { border-top: 1px solid var(--line); }
.yacht-card .mine { background: var(--surface-2); }
.yacht-card .taken { font-weight: 700; }
/* What a free row WOULD pay, in the caller's own column: lighter than a score
   so the card never reads as though it has been filled in already. */
.yacht-card .would { color: var(--muted); font-style: italic; }
.yacht-card tr.free th[scope="row"] { font-weight: 700; }
.yacht-card .take { display: block; width: 100%; text-align: left; cursor: pointer;
                    min-height: 2.2rem; touch-action: manipulation; }
.yacht-card .take:disabled { cursor: default; }
.yacht-card .total { font-weight: 700; border-top: 2px solid var(--line); }
.yacht-card .total.leader { color: var(--brass); }
.yacht-card thead .swatch { vertical-align: -.1rem; }

/* A phone at four seats. The label column abbreviates by clipping rather than
   by changing the text, so nothing the assistive tree reads is lost, and the
   tap target stays the full height of the row. */
@media (max-width: 26rem) {
    .yacht-card { font-size: .85rem; }
    .yacht-card th, .yacht-card td { padding: .25rem .3rem; }
    .yacht-card tbody th[scope="row"] { max-width: 7.5rem; }
    .yacht-card .take { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .yacht-die { width: 2.6rem; height: 2.6rem; }
    .yacht-die .yacht-pips i { width: .45rem; height: .45rem; }
}
@media (prefers-reduced-motion: reduce) { .yacht-die.held { transform: none; } }

/* ---- snake ---------------------------------------------------------------
   The field is a square canvas the client draws; nothing here paints a cell.
   What this block owns is the frame around it, the seat list under it, and
   the two sentences for a connection that cannot carry the game. Every
   colour is a token: the snakes are drawn from --brass, --peg-blue,
   --peg-green and --peg-amber by snake.js reading these same properties,
   and the food from --peg-red, so the palette is in one place.
   ------------------------------------------------------------------------ */

/* The field is DARK: a snake on the site's ivory paper is a shape on a
   page, and this is a screen you look into. --hole is the site's recessed
   brown, the cribbage board's, and the frame around it is the same walnut
   as every other board here. snake.js lifts the seat colours toward the
   ivory before drawing them on it. */
.snake-field-wrap {
  position: relative;
  width: min(100%, 34rem);
  margin: 0 auto;
  aspect-ratio: 1 / 1;
  border: 2px solid var(--walnut);
  border-radius: .4rem;
  background: var(--hole);
  overflow: hidden;
}

/* The one surface a drag must not scroll the page from. Scoped to the
   canvas: the rest of the page scrolls as it always did. */
.snake-field {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

.snake-note {
  position: absolute;
  left: 50%;
  bottom: .6rem;
  transform: translateX(-50%);
  margin: 0;
  padding: .3rem .7rem;
  border-radius: .3rem;
  background: var(--walnut-deep);
  color: var(--card);
  font-size: .9rem;
  max-width: 90%;
  text-align: center;
}

.snake-cannot {
  margin: .8rem auto;
  max-width: 34rem;
  padding: .6rem .8rem;
  border-left: 3px solid var(--peg-red);
  background: var(--surface-2);
}

.snake-seats {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: .3rem 1.4rem;
  margin: .7rem auto;
  padding: 0;
  max-width: 34rem;
}

/* A row reads as one sentence: "Alice (you) starts at the right, 0 rounds".
   NOT a flex container, which is what it was: every text node between the
   spans became an anonymous flex item, so the name, the side, the comma and
   the count were laid out as four separate things and read as a list of
   fragments. Inline content in a block does what was wanted. */
.snake-seat { line-height: 1.5; }
.snake-seat[data-you="1"] .snake-seat-name { font-weight: 600; }

.snake-swatch {
  display: inline-block;
  width: .7rem;
  height: .7rem;
  margin-right: .35rem;
  vertical-align: -1px;
  border-radius: .2rem;
  background: var(--peg-blue);
}

/* The seat order the client draws with: this seat is always the first
   colour, so your own snake is brass in every chair. */
.snake-seat.brass     .snake-swatch { background: var(--brass); }
.snake-seat.peg-blue  .snake-swatch { background: var(--peg-blue); }
.snake-seat.peg-green .snake-swatch { background: var(--peg-green); }
.snake-seat.peg-amber .snake-swatch { background: var(--peg-amber); }

.snake-reading { max-width: 34rem; margin: .6rem auto; }
.snake-rounds { max-width: 34rem; margin-inline: auto; }

/* The slide is the client's, done on a canvas, so there is nothing here to
   turn off; this is for the note, which is the only thing that moves. */
@media (prefers-reduced-motion: reduce) {
  .snake-note { transition: none; }
}


/* The ready lobby, retrofitted 25 September 2026. Lightlines' block word for
   word, because it is the same panel doing the same job, and a second set of
   sizes would be a second thing to keep in step. No colour of its own: the
   button is the shared .button.primary and this only places and sizes it,
   which is what keeps this block tokens-only. */
.snake-ready {
  display: flex;
  justify-content: center;
  margin: .8rem auto 0;
  max-width: 34rem;
}
.snake-ready-button {
  min-width: 12rem;
  padding: .7rem 1.4rem;
  font-size: 1.05rem;
}
.snake-ready-button[disabled] { opacity: .55; }
/* ---- paintfield -----------------------------------------------------------
   The field is a square canvas the client draws; nothing here paints a cell.
   What this block owns is the bar above the field, the frame around it, the
   seat list under it, and the two sentences for a connection that cannot
   carry the game. Every colour is a token: paintfield.js reads --brass,
   --peg-blue, --peg-green and --peg-amber from these same properties and
   lifts them toward the ivory before drawing them on the dark ground, so the
   palette is in one place and the swatches below show it unlifted.
   ------------------------------------------------------------------------ */

/* THE COUNT BAR is the product. A round is sixty seconds of cells changing
   hands and the numbers in the seat list are too small to watch, so the bar
   carries the whole state of the round in one glance: a band a seat, its
   width its share of the painted cells. It sits ABOVE the field because that
   is where a thumb is not. */
.pf-bar {
  display: block;
  width: min(100%, 34rem);
  height: 1rem;
  margin: 0 auto .4rem;
  border: 1px solid var(--walnut);
  border-radius: .2rem;
  background: var(--hole);
  overflow: hidden;
  font-size: 0;              /* no space between inline bands */
  white-space: nowrap;
}

.pf-band {
  display: inline-block;
  height: 100%;
  width: 0;
  vertical-align: top;
  transition: width 120ms linear;
}

.pf-band.brass     { background: var(--brass); }
.pf-band.peg-blue  { background: var(--peg-blue); }
.pf-band.peg-green { background: var(--peg-green); }
.pf-band.peg-amber { background: var(--peg-amber); }

/* The field is DARK: --hole is the site's recessed brown, the cribbage
   board's, and the frame is the same walnut as every other board here. */
.pf-wrap {
  position: relative;
  width: min(100%, 34rem);
  margin: 0 auto;
  aspect-ratio: 1 / 1;
  border: 2px solid var(--walnut);
  border-radius: .4rem;
  background: var(--hole);
  overflow: hidden;
}

/* The one surface a drag must not scroll the page from. Scoped to the
   canvas: the rest of the page scrolls as it always did. */
.pf-field {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

.pf-note {
  position: absolute;
  left: 50%;
  bottom: .6rem;
  transform: translateX(-50%);
  margin: 0;
  padding: .3rem .7rem;
  border-radius: .3rem;
  background: var(--walnut-deep);
  color: var(--card);
  font-size: .9rem;
  max-width: 90%;
  text-align: center;
}

.pf-cannot {
  margin: .8rem auto;
  max-width: 34rem;
  padding: .6rem .8rem;
  border-left: 3px solid var(--peg-red);
  background: var(--surface-2);
}

.pf-seats {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: .3rem 1.4rem;
  margin: .7rem auto;
  padding: 0;
  max-width: 34rem;
}

/* A row reads as one sentence: "Alice (you) starts top left, 14 cells, 0
   rounds". NOT a flex container: every text node between the spans would
   become an anonymous flex item and the row would read as a list of
   fragments, which is a bug the owner found on Snake's page. */
.pf-seat { line-height: 1.5; }
.pf-seat[data-you="1"] .pf-seat-name { font-weight: 600; }

.pf-swatch {
  display: inline-block;
  width: .7rem;
  height: .7rem;
  margin-right: .35rem;
  vertical-align: -1px;
  border-radius: .2rem;
  background: var(--peg-blue);
}

/* The seat order the client draws with: this seat is always the first
   colour, so your own paint is brass in every chair. */
.pf-seat.brass     .pf-swatch { background: var(--brass); }
.pf-seat.peg-blue  .pf-swatch { background: var(--peg-blue); }
.pf-seat.peg-green .pf-swatch { background: var(--peg-green); }
.pf-seat.peg-amber .pf-swatch { background: var(--peg-amber); }

.pf-reading { max-width: 34rem; margin: .6rem auto; }
.pf-rounds { max-width: 34rem; margin-inline: auto; }

/* The bands are the only thing on this page that animates in CSS; the field
   and the painters are the client's, on a canvas, and it turns its own slide
   off under the same preference. */
@media (prefers-reduced-motion: reduce) {
  .pf-band { transition: none; }
  .pf-note { transition: none; }
}


/* The ready lobby, retrofitted 25 September 2026. Lightlines' block word for
   word, because it is the same panel doing the same job, and a second set of
   sizes would be a second thing to keep in step. No colour of its own: the
   button is the shared .button.primary and this only places and sizes it,
   which is what keeps this block tokens-only. */
.pf-ready {
  display: flex;
  justify-content: center;
  margin: .8rem auto 0;
  max-width: 34rem;
}
.pf-ready-button {
  min-width: 12rem;
  padding: .7rem 1.4rem;
  font-size: 1.05rem;
}
.pf-ready-button[disabled] { opacity: .55; }
/* ---- the language switcher, in the footer of every page (plan_i18n/01) ----
   A real form: a select and a submit button, which app.js turns into a
   select alone by hiding the button once its change handler is attached.

   `.lang-switch` and not `.lang`, on the house convention of a per-feature
   prefix, because a bare name in this file collides with somebody else's
   silently: nothing errors, the page just draws wrong.

   Every token here is declared on :root, checked rather than assumed. */
.lang-switch { display: flex; align-items: center; gap: .4rem; margin-inline-start: auto; }
.lang-switch label { color: var(--muted); }
.lang-switch select { font: inherit; font-size: .9rem; color: var(--ink);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 6px; padding: .15rem .3rem; }
/* Hidden by app.js rather than by this file. A rule here would leave anybody
   whose JavaScript failed to load with a form and no way to submit it. */
.lang-switch button[hidden] { display: none; }

/* --- Maker and Breaker (plan_order_and_chaos/05) --------------------------

   Two six by six boards, stacked, yours first. Warship is the precedent for
   two grids on a phone and its answer is stacked rather than side by side: at
   375px two boards side by side give 24 pixel cells and a thumb covers four
   of them. Stacked, a cell is about 55px, which is why this game has no
   tap-target gate mark where Dots and Boxes needed one.

   THE LIVE BOARD SWAPS EVERY ROUND. That is the one thing about this game a
   new player finds confusing, so it is signalled three ways and never by
   colour alone: the live board keeps full opacity and gains an outline, the
   other is dimmed, and the label above says which in words.

   THE SYMBOLS BELONG TO NOBODY. X and O are both marks either player may
   place, so they are drawn in --ink on both boards; colouring them by seat
   would teach the opposite of the one rule people get wrong. The BOARDS carry
   the seat tint instead, and the labels back it up. */

.mb { display: flex; flex-direction: column; align-items: center; gap: .9rem; }

.mb-marks { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  justify-content: center; }
.mb-mark { font: inherit; font-weight: 700; font-size: 1.1rem; line-height: 1;
  min-width: 2.6rem; padding: .45rem .6rem; color: var(--ink);
  background: var(--surface-2); border: 2px solid var(--line);
  border-radius: 8px; cursor: pointer; }
.mb-mark.on { border-color: var(--accent); background: var(--surface); }
.mb-mark:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* BORDER-BOX, and this site does not set it globally, so a width here is a
   CONTENT width and the padding and the border are added on top. Declared
   360, this box measured 380 (360 + 8 + 8 + 2 + 2) and spilled out of a 375
   pixel phone, which is the width the whole stacked layout exists for. It
   could not be seen in a browser window, because Chrome will not open
   narrower than 500px; the gate's mark 9 pins the page to 375 and found it. */
.mb-side { box-sizing: border-box; width: min(100%, 360px); padding: .5rem;
  border-radius: 10px; border: 2px solid transparent; opacity: .72;
  transition: opacity .2s ease; }
.mb-side[data-side="mine"] { background: color-mix(in srgb, var(--peg-red) 8%, transparent); }
.mb-side[data-side="theirs"] { background: color-mix(in srgb, var(--peg-blue) 8%, transparent); }
.mb-side.mb-live { opacity: 1; }
.mb-side[data-side="mine"].mb-live { border-color: var(--peg-red); }
.mb-side[data-side="theirs"].mb-live { border-color: var(--peg-blue); }
.mb-side.mb-done { opacity: .6; }

.mb-label { font-size: .95rem; margin: 0 0 .4rem; font-weight: 600; }
.mb-flag { font-weight: 400; color: var(--muted); }
.mb-run { margin: .4rem 0 0; font-size: .85rem; }

/* width: min(100%, 360px) on the board, never max-content: a max-content grid
   item sets the column that wide and the phone scrolls sideways. That is the
   rule the site learned on Four Connect. */
/* BOTH AXES ARE FIXED FRACTIONS, and the rows matter as much as the columns.
   With columns declared and rows left to `auto`, every row sizes to its own
   content: an empty row is one line of nothing and a row holding a piece is a
   line of text, so the board GREW A ROW AT A TIME as pieces went down and the
   two boards stopped matching each other. aspect-ratio sets the box, not the
   tracks inside it. */
.mb-board { box-sizing: border-box; display: grid; grid-template-columns: repeat(6, 1fr);
  grid-template-rows: repeat(6, 1fr); gap: 3px;
  width: 100%; aspect-ratio: 1; }

/* min-width and min-height 0: a grid item's automatic minimum size is its
   content, which would push a track wider than its 1fr share. */
.mb-cell { box-sizing: border-box; display: flex; align-items: center; justify-content: center;
  min-width: 0; min-height: 0; overflow: hidden;
  font: inherit; font-weight: 700; font-size: clamp(1rem, 6vw, 1.6rem);
  line-height: 1; color: var(--ink); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 4px; padding: 0; cursor: pointer; }
.mb-cell i { font-style: normal; }
.mb-cell[disabled] { cursor: default; }
.mb-cell.taken { background: var(--surface); }
.mb-cell.pending i { opacity: .55; }
.mb-cell.pending { border-style: dashed; border-color: var(--accent); }
.mb-cell.hinted { outline: 2px dashed var(--accent); outline-offset: -2px; }
.mb-cell:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.mb-side[data-side="mine"] .mb-cell.win { background: color-mix(in srgb, var(--peg-red) 70%, var(--surface)); }
.mb-side[data-side="theirs"] .mb-cell.win { background: color-mix(in srgb, var(--peg-blue) 70%, var(--surface)); }
.mb-cell.win { animation: mb-land .3s ease; }

@keyframes mb-land { from { transform: scale(.85); } to { transform: scale(1); } }

/* SIDE BY SIDE ON A DESKTOP, stacked on a phone. Stacking is forced at 375px
   (two boards across give 24 pixel cells and a thumb covers four of them), but
   on a wide screen there is room for both and putting them side by side is
   what a player wants: the board you are making on and the board you are
   breaking on, in one glance, with no scrolling between them.

   The piece toggle and the controls keep a row of their own (flex-basis:100%
   with wrap), so the DOM order still reads marks, yours, theirs, controls. */
@media (min-width: 720px) {
  .mb { flex-direction: row; flex-wrap: wrap; align-items: flex-start;
    justify-content: center; gap: 1.2rem; }
  .mb-marks, .mb .controls { flex-basis: 100%; }
  .mb-side { width: min(48%, 340px); }
}

@media (prefers-reduced-motion: reduce) {
  .mb-cell.win { animation: none; }
  .mb-side { transition: none; }
}

/* ---- Word Grid (plan_word_grid/06) ---------------------------------------
 * Sixteen letters, a box under them, your own list beside, and a count per
 * seat. The tray is drawn once by the template and only ever gains and loses
 * classes, so nothing here depends on a rebuild.
 *
 * The phone shape is the constraint: with the keyboard up, the tray and the
 * box have to be in the half of the screen that is left, so the tray is
 * first, the box second, and everything else scrolls under them. */
.wg { display: grid; gap: .75rem; width: min(100%, 44rem); margin: 0 auto; }

/* THE HEIGHT CAP IS THE POINT, and it took a browser to find it.
 *
 * The cells are square, so the tray's WIDTH sets its HEIGHT. At full
 * width on a phone that is a 375px tall tray, and with the status line
 * above it and the box below, the box lands under the halfway line: with
 * the keyboard up, a player cannot see the letters they are typing from.
 * Measured in headless Chrome the tray's bottom edge was at 482px against
 * a fold at 378.
 *
 * So the tray is capped by the viewport's height as well as its width.
 * 38vh leaves room for the status line above and the box below inside
 * half the screen, on the 757px window Chrome gives and on a real 812px
 * phone both. */
.wg-tray { display: grid; grid-template-columns: repeat(4, 1fr); gap: .4rem;
  list-style: none; margin: 0 auto; padding: 0;
  width: min(100%, 22rem, 34vh); }
.wg-cell-slot { display: block; }
.wg-cell { display: flex; align-items: center; justify-content: center;
  width: 100%; aspect-ratio: 1; padding: 0; border: 1px solid var(--line);
  border-radius: 10px; background: var(--surface); color: var(--ink);
  font: inherit; font-size: clamp(1.25rem, 7vw, 2rem); font-weight: 600;
  text-transform: uppercase; cursor: default;
  transition: background-color .15s ease-out, color .15s ease-out; }
/* A cell on the path of an accepted word, for a moment. */
.wg-cell.lit { background: var(--accent); color: var(--accent-ink);
  border-color: var(--accent); }
/* Tapping the letters to spell. The cursor is a pointer only while the
 * tray is live, because a pointer over a cell that does nothing is a
 * small lie about what the page will do. */
.wg-tray.tappable .wg-cell { cursor: pointer; }
/* A cell tapped into the word being built. Held, not flashed: it stays
 * until the word is sent or the path abandoned, so a player can see what
 * they have spelled without reading the box. */
.wg-cell.picked { background: var(--accent); color: var(--accent-ink);
  border-color: var(--accent); position: relative; }
/* The one that can be untapped, and the only one that can be added to. */
.wg-cell.picked-last { box-shadow: 0 0 0 2px var(--accent); }
/* Its place in the word, small, in the corner: with a letter repeated in
 * the tray the order is otherwise unreadable. */
.wg-cell.picked::after { content: attr(data-pick); position: absolute;
  top: 2px; right: 4px; font-size: .6rem; font-weight: 400; opacity: .8;
  letter-spacing: 0; }

/* The box and its send button on one line. The button is what a player
 * who is tapping submits with: no keyboard is open, so Enter is not
 * theirs to press. */
.wg-entry { display: flex; gap: .4rem; align-items: stretch;
  justify-content: center; width: min(100%, 22rem); }
.wg-entry .wg-input { width: auto; flex: 1 1 auto; min-width: 0; }
.wg-send { flex: 0 0 auto; padding-inline: .8rem; }

/* The countdown and the clock share a line rather than stacking: two
 * stacked output elements cost 30px of the half screen the keyboard
 * leaves, and neither has more than a few characters in it. */
.wg-play { display: grid; gap: .3rem; justify-items: center; }
.wg-meters { display: flex; gap: .8rem; align-items: baseline;
  justify-content: center; min-height: 1.4em; }
.wg-input { width: min(100%, 22rem); padding: .6rem .7rem; border-radius: 8px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  font: inherit; font-size: 1.1rem; text-align: center; letter-spacing: .04em; }
.wg-input.wrong { border-color: var(--peg-red); color: var(--peg-red); }
.wg-count { min-height: 1.4em; font-weight: 600; }
.wg-clock { font-variant-numeric: tabular-nums; color: var(--muted); }
.wg-clock.urgent { color: var(--peg-red); font-weight: 600; }
.wg-english { margin: .2rem 0 0; text-align: center; font-size: .85rem; }

.wg-found-head { margin: .4rem 0 .2rem; font-size: 1rem; }
.wg-found { display: flex; flex-wrap: wrap; gap: .3rem .5rem;
  list-style: none; margin: 0; padding: 0; }
.wg-word { display: inline-flex; gap: .3rem; align-items: baseline;
  padding: .15rem .45rem; border-radius: 6px; background: var(--surface-2); }
.wg-word-text { text-transform: uppercase; letter-spacing: .03em; }
.wg-word-points { color: var(--muted); font-size: .8rem;
  font-variant-numeric: tabular-nums; }
/* A word somebody else found too: it scored nobody, and the page says so
 * rather than quietly subtracting it. */
.wg-word.cancelled .wg-word-text { text-decoration: line-through;
  text-decoration-thickness: 2px; color: var(--muted); }

.wg-seats { list-style: none; margin: 0; padding: 0; display: grid; gap: .3rem; }
.wg-seat { display: flex; align-items: baseline; gap: .5rem; }
.wg-seat.you .wg-name { font-weight: 600; }
.wg-seat[data-status="dq"] { opacity: .55; }
.wg-who { display: inline-flex; align-items: baseline; gap: .35rem; }
.wg-words { margin-left: auto; font-variant-numeric: tabular-nums;
  font-weight: 600; }
.wg-score { font-variant-numeric: tabular-nums; color: var(--muted);
  min-width: 2.5em; text-align: right; }

.wg-result { width: 100%; border-collapse: collapse; }
.wg-result th, .wg-result td { padding: .3rem .4rem; text-align: left;
  border-bottom: 1px solid var(--line); }
.wg-result td:first-child, .wg-result td:last-child { text-align: right;
  font-variant-numeric: tabular-nums; }

.wg-reveal-head { margin: .8rem 0 .2rem; font-size: 1rem; }
.wg-cancelled-word { text-transform: uppercase; letter-spacing: .03em; }
.wg-key { display: flex; flex-wrap: wrap; gap: .25rem .45rem;
  list-style: none; margin: 0; padding: 0; }
.wg-key-word { display: inline-flex; gap: .25rem; align-items: baseline;
  font-size: .9rem; }

@media (prefers-reduced-motion: reduce) {
  .wg-cell { transition: none; }
}

/* ---- oware ---- */
/* A wooden board with twelve pits in it, and REAL SEEDS in the pits, drawn from
   the MATERIAL tokens rather than the surface ones. That is the whole fix for a
   bug worth remembering: --card, --hole, --walnut and --brass are fixed colours
   that do NOT flip with the theme, while --ink and --surface do. The first
   draft put a fixed cream (--card) behind text that inherited --ink, which is
   dark in light mode and LIGHT IN DARK MODE, so your own six houses were
   cream-on-cream and unreadable. A fixed background needs a fixed foreground;
   checkers pairs --card with explicitly coloured discs for the same reason.

   It also wrote `background: var(--bg)`, and there is no --bg token on this
   site. An undefined custom property with no fallback makes the declaration
   invalid at computed-value time, so it silently became transparent instead of
   failing - which looked plausible and hid the other half of the bug. */
.oware {
	/* Scoped to this game rather than added to the global palette: nickernut,
	   which is what the seeds actually are.

	   --pit-ink IS A LITERAL AND NOT --accent-ink, WHICH IS THE SAME BUG TWICE.
	   --accent-ink is the text colour FOR THE ACCENT, so it flips: cream in
	   light mode, #1b1815 in dark. On a --hole pit, which does not flip, that
	   put near-black numerals on a near-black pit and the counts vanished. A
	   fixed background needs a foreground that is equally fixed. */
	--seed: #d9c7a3;
	--seed-edge: rgba(36, 21, 9, .55);
	--pit-ink: #f3ead8;
}
.oware-board {
	display: grid; gap: .45rem;
	width: min(100%, 34rem); margin: 0 auto; padding: .55rem;
	box-sizing: border-box;
	background: var(--walnut); border-radius: 14px;
	box-shadow: inset 0 2px 8px rgba(0, 0, 0, .35), 0 3px 10px var(--shadow);
}
.oware-row { display: grid; grid-template-columns: repeat(6, 1fr); gap: .4rem; }

.oware .house {
	position: relative;
	display: flex; flex-direction: column; align-items: center; justify-content: center;
	/* A tap target, not a decoration: twelve of these have to fit a narrow
	   screen and still be worth aiming at. */
	min-height: 4.4rem; padding: .3rem .2rem;
	box-sizing: border-box;
	border: 2px solid transparent; border-radius: 999px;
	/* Both fixed, so the pair survives either theme. */
	background: var(--hole); color: var(--pit-ink);
	font: inherit; line-height: 1.1; cursor: default;
	box-shadow: inset 0 3px 7px rgba(0, 0, 0, .6);
}

/* THE SEEDS. A wrapped cluster rather than a grid, so a pit of five and a pit
   of nine look like handfuls rather than like a bar chart. The nth-child
   nudges below are what stop it reading as rows. */
.oware .pips {
	display: flex; flex-wrap: wrap; justify-content: center; align-content: center;
	gap: 1px; width: 2.5rem; min-height: 1.9rem;
}
.oware .seed {
	width: .46rem; height: .46rem; border-radius: 50%;
	background: var(--seed);
	box-shadow: inset 0 -1px 1px var(--seed-edge);
	text-decoration: none;
}
.oware .seed:nth-child(3n)   { transform: translateY(1px); }
.oware .seed:nth-child(4n)   { transform: translate(1px, -1px); }
.oware .seed:nth-child(5n)   { transform: translate(-1px, 1px); }
.oware .seed:nth-child(7n)   { transform: translateY(-1px); }

/* Twelve or more is the point where a sow laps the whole board and skips the
   house it came from, so the pit says so: the cluster is capped at eleven and
   the count carries the rest. */
.oware .house.loaded .pips::after {
	content: ""; width: .46rem; height: .46rem; border-radius: 50%;
	background: var(--seed); opacity: .45;
}
.oware .house.loaded { box-shadow: inset 0 3px 7px rgba(0, 0, 0, .6), inset 0 0 0 1px var(--brass); }

.oware .house.empty .pips { min-height: 1.9rem; }

/* The count stays, small, because players count and because a loaded pit shows
   eleven seeds whatever it holds. */
.oware .house .seeds { font-size: .8rem; font-weight: 700; font-variant-numeric: tabular-nums; opacity: .9; }
.oware .house.empty .seeds { opacity: .4; font-weight: 400; }
.oware .house .letter { font-size: .65rem; font-style: normal; opacity: .6; }

/* Yours are ringed in brass. The ring is the signal rather than the fill, so
   the two rows stay equally readable and only one of them looks like yours. */
.oware .house.mine { border-color: rgba(207, 166, 79, .45); }
.oware .house.playable { border-color: var(--brass); cursor: pointer; }
.oware .house.playable:hover,
.oware .house.playable:focus-visible { background: #33200f; }

/* The last move and the hint are marked by a second ring rather than by colour
   alone, so the board still reads with colour off. */
.oware .house.last-move { outline: 2px dashed rgba(251, 247, 238, .55); outline-offset: 2px; }
.oware .house.hinted { outline: 2px solid var(--brass); outline-offset: 2px; }

.oware .house[disabled] { opacity: 1; }
.oware-captured { margin: .6rem 0 0; text-align: center; }

@media (max-width: 26rem) {
	.oware .house { min-height: 3.9rem; }
	.oware .pips { width: 2.1rem; min-height: 1.6rem; }
	.oware .seed { width: .4rem; height: .4rem; }
	.oware .house .seeds { font-size: .72rem; }
}

/* ---- Hex (plan_hex/05) -------------------------------------------------
   The first board here that is TALLER THAN WIDE, and the first that needs a
   height cap in portrait rather than only in the landscape companion. The
   rhombus is 17R by 16*sqrt(3)R, an aspect of 0.6134 (plan_hex/01), because
   flat-top hexagons in columns give about 37px cells on a phone where the
   wide way round gives 22px.

   A CELL IS A BUTTON CLIPPED TO A HEXAGON, positioned from percentages
   Board::layout computed. clip-path clips POINTER EVENTS as well as paint,
   which is what makes the hit areas tessellate with no gap and nothing to hit
   between them; measured in Chrome at three widths, 121 of 121 taps land on
   the cell whose centre is nearest.

   Everything is scoped under .hex: site.css has already paid once for a
   game-specific block that redefined .card and damaged two games that knew
   nothing about it.

   THIS BLOCK WAS LOST ONCE, on 18 Sep 2026 at 16:01, to another session's
   whole-file write of this stylesheet, and the board rendered as 121 buttons
   in normal flow until it was put back. t/hex-05-page.t asserts the block is
   here and cross-greps it against the template and the client, which is how
   it was noticed. If you are appending a game block to this file, append it:
   do not write the file back from a copy you read minutes ago. */

.hex { width: min(100%, 380px, calc((100vh - 15rem) * 0.6134)); margin: 0 auto; direction: ltr; }

/* direction: ltr is not decoration. An RTL locale would mirror the board
   while the labels stayed correct, which silently exchanges the two seats'
   sides: a1 would be drawn where k1 is and the sentence would still say
   "the left and right sides". */

.hex-board { position: relative; width: 100%; aspect-ratio: 0.6134;
             background: linear-gradient(170deg, #6a4529, var(--walnut) 40%, var(--walnut-deep));
             border-radius: 10px; padding: 0; box-sizing: border-box;
             box-shadow: inset 0 2px 8px rgba(0,0,0,.35), 0 3px 10px var(--shadow); }

/* Sized by the page, never by the cells: a board declared max-content wide
   makes the .game grid's column that wide and a phone scrolls sideways. */
.hex-cell { position: absolute; border: 0; padding: 0; margin: 0; display: block;
            width: 11.7647%; height: 6.2500%;
            background: var(--felt, #237a4f); cursor: default; outline-offset: -3px; }
.hex-cell:not(:disabled) { cursor: pointer; }
.hex-cell:disabled { opacity: 1; }        /* a cell is not a greyed control */
.hex-cell:focus-visible { outline: 3px solid var(--accent); z-index: 2; }

@supports (clip-path: polygon(0 0, 100% 0, 100% 100%)) {
    .hex-cell { clip-path: polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%); }
}

/* A STONE IS NOT TOLD APART BY COLOUR ALONE. Red and blue differ in
   lightness, in border and in the inset highlight, so the board survives a
   monochrome screen and colour-blindness both, which is the rule the reversi
   and seat-swatch blocks above state twice. */
.hex-stone { position: absolute; inset: 22%; border-radius: 50%; display: none;
             box-shadow: inset 0 -2px 4px rgba(0,0,0,.35), 0 2px 3px var(--shadow); }
.hex-red .hex-stone, .hex-blue .hex-stone { display: block; }
.hex-red .hex-stone  { background: var(--peg-red); border: 1px solid #6d1c13;
                       box-shadow: inset 0 -3px 5px rgba(0,0,0,.4), 0 2px 3px var(--shadow); }
.hex-blue .hex-stone { background: var(--peg-blue); border: 2px solid #dfe6f5;
                       box-shadow: inset 0 3px 5px rgba(255,255,255,.45), 0 2px 3px var(--shadow); }

/* A playable cell is a translucent dot in brass, the site's "the system is
   pointing at this" colour, as checkers and reversi both use it. */
.hex-cell.hex-playable::before { content: ''; position: absolute; inset: 38%;
                                 border-radius: 50%; background: var(--brass); opacity: .45; }
.hex-cell.hex-last::after { content: ''; position: absolute; inset: 8%;
                            border-radius: 50%; box-shadow: inset 0 0 0 3px var(--brass); }
.hex-cell.hex-chain .hex-stone { box-shadow: inset 0 0 0 2px var(--brass),
                                             inset 0 -2px 4px rgba(0,0,0,.35); }
.hex-cell.hinted { outline: 3px dashed var(--brass); outline-offset: -4px; z-index: 1; }
.hex-cell.pending .hex-stone { opacity: .6; }

/* WHO JOINS WHAT, in words. This was four coloured bands around the board
   and they were wrong twice over: four rectangles cannot follow the four
   sheared sides of a rhombus, and positioned against a zero-height box they
   rendered as two stray blue lines underneath it, which is what the owner
   saw and asked about. A legend is legible, is read aloud, and does not
   pretend to trace a shape it cannot. */
.hex-legend { display: flex; flex-wrap: wrap; gap: .2rem 1rem; margin: .5rem 0 0;
              font-size: .85rem; color: var(--muted); }
.hex-legend-seat { display: inline-flex; align-items: center; gap: .35rem; }
.hex-swatch { display: inline-block; width: .8rem; height: .8rem; border-radius: 50%; }
.hex-swatch.hex-red  { background: var(--peg-red); border: 1px solid #6d1c13; }
.hex-swatch.hex-blue { background: var(--peg-blue); border: 2px solid #dfe6f5; }

.hex .controls { margin-top: .6rem; }

/* THE SWAP CONTROL HAS TO BE ABLE TO GO AWAY, and `hidden` alone does not
   do it here: `.button` sets `display: inline-block`, and an author
   stylesheet's display beats the user agent's `[hidden] { display: none }`.
   So the client set `hidden`, the button stayed on the page looking merely
   dim, and it was still offering the swap two moves after the swap had gone.
   The owner asked why. Three other games on this site carry the same rule
   for the same reason: .mar-claim[hidden], and both .needs-script ones. */
.hex-swap[hidden], #swap-hint[hidden] { display: none; }

/* The SELECTOR matters as much as the rule. `.hex-swap[hidden]` is a class
   plus an attribute and beats `button.button`, which is a class plus an
   element; a bare `.hex-gone` class would lose to it and change nothing
   while looking like a fix. Measured in the browser rather than worked out
   on paper, because that arithmetic is exactly the kind this page has
   already been wrong about once today. */

/* The landscape companion every board here has, with the rhombus's real
   ratio: a landscape phone is short rather than narrow, so there the height
   sets the limit too. */
@media (orientation: landscape) and (max-height: 520px) {
    .hex { width: min(100%, 380px, calc((100vh - 7rem) * 0.6134)); }
}
/* The 121 cell positions, from P2PGames::Game::Hex::Board::layout.

   THEY ARE HERE AND NOT IN A style ATTRIBUTE BECAUSE THE CSP FORBIDS
   ONE. A game page is served with `default-src 'self'` and no
   `style-src`, so every inline style on it is discarded by the browser
   and a board positioned that way renders as 121 invisible buttons in a
   walnut box. That is what happened on 18 Sep 2026, in front of the
   owner, and neither the Perl tests nor a headless render of a local
   copy could see it: a file:// page carries no CSP header at all.

   The board is one size (plan_hex/00, D4), so these are constants, and
   t/hex-05-page.t regenerates them from layout() and fails if a single
   one drifts. Width and height are the same for every cell and are on
   .hex-cell above. */
.hex-cell[data-cell="0"] { left: 0.0000%; top: 0.0000%; }
.hex-cell[data-cell="1"] { left: 8.8235%; top: 3.1250%; }
.hex-cell[data-cell="2"] { left: 17.6471%; top: 6.2500%; }
.hex-cell[data-cell="3"] { left: 26.4706%; top: 9.3750%; }
.hex-cell[data-cell="4"] { left: 35.2941%; top: 12.5000%; }
.hex-cell[data-cell="5"] { left: 44.1176%; top: 15.6250%; }
.hex-cell[data-cell="6"] { left: 52.9412%; top: 18.7500%; }
.hex-cell[data-cell="7"] { left: 61.7647%; top: 21.8750%; }
.hex-cell[data-cell="8"] { left: 70.5882%; top: 25.0000%; }
.hex-cell[data-cell="9"] { left: 79.4118%; top: 28.1250%; }
.hex-cell[data-cell="10"] { left: 88.2353%; top: 31.2500%; }
.hex-cell[data-cell="11"] { left: 0.0000%; top: 6.2500%; }
.hex-cell[data-cell="12"] { left: 8.8235%; top: 9.3750%; }
.hex-cell[data-cell="13"] { left: 17.6471%; top: 12.5000%; }
.hex-cell[data-cell="14"] { left: 26.4706%; top: 15.6250%; }
.hex-cell[data-cell="15"] { left: 35.2941%; top: 18.7500%; }
.hex-cell[data-cell="16"] { left: 44.1176%; top: 21.8750%; }
.hex-cell[data-cell="17"] { left: 52.9412%; top: 25.0000%; }
.hex-cell[data-cell="18"] { left: 61.7647%; top: 28.1250%; }
.hex-cell[data-cell="19"] { left: 70.5882%; top: 31.2500%; }
.hex-cell[data-cell="20"] { left: 79.4118%; top: 34.3750%; }
.hex-cell[data-cell="21"] { left: 88.2353%; top: 37.5000%; }
.hex-cell[data-cell="22"] { left: 0.0000%; top: 12.5000%; }
.hex-cell[data-cell="23"] { left: 8.8235%; top: 15.6250%; }
.hex-cell[data-cell="24"] { left: 17.6471%; top: 18.7500%; }
.hex-cell[data-cell="25"] { left: 26.4706%; top: 21.8750%; }
.hex-cell[data-cell="26"] { left: 35.2941%; top: 25.0000%; }
.hex-cell[data-cell="27"] { left: 44.1176%; top: 28.1250%; }
.hex-cell[data-cell="28"] { left: 52.9412%; top: 31.2500%; }
.hex-cell[data-cell="29"] { left: 61.7647%; top: 34.3750%; }
.hex-cell[data-cell="30"] { left: 70.5882%; top: 37.5000%; }
.hex-cell[data-cell="31"] { left: 79.4118%; top: 40.6250%; }
.hex-cell[data-cell="32"] { left: 88.2353%; top: 43.7500%; }
.hex-cell[data-cell="33"] { left: 0.0000%; top: 18.7500%; }
.hex-cell[data-cell="34"] { left: 8.8235%; top: 21.8750%; }
.hex-cell[data-cell="35"] { left: 17.6471%; top: 25.0000%; }
.hex-cell[data-cell="36"] { left: 26.4706%; top: 28.1250%; }
.hex-cell[data-cell="37"] { left: 35.2941%; top: 31.2500%; }
.hex-cell[data-cell="38"] { left: 44.1176%; top: 34.3750%; }
.hex-cell[data-cell="39"] { left: 52.9412%; top: 37.5000%; }
.hex-cell[data-cell="40"] { left: 61.7647%; top: 40.6250%; }
.hex-cell[data-cell="41"] { left: 70.5882%; top: 43.7500%; }
.hex-cell[data-cell="42"] { left: 79.4118%; top: 46.8750%; }
.hex-cell[data-cell="43"] { left: 88.2353%; top: 50.0000%; }
.hex-cell[data-cell="44"] { left: 0.0000%; top: 25.0000%; }
.hex-cell[data-cell="45"] { left: 8.8235%; top: 28.1250%; }
.hex-cell[data-cell="46"] { left: 17.6471%; top: 31.2500%; }
.hex-cell[data-cell="47"] { left: 26.4706%; top: 34.3750%; }
.hex-cell[data-cell="48"] { left: 35.2941%; top: 37.5000%; }
.hex-cell[data-cell="49"] { left: 44.1176%; top: 40.6250%; }
.hex-cell[data-cell="50"] { left: 52.9412%; top: 43.7500%; }
.hex-cell[data-cell="51"] { left: 61.7647%; top: 46.8750%; }
.hex-cell[data-cell="52"] { left: 70.5882%; top: 50.0000%; }
.hex-cell[data-cell="53"] { left: 79.4118%; top: 53.1250%; }
.hex-cell[data-cell="54"] { left: 88.2353%; top: 56.2500%; }
.hex-cell[data-cell="55"] { left: 0.0000%; top: 31.2500%; }
.hex-cell[data-cell="56"] { left: 8.8235%; top: 34.3750%; }
.hex-cell[data-cell="57"] { left: 17.6471%; top: 37.5000%; }
.hex-cell[data-cell="58"] { left: 26.4706%; top: 40.6250%; }
.hex-cell[data-cell="59"] { left: 35.2941%; top: 43.7500%; }
.hex-cell[data-cell="60"] { left: 44.1176%; top: 46.8750%; }
.hex-cell[data-cell="61"] { left: 52.9412%; top: 50.0000%; }
.hex-cell[data-cell="62"] { left: 61.7647%; top: 53.1250%; }
.hex-cell[data-cell="63"] { left: 70.5882%; top: 56.2500%; }
.hex-cell[data-cell="64"] { left: 79.4118%; top: 59.3750%; }
.hex-cell[data-cell="65"] { left: 88.2353%; top: 62.5000%; }
.hex-cell[data-cell="66"] { left: 0.0000%; top: 37.5000%; }
.hex-cell[data-cell="67"] { left: 8.8235%; top: 40.6250%; }
.hex-cell[data-cell="68"] { left: 17.6471%; top: 43.7500%; }
.hex-cell[data-cell="69"] { left: 26.4706%; top: 46.8750%; }
.hex-cell[data-cell="70"] { left: 35.2941%; top: 50.0000%; }
.hex-cell[data-cell="71"] { left: 44.1176%; top: 53.1250%; }
.hex-cell[data-cell="72"] { left: 52.9412%; top: 56.2500%; }
.hex-cell[data-cell="73"] { left: 61.7647%; top: 59.3750%; }
.hex-cell[data-cell="74"] { left: 70.5882%; top: 62.5000%; }
.hex-cell[data-cell="75"] { left: 79.4118%; top: 65.6250%; }
.hex-cell[data-cell="76"] { left: 88.2353%; top: 68.7500%; }
.hex-cell[data-cell="77"] { left: 0.0000%; top: 43.7500%; }
.hex-cell[data-cell="78"] { left: 8.8235%; top: 46.8750%; }
.hex-cell[data-cell="79"] { left: 17.6471%; top: 50.0000%; }
.hex-cell[data-cell="80"] { left: 26.4706%; top: 53.1250%; }
.hex-cell[data-cell="81"] { left: 35.2941%; top: 56.2500%; }
.hex-cell[data-cell="82"] { left: 44.1176%; top: 59.3750%; }
.hex-cell[data-cell="83"] { left: 52.9412%; top: 62.5000%; }
.hex-cell[data-cell="84"] { left: 61.7647%; top: 65.6250%; }
.hex-cell[data-cell="85"] { left: 70.5882%; top: 68.7500%; }
.hex-cell[data-cell="86"] { left: 79.4118%; top: 71.8750%; }
.hex-cell[data-cell="87"] { left: 88.2353%; top: 75.0000%; }
.hex-cell[data-cell="88"] { left: 0.0000%; top: 50.0000%; }
.hex-cell[data-cell="89"] { left: 8.8235%; top: 53.1250%; }
.hex-cell[data-cell="90"] { left: 17.6471%; top: 56.2500%; }
.hex-cell[data-cell="91"] { left: 26.4706%; top: 59.3750%; }
.hex-cell[data-cell="92"] { left: 35.2941%; top: 62.5000%; }
.hex-cell[data-cell="93"] { left: 44.1176%; top: 65.6250%; }
.hex-cell[data-cell="94"] { left: 52.9412%; top: 68.7500%; }
.hex-cell[data-cell="95"] { left: 61.7647%; top: 71.8750%; }
.hex-cell[data-cell="96"] { left: 70.5882%; top: 75.0000%; }
.hex-cell[data-cell="97"] { left: 79.4118%; top: 78.1250%; }
.hex-cell[data-cell="98"] { left: 88.2353%; top: 81.2500%; }
.hex-cell[data-cell="99"] { left: 0.0000%; top: 56.2500%; }
.hex-cell[data-cell="100"] { left: 8.8235%; top: 59.3750%; }
.hex-cell[data-cell="101"] { left: 17.6471%; top: 62.5000%; }
.hex-cell[data-cell="102"] { left: 26.4706%; top: 65.6250%; }
.hex-cell[data-cell="103"] { left: 35.2941%; top: 68.7500%; }
.hex-cell[data-cell="104"] { left: 44.1176%; top: 71.8750%; }
.hex-cell[data-cell="105"] { left: 52.9412%; top: 75.0000%; }
.hex-cell[data-cell="106"] { left: 61.7647%; top: 78.1250%; }
.hex-cell[data-cell="107"] { left: 70.5882%; top: 81.2500%; }
.hex-cell[data-cell="108"] { left: 79.4118%; top: 84.3750%; }
.hex-cell[data-cell="109"] { left: 88.2353%; top: 87.5000%; }
.hex-cell[data-cell="110"] { left: 0.0000%; top: 62.5000%; }
.hex-cell[data-cell="111"] { left: 8.8235%; top: 65.6250%; }
.hex-cell[data-cell="112"] { left: 17.6471%; top: 68.7500%; }
.hex-cell[data-cell="113"] { left: 26.4706%; top: 71.8750%; }
.hex-cell[data-cell="114"] { left: 35.2941%; top: 75.0000%; }
.hex-cell[data-cell="115"] { left: 44.1176%; top: 78.1250%; }
.hex-cell[data-cell="116"] { left: 52.9412%; top: 81.2500%; }
.hex-cell[data-cell="117"] { left: 61.7647%; top: 84.3750%; }
.hex-cell[data-cell="118"] { left: 70.5882%; top: 87.5000%; }
.hex-cell[data-cell="119"] { left: 79.4118%; top: 90.6250%; }
.hex-cell[data-cell="120"] { left: 88.2353%; top: 93.7500%; }

/* ---- go (plan_game_go/11) ------------------------------------------------ */

/* A STONE SITS ON AN INTERSECTION, NOT IN A CELL. Every other grid game on
   this site draws cells and puts a piece inside one; Go draws LINES and puts a
   stone where two of them cross. A board built as a grid of bordered boxes is
   a chessboard with the wrong game on it.

   EACH POINT DRAWS ITS OWN HALF OF THE TWO LINES THAT MEET THERE, and that is
   not the obvious way round. The obvious way is one background on the board,
   inset by half a cell - and it was half a cell out, so every stone sat in the
   middle of a square instead of on a crossing. Drawn per point, the lines pass
   through the stones' centres by construction and cannot drift.

   The outermost line of a goban IS the edge of the playing area, so the edge
   points draw only the inner half of theirs. Page.pm names them. */

.go { width: min(100%, 520px); margin: 0 auto; direction: ltr; }

/* --n IS SET HERE AND NOT IN A style ATTRIBUTE. The site's CSP is
   default-src 'self' with no style-src 'unsafe-inline', so an inline
   custom property is dropped by the browser, every repeat(var(--n), ...)
   below becomes invalid, and the board collapses. There are three board
   sizes and there will never be a fourth, so three rules is the whole cost
   of obeying the policy. */
.go-9  { --n: 9; }
.go-13 { --n: 13; }
.go-19 { --n: 19; }

/* direction: ltr is not decoration. An RTL locale would mirror the board while
   the coordinates stayed correct, so A1 would be drawn where J1 is and every
   sentence naming a point would be wrong. Same reasoning as the hex table. */

.go-frame {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-rows: auto auto auto;
    gap: .15rem .2rem;
    /* STRETCH, NOT CENTRE. The number strips are grid children whose own rows
       have to line up with the board's, so they need the board's full height:
       centred, they collapsed to their text height and printed 9 8 7 6 5 4 3
       2 1 as a tiny stack beside a full-size board. */
    align-items: stretch;
    justify-items: stretch;
    background: linear-gradient(165deg, #dcb173, var(--goban, #d2a05f) 45%, #bb8742);
    border-radius: 10px;
    padding: .5rem .55rem;
    box-shadow: inset 0 2px 10px rgba(90, 55, 20, .28), 0 3px 10px var(--shadow);
}

.go-cols, .go-nums {
    display: grid; font-size: .6rem; font-style: normal; font-weight: 600;
    color: #5f3f22; letter-spacing: 0;
}
.go-cols { grid-column: 2; grid-template-columns: repeat(var(--n), minmax(0, 1fr)); align-self: center; }
.go-cols i { text-align: center; }
.go-cols.bottom { grid-row: 3; }
.go-nums { grid-row: 2; grid-template-rows: repeat(var(--n), minmax(0, 1fr)); height: 100%; }
.go-nums i { display: grid; place-items: center; padding: 0 .1rem; }
.go-nums.right { grid-column: 3; grid-row: 2; }

.go-board {
    grid-column: 2; grid-row: 2;
    width: 100%;
    aspect-ratio: 1;
    display: grid;
    grid-template-columns: repeat(var(--n), minmax(0, 1fr));
    grid-template-rows: repeat(var(--n), minmax(0, 1fr));
}

.go-pt {
    position: relative;
    border: 0; margin: 0; padding: 0;
    background: transparent;
    -webkit-appearance: none; appearance: none;
    cursor: default;
    border-radius: 50%;
}
.go-pt:not(:disabled) { cursor: pointer; }
.go-pt:disabled { opacity: 1; }   /* a point is not a greyed control */
.go-pt:focus-visible { outline: 3px solid var(--accent); outline-offset: -2px; z-index: 5; }

/* The two lines. ::before runs across, ::after runs down, and an edge point
   keeps only the half that points inwards. */
.go-pt::before, .go-pt::after { content: ""; position: absolute; background: #4a3418; }
.go-pt::before { left: 0; right: 0; top: 50%; height: 1px; transform: translateY(-.5px); }
.go-pt::after  { top: 0; bottom: 0; left: 50%; width: 1px; transform: translateX(-.5px); }
.go-pt.first-col::before { left: 50%; }
.go-pt.last-col::before  { right: 50%; }
.go-pt.first-row::after  { top: 50%; }
.go-pt.last-row::after   { bottom: 50%; }

/* The star points (hoshi) and every other marker share one element, because a
   point can carry a stone AND a mark at the same time. */
.go-mark, .go-stone {
    position: absolute; inset: 0; margin: auto; pointer-events: none;
}
.go-stone { z-index: 2; }
.go-mark  { z-index: 3; }

.go-pt.star .go-mark {
    width: 20%; height: 20%; min-width: 4px; min-height: 4px;
    border-radius: 50%; background: #4a3418;
}

/* THE STONE. Nearly a whole cell, which is what a real set looks like: stones
   on adjacent points very nearly touch and the line vanishes underneath. */
.go-pt.stone .go-stone {
    width: 92%; height: 92%;
    border-radius: 50%;
    box-shadow: 0 1px 2px rgba(40, 25, 10, .45);
}
.go-pt.stone.p1 .go-stone {
    background: radial-gradient(circle at 32% 27%, #6e6b67 0%, #302c28 38%, #0d0c0b 100%);
}
.go-pt.stone.p2 .go-stone {
    background: radial-gradient(circle at 32% 27%, #fff 0%, #f4efe4 45%, #cec4b3 100%);
}
/* A stone covers the star dot under it. */
.go-pt.stone.star .go-mark { display: none; }

/* An empty point shows nothing until it is pointed at. A marker on every legal
   point would draw three hundred of them on a full board and tell the player
   nothing, which is the decision the terminal made for the same reason. */
.go-pt.playable:hover .go-stone,
.go-pt.playable:focus-visible .go-stone {
    width: 62%; height: 62%; border-radius: 50%;
    background: rgba(20, 16, 12, .30);
}

/* THE LAST MOVE, because a capture of a big group is the most important thing
   that can happen and it is invisible in an after-picture. */
.go-pt.last-move .go-mark {
    width: 34%; height: 34%; border-radius: 50%;
    background: transparent; border: 2px solid #d94b2e; box-sizing: border-box;
}
.go-pt.stone.p1.last-move .go-mark { border-color: #ff7a5c; }

/* A GROUP AGREED DEAD during the confirmation phase. It stays on the board,
   because it has not been removed yet and the other player has to see what is
   being proposed, but it is plainly no longer alive. */
.go-pt.dead .go-stone { opacity: .35; }
.go-pt.dead .go-mark {
    width: 46%; height: 2px; background: #b1341c; border-radius: 2px;
    transform: rotate(45deg);
}
.go-pt.markable { cursor: pointer; }

/* TERRITORY, AND ONLY ONCE IT IS AGREED. Marks::territory returns nothing
   while a game is active, for the reason the leak gate checks: until the dead
   stones are settled the same empty point belongs to nobody, and shading it
   during play would hand one player a reading of the position. */
.go-pt.owned .go-mark { width: 28%; height: 28%; border-radius: 2px; opacity: .85; }
.go-pt.owned.p1 .go-mark { background: #17140f; }
.go-pt.owned.p2 .go-mark { background: #fbf7ee; }

/* `.button` SETS display: inline-block, WHICH BEATS THE hidden ATTRIBUTE.
   The UA rule is [hidden] { display: none } at the lowest specificity, so a
   class on the same element wins and a button marked hidden is drawn anyway:
   Done, Accept and Play on all appeared during ordinary play. Named by id so
   this outranks .button without !important. */
#go-done[hidden], #go-accept[hidden], #go-dispute[hidden], #go-pass[hidden] {
    display: none;
}

.go-marking {
    margin: .6rem 0 0; padding: .6rem .8rem;
    background: var(--surface); border: 1px solid var(--line);
    border-left: 3px solid var(--accent); border-radius: 8px;
}
.go-counts { margin: .5rem 0 0; }

/* 19x19 at 520px is a 26px cell, which is usable with a mouse and tight on a
   phone. The coordinates are the first thing to give up. */
@media (max-width: 420px) {
    .go-cols, .go-nums { font-size: .5rem; }
    .go-frame { padding: .3rem; gap: .1rem; }
}

@media (prefers-color-scheme: dark) {
    /* The board stays wood in both schemes, like the hex table and the cards:
       a goban is not a surface that follows a theme. Only the shadow is
       softened so it does not glow against a dark page. */
    .go-frame { box-shadow: inset 0 2px 10px rgba(60, 35, 12, .35), 0 2px 8px rgba(0,0,0,.5); }
}

/* ---- Crosstiles (plan_crosstiles/07) -------------------------------------
 *
 * GENERIC NAMES ARE THE TRAP HERE. Everything is prefixed `ct-` and the
 * game's own colours are tokens scoped to `.ct` rather than added to :root,
 * so a premium square cannot repaint another game's board.
 *
 * NO GEOMETRY COMES FROM THE MARKUP. A game page is served under
 * `default-src 'self'` with no `style-src`, so an inline style is discarded:
 * the board is a grid here and the squares are placed by document order.
 */
.ct { --ct-tw: #c0392b; --ct-dw: #e08a7a; --ct-tl: #2874a6; --ct-dl: #85c1e9;
      --ct-start: #b8860b; --ct-tile: #efe2c0; --ct-tile-ink: #2b2317;
      --ct-square: var(--panel, #f4f1e8); }
:root:not([data-theme="light"]) .ct {
  --ct-tw: #8f2f24; --ct-dw: #a8635a; --ct-tl: #1f5578; --ct-dl: #4a7f9e;
  --ct-start: #8a6508; --ct-tile: #d9c9a3; --ct-tile-ink: #211b12;
  --ct-square: var(--panel, #22201c); }

.ct-board { display: grid; grid-template-columns: repeat(15, 1fr);
  gap: 1px; width: min(100%, 34rem); margin: 0 auto; aspect-ratio: 1;
  background: var(--line, #b9b2a2); padding: 1px; border-radius: 6px; }

.ct-cell { position: relative; display: flex; align-items: center;
  justify-content: center; padding: 0; border: 0; border-radius: 2px;
  background: var(--ct-square); color: inherit; font: inherit;
  font-weight: 600; line-height: 1; cursor: default; min-width: 0; }
.ct-cell:not(:disabled) { cursor: pointer; }

.ct-tw { background: var(--ct-tw); color: #fff; }
.ct-dw { background: var(--ct-dw); }
.ct-tl { background: var(--ct-tl); color: #fff; }
.ct-dl { background: var(--ct-dl); }
.ct-start { background: var(--ct-start); color: #fff; }

/* A COVERED SQUARE IS A TILE, whatever premium is under it. */
.ct-filled { background: var(--ct-tile); color: var(--ct-tile-ink);
  box-shadow: inset 0 -2px 0 rgba(0,0,0,.18); }
.ct-draft { outline: 2px solid var(--accent); outline-offset: -2px; }
.ct-last { box-shadow: inset 0 0 0 2px var(--accent); }

/* A BLANK IS VISIBLY NOT A NATURAL TILE, AT A GLANCE AND FOR EVER. It has
 * no value in the corner and its letter is outlined rather than solid, so a
 * player reading the board a week later can see which S is worth nothing. */
.ct-blank .ct-letter { color: transparent; -webkit-text-stroke: 1px var(--ct-tile-ink);
  text-stroke: 1px var(--ct-tile-ink); }
.ct-blank { background: var(--ct-tile); border: 1px dashed var(--ct-tile-ink); }

.ct-letter { font-size: clamp(.55rem, 1.9vw, 1rem); }
.ct-pts { position: absolute; right: 1px; bottom: 0; font-size: .5rem;
  font-weight: 400; opacity: .75; }
.ct-prem { font-size: clamp(.4rem, 1.1vw, .6rem); font-weight: 700;
  letter-spacing: -.02em; opacity: .9; }

/* FIFTEEN COLUMNS ON A 375px PHONE IS 25px A SQUARE, which is legible for a
 * letter and not for a letter plus its value. The value goes; the rack keeps
 * it at every width, and a tap on a square says what it is worth. */
@media (max-width: 420px) { .ct-board .ct-pts { display: none; } }

.ct-rack { display: flex; flex-wrap: wrap; gap: .35rem; justify-content: center;
  margin: .6rem auto; }
.ct-tile { position: relative; width: 2.6rem; height: 2.6rem; border: 0;
  border-radius: 4px; background: var(--ct-tile); color: var(--ct-tile-ink);
  font: inherit; font-weight: 700; font-size: 1.1rem; cursor: pointer;
  box-shadow: 0 2px 0 rgba(0,0,0,.2); }
.ct-tile .ct-pts { font-size: .6rem; }
.ct-tile-blank { border: 1px dashed var(--ct-tile-ink); }
.ct-tile.ct-held { outline: 2px solid var(--accent); outline-offset: 2px;
  transform: translateY(-3px); }
.ct-tile.ct-used { opacity: .35; }
.ct-tile.ct-dumping { outline: 2px dashed var(--peg-red, #c0392b); outline-offset: 2px; }
.ct-tile[hidden] { display: none !important; }

.ct-english { margin: .2rem 0; text-align: center; font-size: .85rem; }
.ct-bag { text-align: center; font-size: .85rem; }

.ct-picker { display: flex; flex-wrap: wrap; gap: .25rem; justify-content: center;
  margin: .5rem auto; max-width: 30rem; }
.ct-picker p { flex: 1 0 100%; text-align: center; margin: 0 0 .2rem; }
.ct-pick { padding: .3rem .5rem; min-width: 2rem; }
/* `[hidden]` has to outrank `.button`'s display, exactly as .hex-swap does:
 * an author display beats the user agent's rule for [hidden], and the picker
 * sat there offering letters with nothing to put them on. */
.ct-picker[hidden] { display: none !important; }
/* The shuffle's display order. Seven positions, one class each: a class
 * rather than `style.order` so that nothing on this page depends on an
 * inline style surviving. */
.ct-o0 { order: 0; } .ct-o1 { order: 1; } .ct-o2 { order: 2; } .ct-o3 { order: 3; }
.ct-o4 { order: 4; } .ct-o5 { order: 5; } .ct-o6 { order: 6; } .ct-o7 { order: 7; }

/* ---- crater (plan_crater/08) ---------------------------------------------
   The field is a canvas the client draws; nothing here paints a cell, a
   character or the sky, and the canvas reads its team colours back out of
   the site's tokens with getComputedStyle. What this block owns is the frame
   round the field, the server's silhouette that stands in for it with
   scripting off, the controls under a thumb, and the ring around Fire that
   is the turn clock. Every class starts crater-; the prototype's own
   classes (layer, key, glyph, name, charging, late) are scoped under
   .crater. Every colour is a token.
   ------------------------------------------------------------------------ */
.crater { margin: 0 0 .8rem; }
.crater .crater-svg { display: block; width: 100%; height: auto; margin: 0 auto .6rem; border-radius: 12px;
                      box-shadow: 0 3px 10px var(--shadow); background: var(--surface-2); }
.crater .crater-svg .crater-sky { fill: var(--surface-2); }
.crater .crater-svg .crater-ground { fill: var(--walnut); }
.crater .crater-svg .crater-body rect { fill: var(--peg-red); stroke: var(--walnut-deep); stroke-width: 1; }
.crater .crater-svg .crater-body.seat-2 rect { fill: var(--peg-blue); }
.crater .crater-svg .crater-body.seat-3 rect { fill: var(--peg-green); }
.crater .crater-svg .crater-body.seat-4 rect { fill: var(--peg-amber); }
.crater .crater-svg .crater-body text { fill: var(--ink); font-family: var(--body); font-weight: 700; }
.crater .crater-svg .crater-grave { fill: var(--line); }
.crater .crater-field { display: none; position: relative; width: 100%; aspect-ratio: 2 / 1; touch-action: none;
                        border-radius: 12px; overflow: hidden; background: var(--surface-2); box-shadow: 0 10px 28px var(--shadow);
                        user-select: none; -webkit-user-select: none; cursor: crosshair; }
.crater .crater-field:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.crater .layer { position: absolute; inset: 0; display: block; }
.crater.scripted .crater-field { display: block; }
.crater.scripted .crater-svg { display: none; }
.crater .crater-summary { margin: 0 0 .4rem; }
.crater .crater-teams { list-style: none; display: flex; flex-wrap: wrap; gap: .3rem 1.4rem; margin: .4rem 0 .6rem; padding: 0; }
.crater .crater-team { display: inline-flex; align-items: center; gap: .35rem; }
.crater .needs-script { display: none; }
.crater.scripted .needs-script { display: inline-flex; }
.crater.scripted .needs-script[hidden] { display: none; }
.crater .crater-controls { display: grid; grid-template-columns: auto auto 1fr auto auto auto; grid-template-areas: "pad jump tray rival recentre fire" "armed armed armed armed armed armed";
                           align-items: center; gap: 6px 14px; padding: 8px 0; }
/* a held button must never hand its press to the browser: no callout, no
   selection, no scroll, no long-press menu, or a hold on Fire ends early */
.crater .crater-fire, .crater .crater-key, .crater .crater-jump, .crater .crater-tray button, .crater .crater-recentre, .crater .crater-rival {
    touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
.crater .crater-armed { grid-area: armed; margin: 0; font-size: .9rem; color: var(--muted); min-height: 1.2em; }
.crater .crater-armed:empty { display: none; }
.crater .crater-pad { grid-area: pad; gap: 6px; }
.crater .crater-key, .crater .crater-jump { width: 56px; height: 56px; font-size: 20px; border-radius: 12px; padding: 0; }
.crater .crater-jump { grid-area: jump; width: auto; padding: 0 16px; font-size: .95rem; }
.crater .crater-tray { grid-area: tray; flex-wrap: wrap; gap: 6px; justify-content: center; }
.crater .crater-tray button { display: inline-flex; flex-direction: column; align-items: center; gap: 3px; padding: 6px 8px; min-width: 62px;
                              font: 500 .75rem/1 var(--body); color: var(--ink); background: var(--surface-2); border: 1px solid var(--line);
                              border-radius: 8px; cursor: pointer; touch-action: manipulation; }
.crater .crater-tray button[aria-pressed="true"] { outline: 3px solid var(--accent); outline-offset: -1px; background: var(--surface); }
.crater .crater-tray button:disabled { opacity: .45; cursor: default; }
.crater .crater-tray button[data-left]::after { content: attr(data-left); font-size: .65rem; color: var(--muted); }
.crater .crater-tray .key { font-size: .6rem; color: var(--muted); }
.crater .crater-tray .glyph { width: 22px; height: 14px; border-radius: 3px; background: var(--ink); opacity: .8; }
.crater .crater-tray .glyph-bazooka { border-radius: 7px 3px 3px 7px; }
.crater .crater-tray .glyph-grenade { width: 14px; height: 14px; border-radius: 50%; }
.crater .crater-tray .glyph-shotgun { height: 5px; border-radius: 2px; }
.crater .crater-tray .glyph-cluster { width: 16px; height: 16px; border-radius: 50%; box-shadow: 8px -3px 0 -3px var(--ink), -8px -3px 0 -3px var(--ink); }
.crater .crater-tray .glyph-dynamite { width: 8px; height: 18px; border-radius: 2px; background: var(--danger); }
.crater .crater-recentre { grid-area: recentre; width: 44px; height: 44px; font-size: 18px; border-radius: 50%; padding: 0; }
.crater .crater-rival { grid-area: rival; width: 44px; height: 44px; font-size: 18px; border-radius: 50%; padding: 0; }
.crater .crater-fire-wrap { grid-area: fire; position: relative; width: 84px; height: 84px; }
.crater .crater-fire-wrap svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); pointer-events: none; }
.crater .crater-track { fill: none; stroke: var(--line); stroke-width: 5; }
.crater .crater-ring { fill: none; stroke: var(--accent); stroke-width: 5; stroke-linecap: round; transition: stroke-dashoffset .1s linear; }
.crater .crater-ring.late { stroke: var(--danger); }
.crater .crater-fire { position: absolute; inset: 9px; border-radius: 50%; font-size: 1rem; font-weight: 700; padding: 0; }
.crater .crater-fire.charging { background: var(--danger); border-color: var(--danger); transform: scale(.96); }
.crater .crater-fire:disabled { opacity: .45; cursor: default; }
.crater .crater-controls[data-busy="1"] .needs-script { opacity: .55; }
.crater .crater-keys { margin: .2rem 0 .6rem; font-size: .85rem; }
.crater .crater-weapons { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: .3rem 1rem; margin: .6rem 0 0; }
.crater .crater-weapon dt { font-weight: 600; }
.crater .crater-weapon dd { margin: 0; color: var(--muted); font-size: .9rem; }
@media (max-width: 640px) and (orientation: portrait), (max-width: 640px) {
    .crater .crater-field { aspect-ratio: auto; height: clamp(300px, 56vh, 620px); border-radius: 8px; }
    .crater .crater-controls { position: sticky; bottom: 0; background: var(--ground); border-top: 1px solid var(--line); padding: 6px 0; z-index: 2;
                               grid-template-columns: auto 1fr auto; grid-template-areas: "tray tray tray" "armed armed armed" "pad jump fire"; gap: 6px 8px; }
    .crater .crater-tray { justify-content: flex-start; gap: 4px; }
    .crater .crater-tray button { min-width: 0; padding: 4px 6px; }
    .crater .crater-tray .name { display: none; }
    .crater .crater-key, .crater .crater-jump { width: 60px; height: 60px; border-radius: 14px; }
    .crater .crater-jump { width: auto; padding: 0 14px; }
    .crater .crater-recentre { position: absolute; right: 8px; bottom: 122px; z-index: 3; opacity: .85; }
    .crater .crater-rival { position: absolute; right: 60px; bottom: 122px; z-index: 3; opacity: .85; }
    .crater .crater-armed { font-size: .85rem; }
    .crater .crater-fire-wrap { width: 76px; height: 76px; justify-self: end; }
    .crater .crater-keys { display: none; }
}
@media (prefers-reduced-motion: reduce) {
    .crater .crater-ring { transition: none; }
    .crater .crater-fire.charging { transform: none; }
}

/* ---- the rink: curling and bowls (plan_curling_bowls/07) ------------------------
   One block for both games: `.rink` is the section, `.curling` or `.bowls`
   beside it says which surface the painter draws. The surface is ten times
   longer than it is wide, so the stack is a WINDOW on it, square on a desk
   and taller than wide on a phone (the playing end, hog line to back line, is
   about 8.7 m by 4.75 m); the strip beside it is the whole length. Sizing is
   `width: min(100%, Npx)` and an aspect ratio, never max-content, as every
   board on the site learned the hard way. */
.rink { margin: 0 0 .8rem;
    --ice: #e6eef3;
    --boards: #5a3a24;
    --boards-deep: #3d2616;
    --mark: #2b2b2b;
    --hog: #8c2b2b;
    --house-blue: #2f5d9e;
    --house-red: #c23b2c;
    --house-white: #f7f5ef;
    --grass: #4f8a3f;
    --grass-2: #579545;
    --ditch: #3b2a1a;
    --bank: #a89a7a;
    --string: #f4f0e6;
}
:root:not([data-theme="light"]) .rink {
    --ice: #d6e0e8;
    --boards: #4d3421;
    --grass: #3f7433;
    --grass-2: #467d3a;
    --bank: #6b6250;
}
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .rink {
    --ice: #e6eef3;
    --boards: #5a3a24;
    --grass: #4f8a3f;
    --grass-2: #579545;
    --bank: #a89a7a;
} }
:root[data-theme="dark"] .rink {
    --ice: #d6e0e8;
    --boards: #4d3421;
    --grass: #3f7433;
    --grass-2: #467d3a;
    --bank: #6b6250;
}
.rink .rink-svg { display: block; width: min(100%, 520px); height: auto; margin: 0 auto .6rem;
                  border-radius: 10px; box-shadow: 0 3px 10px var(--shadow); background: var(--boards); }
.rink.bowls .rink-svg { background: var(--bank); }
.rink .rink-svg .ice { fill: var(--ice); }
.rink .rink-svg .house-12 { fill: var(--house-blue); }
.rink .rink-svg .house-8 { fill: var(--house-white); }
.rink .rink-svg .house-4 { fill: var(--house-red); }
.rink .rink-svg .button { fill: var(--house-white); }
.rink .rink-svg .mark { stroke: var(--mark); stroke-width: 20; stroke-opacity: .55; }
.rink .rink-svg .hog { stroke: var(--hog); stroke-width: 100; }
.rink .rink-svg .bank { fill: var(--bank); }
.rink .rink-svg .grass { fill: var(--grass); }
.rink .rink-svg .ditch { fill: var(--ditch); }
.rink .rink-svg .string { stroke: var(--string); stroke-width: 12; stroke-opacity: .8; }
.rink .rink-svg .mat { fill: #17161a; stroke: var(--string); stroke-width: 8; }
.rink .rink-frame { display: none; gap: 8px; align-items: flex-start; justify-content: center; margin: 0 auto .6rem; width: min(100%, 660px); }
.rink .strip { display: block; width: 40px; flex: 0 0 40px; border-radius: 6px; background: var(--boards); }
.rink.bowls .strip { background: var(--bank); }
.rink .rink-stack { position: relative; flex: 1 1 0; min-width: 0; max-width: 600px; aspect-ratio: 1 / 1;
                    touch-action: none; border-radius: 10px; overflow: hidden; background: var(--boards);
                    box-shadow: 0 10px 28px var(--shadow); user-select: none; -webkit-user-select: none; }
.rink.bowls .rink-stack { background: var(--bank); }
.rink .rink-stack:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.rink .layer { position: absolute; inset: 0; display: block; }
.rink .layer-top { cursor: crosshair; }
.rink.scripted .rink-frame { display: flex; }
.rink.scripted .rink-svg, .rink.scripted .stone-list { display: none; }
.rink .rink-summary { margin: 0 0 .4rem; }
.rink .needs-script { display: none; }
.rink.scripted .needs-script { display: inline-flex; }
.rink.scripted .needs-script[hidden] { display: none; }
.rink .rink-controls { display: flex; flex-wrap: wrap; gap: .5rem .8rem; align-items: center; }
.rink .placement, .rink .hands, .rink .sweep-call { flex-wrap: wrap; gap: .4rem; align-items: center; }
.rink .hands button { display: inline-flex; flex-direction: column; align-items: flex-start; padding: .3rem .6rem; border: 1px solid var(--line);
                      border-radius: 8px; background: var(--surface); color: inherit; cursor: pointer; font: inherit; line-height: 1.15; }
.rink .hands button[aria-pressed="true"] { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent) inset; }
.rink .hands .hand-name { font-weight: 600; }
.rink .hands .hand-hint { font-size: .78rem; color: var(--muted); }
.rink .fine { flex: 1 1 160px; min-width: 120px; height: 38px; border: 1px solid var(--line); border-radius: 8px;
              background: var(--surface); align-items: center; justify-content: center; color: var(--muted);
              font-size: .85rem; cursor: ew-resize; touch-action: none; user-select: none; -webkit-user-select: none; }
.rink .fine:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.rink .power { align-items: center; gap: 6px; font-size: .9rem; }
.rink .power input { width: 150px; accent-color: var(--accent); }
.rink .guide { align-items: center; gap: 6px; font-size: .9rem; }
.rink .guide input { accent-color: var(--accent); }
.rink .rink-controls[data-busy="1"] .needs-script { opacity: .55; }
.rink .scorecard-wrap { overflow-x: auto; margin-top: .8rem; }
.rink .scorecard { border-collapse: collapse; font-size: .9rem; min-width: 60%; }
.rink .scorecard caption { text-align: left; padding-bottom: .3rem; }
.rink .scorecard th, .rink .scorecard td { text-align: center; padding: .25rem .4rem; border-bottom: 1px solid var(--line); white-space: nowrap; }
.rink .scorecard th[scope="row"] { text-align: left; }
.rink .scorecard th.current { color: var(--accent); text-decoration: underline; }
.rink .scorecard tr.on-turn th[scope="row"] { color: var(--accent); }
.rink .scorecard td[data-total] { font-weight: 600; }
.rink .ends { margin: .4rem 0 0; padding-left: 1.2rem; font-size: .9rem; }
@media (max-width: 640px) {
    .rink .rink-stack { aspect-ratio: 3 / 4; }
    .rink .strip { width: 28px; flex-basis: 28px; }
    .rink .fine { display: none !important; }
    .rink .rink-controls { position: sticky; bottom: 0; background: var(--ground); border-top: 1px solid var(--line);
                           padding: 6px 0; z-index: 2; gap: 8px 12px; }
    .rink .power input { width: 120px; }
}

/* ---- Maze Race (plan_maze_race/07) ------------------------------------------
   One block, everything under .maze or .mz-*. The board is 225 spans in a
   square grid, both axes 1fr tracks and aspect-ratio 1 (maker-breaker's note:
   rows left to auto grow a row at a time); the width is min(100%, cap) and
   never max-content (a max-content board makes the .game column that wide and
   a phone scrolls sideways). The fog is classes: an unknown cell is the hole
   colour and draws no wall; a known cell is paper and draws its north and
   west walls, plus its east or south wall where the neighbour on that side is
   still unknown (edge-e, edge-s), so an interior wall between two known
   cells is drawn once and the seen edge of a corridor still shows against the
   fog. The token is positioned by data-col and data-row through thirty static
   rules and its transition takes its duration from data-run, one step a cell:
   no inline style anywhere. Seat colours are the site's four, and a runner
   is never told apart by colour alone: the strip carries the numeral. */
.maze { display: grid; gap: .6rem; width: min(100%, 520px, calc(100vh - 14rem)); margin: 0 auto;
        /* an RTL locale would mirror the maze while the cell names stayed correct: the board is a map */
        direction: ltr; }
@media (orientation: landscape) and (max-height: 520px) {
  .maze { width: min(100%, 520px, calc(100vh - 6rem)); }
}
.mz-board {
--mz-n: 15;
            position: relative; display: grid; grid-template-columns: repeat(15, 1fr); grid-template-rows: repeat(15, 1fr);
            gap: 0; aspect-ratio: 1; background: var(--hole); border-radius: 6px; overflow: hidden;
            border-right: 2px solid var(--ink); border-bottom: 2px solid var(--ink);
            box-shadow: 0 3px 10px var(--shadow); touch-action: none; user-select: none; -webkit-user-select: none; }
.mz-board[data-size="5"] {
--mz-n: 5;
            grid-template-columns: repeat(5, 1fr); grid-template-rows: repeat(5, 1fr); }
.mz-cell { min-width: 0; min-height: 0; box-sizing: border-box; background: var(--hole);
           border-top: 2px solid transparent; border-left: 2px solid transparent; }
.mz-cell.known { background: var(--surface); border-top-color: var(--surface); border-left-color: var(--surface); }
.mz-cell.known.wall-n { border-top-color: var(--ink); }
.mz-cell.known.wall-w { border-left-color: var(--ink); }
.mz-cell.known.edge-e { box-shadow: inset -2px 0 0 var(--ink); }
.mz-cell.known.edge-s { box-shadow: inset 0 -2px 0 var(--ink); }
.mz-cell.known.edge-e.edge-s { box-shadow: inset -2px 0 0 var(--ink), inset 0 -2px 0 var(--ink); }
.mz-cell.walked { background: var(--surface-2); }
.mz-cell.walked.known { border-top-color: var(--surface-2); border-left-color: var(--surface-2); }
.mz-cell.walked.known.wall-n { border-top-color: var(--ink); }
.mz-cell.walked.known.wall-w { border-left-color: var(--ink); }
.mz-cell.exit { background: var(--brass); }
.mz-cell.exit.known { border-top-color: var(--brass); border-left-color: var(--brass); }
.mz-cell.exit.known.wall-n { border-top-color: var(--ink); }
.mz-cell.exit.known.wall-w { border-left-color: var(--ink); }
.mz-cell.start.known { background: var(--surface-2); }
.mz-token {
--mz-run: 0ms;
            position: absolute; left: 0; top: 0; width: calc(100% / var(--mz-n)); height: calc(100% / var(--mz-n));
            pointer-events: none; transition: left var(--mz-run) linear, top var(--mz-run) linear; }
.mz-token::after { content: ''; position: absolute; inset: 22%; border-radius: 50%; background: var(--peg-red);
                   box-shadow: 0 1px 3px var(--shadow); }
.mz-token[data-col="0"] { left: 0; }                                   .mz-token[data-row="0"] { top: 0; }
.mz-token[data-col="1"] { left: calc(1 * 100% / var(--mz-n)); }       .mz-token[data-row="1"] { top: calc(1 * 100% / var(--mz-n)); }
.mz-token[data-col="2"] { left: calc(2 * 100% / var(--mz-n)); }       .mz-token[data-row="2"] { top: calc(2 * 100% / var(--mz-n)); }
.mz-token[data-col="3"] { left: calc(3 * 100% / var(--mz-n)); }       .mz-token[data-row="3"] { top: calc(3 * 100% / var(--mz-n)); }
.mz-token[data-col="4"] { left: calc(4 * 100% / var(--mz-n)); }       .mz-token[data-row="4"] { top: calc(4 * 100% / var(--mz-n)); }
.mz-token[data-col="5"] { left: calc(5 * 100% / var(--mz-n)); }       .mz-token[data-row="5"] { top: calc(5 * 100% / var(--mz-n)); }
.mz-token[data-col="6"] { left: calc(6 * 100% / var(--mz-n)); }       .mz-token[data-row="6"] { top: calc(6 * 100% / var(--mz-n)); }
.mz-token[data-col="7"] { left: calc(7 * 100% / var(--mz-n)); }       .mz-token[data-row="7"] { top: calc(7 * 100% / var(--mz-n)); }
.mz-token[data-col="8"] { left: calc(8 * 100% / var(--mz-n)); }       .mz-token[data-row="8"] { top: calc(8 * 100% / var(--mz-n)); }
.mz-token[data-col="9"] { left: calc(9 * 100% / var(--mz-n)); }       .mz-token[data-row="9"] { top: calc(9 * 100% / var(--mz-n)); }
.mz-token[data-col="10"] { left: calc(10 * 100% / var(--mz-n)); }     .mz-token[data-row="10"] { top: calc(10 * 100% / var(--mz-n)); }
.mz-token[data-col="11"] { left: calc(11 * 100% / var(--mz-n)); }     .mz-token[data-row="11"] { top: calc(11 * 100% / var(--mz-n)); }
.mz-token[data-col="12"] { left: calc(12 * 100% / var(--mz-n)); }     .mz-token[data-row="12"] { top: calc(12 * 100% / var(--mz-n)); }
.mz-token[data-col="13"] { left: calc(13 * 100% / var(--mz-n)); }     .mz-token[data-row="13"] { top: calc(13 * 100% / var(--mz-n)); }
.mz-token[data-col="14"] { left: calc(14 * 100% / var(--mz-n)); }     .mz-token[data-row="14"] { top: calc(14 * 100% / var(--mz-n)); }
.mz-token[data-run="1"] { --mz-run: 120ms; }   .mz-token[data-run="2"] { --mz-run: 240ms; }   .mz-token[data-run="3"] { --mz-run: 360ms; }
.mz-token[data-run="4"] { --mz-run: 480ms; }   .mz-token[data-run="5"] { --mz-run: 600ms; }   .mz-token[data-run="6"] { --mz-run: 720ms; }
.mz-token[data-run="7"] { --mz-run: 840ms; }   .mz-token[data-run="8"] { --mz-run: 960ms; }   .mz-token[data-run="9"] { --mz-run: 1080ms; }
.mz-token[data-run="10"] { --mz-run: 1200ms; } .mz-token[data-run="11"] { --mz-run: 1320ms; } .mz-token[data-run="12"] { --mz-run: 1440ms; }
.mz-token[data-run="13"] { --mz-run: 1560ms; } .mz-token[data-run="14"] { --mz-run: 1680ms; }
.mz-said { display: block; min-height: 1.3rem; text-align: center; }
.mz-said:empty { display: none; }
.mz-keys { display: grid; grid-template-columns: repeat(3, 3.4rem); grid-template-rows: repeat(2, 3.4rem); gap: .3rem; justify-content: center; }
.mz-key { font-size: 1.5rem; line-height: 1; padding: 0; min-width: 0; }
.mz-key[data-dir="N"] { grid-column: 2; grid-row: 1; }
.mz-key[data-dir="W"] { grid-column: 1; grid-row: 2; }
.mz-key[data-dir="S"] { grid-column: 2; grid-row: 2; }
.mz-key[data-dir="E"] { grid-column: 3; grid-row: 2; }
.mz-key:disabled { opacity: .35; }
.mz-key.hinted { outline: 3px solid var(--brass); outline-offset: 2px; }
.mz-key:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.mz-ready { display: flex; gap: .6rem; align-items: center; justify-content: center; flex-wrap: wrap; }
/* an author display beats the user agent's [hidden] rule (hex.js's lesson): the panel must actually go */
.mz-ready[hidden] { display: none; }
.mz-count { display: block; text-align: center; font: 600 1.4rem/1 var(--display); color: var(--accent); min-height: 1.4rem; }
.mz-count:empty { display: none; }
.mz-heading { font: 600 .8rem/1.2 var(--body); letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin: .4rem 0 0; }
.mz-runners { list-style: none; margin: 0; padding: 0; display: grid; gap: .3rem; }
.mz-runner { display: flex; gap: .5rem; align-items: center; }
.mz-runner.you .mz-runner-text { font-weight: 600; }
.mz-runner[data-out="1"] .mz-runner-text { color: var(--muted); }
.mz-runner .swatch.seat-1 { background: var(--brass); }
.mz-runner .swatch.seat-2 { background: var(--peg-blue); }
.mz-runner .swatch.seat-3 { background: var(--peg-green); }
.mz-runner .swatch.seat-4 { background: var(--peg-amber); }
.mz-blind { margin: 0; font-size: .85rem; }
@media (prefers-reduced-motion: reduce) {
  .mz-token { transition: none; }
}

/* ---- bowling (plan_bowling/07) --------------------------------------------------
   Minigolf's shape: the server's SVG of the deck until the client sets .scripted,
   then the canvas stack; the scorecard is HTML and stays. EVERY TOKEN IS SCOPED
   on .bowling and named --bl-*, because Yacht's dice rendered blank when a
   generic --pip turned out to be .dom-tile's: the painter reads these off the
   stack's computed style, so both pictures take the same colours in both themes,
   and nothing else on the site can want a name like --bl-kickback. */
.bowling {
  margin: 0 0 .8rem;
  --bl-wood: #d9b27a;
  --bl-wood-deep: #c39a63;
  --bl-deck: #c9a06a;
  --bl-gutter: #3a2e26;
  --bl-gutter-lip: #5a4a3c;
  --bl-pit: #120e0b;
  --bl-kickback: #4a3b30;
  --bl-approach: #b9a58a;
  --bl-mask: #2b2420;
  --bl-foul: #b8262a;
  --bl-mark: rgba(60,40,20,.75);
  --bl-board: rgba(70,45,20,.16);
  --bl-oil: rgba(255,255,255,.22);
  --bl-inset: rgba(20,14,10,.78);
  --bl-pin: #f5f1e8;
  --bl-pin-down: #9c948a;
  --bl-spot: rgba(40,25,10,.35);
}
:root:not([data-theme="light"]) .bowling { --bl-wood: #b8925c; --bl-wood-deep: #9c7a4a; --bl-deck: #a8804f; --bl-approach: #8e7b64; --bl-mask: #1a1512; --bl-oil: rgba(255,255,255,.14); }
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .bowling { --bl-wood: #d9b27a; --bl-wood-deep: #c39a63; --bl-deck: #c9a06a; --bl-approach: #b9a58a; --bl-mask: #2b2420; --bl-oil: rgba(255,255,255,.22); } }
:root[data-theme="dark"] .bowling { --bl-wood: #b8925c; --bl-wood-deep: #9c7a4a; --bl-deck: #a8804f; --bl-approach: #8e7b64; --bl-mask: #1a1512; --bl-oil: rgba(255,255,255,.14); }
.bowling .lane-line { margin: 0 0 .4rem; font-weight: 600; }
.bowling .deck-svg { display: block; width: min(100%, 360px); height: auto; margin: 0 auto .6rem;
                     border-radius: 10px; box-shadow: 0 3px 10px var(--shadow); background: var(--bl-mask); }
.bowling .deck-svg .pit { fill: var(--bl-pit); }
.bowling .deck-svg .gutter { fill: var(--bl-gutter); }
.bowling .deck-svg .deck { fill: var(--bl-deck); }
.bowling .deck-svg .kickback { fill: var(--bl-kickback); }
.bowling .deck-svg .spot { fill: var(--bl-spot); }
.bowling .deck-svg .pin-down { fill: var(--bl-pin-down); }
.bowling .deck-svg .pin circle { fill: var(--bl-pin); stroke: #b8a892; stroke-width: 4; }
.bowling .deck-svg .pin text { font-family: var(--body); font-weight: 700; fill: #3a2e26; }
.bowling .lane-stack { display: none; position: relative; width: min(100%, 560px); margin: 0 auto .6rem;
                       touch-action: none; border-radius: 10px; overflow: hidden; background: var(--bl-mask);
                       box-shadow: 0 10px 28px var(--shadow); user-select: none; -webkit-user-select: none; }
.bowling .lane-stack:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.bowling .layer { position: absolute; inset: 0; display: block; }
.bowling .layer-top { cursor: crosshair; }
.bowling.scripted .lane-stack { display: block; }
.bowling.scripted .deck-svg { display: none; }
.bowling .lane-status { margin: 0 0 .4rem; }
.bowling .needs-script { display: none; }
.bowling.scripted .needs-script { display: inline-flex; }
.bowling.scripted .needs-script[hidden] { display: none; }
.bowling .readout { align-items: center; padding: 0 .4rem; font-size: .85rem; color: var(--muted); white-space: nowrap; }
.bowling .bl-slider { align-items: center; gap: 6px; font-size: .9rem; }
.bowling .bl-slider input { width: 130px; accent-color: var(--accent); }
.bowling .guide { align-items: center; gap: 6px; font-size: .9rem; }
.bowling .guide input { accent-color: var(--accent); }
.bowling .bowling-controls[data-busy="1"] .needs-script { opacity: .55; }
/* the card, Rule 2b: the first ball beside the square, the second in it, three
   boxes in the tenth, the running total under them, a ring round a split */
/* contain: inline-size, because main is a flex item beside the sidebar and its
   automatic minimum is its content's min-content width: ten frames of boxes
   are wider than a phone, and without this the whole page widened to the card
   and scrolled sideways (the probe in t/bowling-05-page.t found it) */
.bowling .scorecard-wrap { overflow-x: auto; margin-top: .8rem; contain: inline-size; }
.bowling .scorecard.bowling-card { border-collapse: collapse; font-size: .9rem; min-width: 100%; }
.bowling .scorecard th, .bowling .scorecard td { text-align: center; padding: .2rem .25rem; border-bottom: 1px solid var(--line); white-space: nowrap; vertical-align: top; }
.bowling .scorecard th[scope="row"] { text-align: left; vertical-align: middle; }
.bowling .scorecard th.current { color: var(--accent); text-decoration: underline; }
.bowling .scorecard tr.on-turn th[scope="row"] { color: var(--accent); }
.bowling .scorecard td.open { background: var(--surface-2); }
.bowling .scorecard td[data-total] { font-weight: 600; vertical-align: middle; }
.bowling .bl-marks { display: flex; justify-content: flex-end; gap: 2px; min-height: 1.25rem; }
.bowling .bl-mark { display: inline-block; min-width: 1.05rem; height: 1.2rem; line-height: 1.2rem; font-size: .8rem; text-align: center; }
.bowling .bl-box { border: 1px solid var(--line); border-radius: 2px; }
.bowling .bl-split { border-radius: 50%; box-shadow: 0 0 0 1px var(--accent) inset; }
.bowling .bl-running { display: block; min-height: 1.1rem; font-size: .85rem; }
@media (max-width: 640px) and (orientation: portrait) {
    .bowling .readout { display: none !important; }
    /* the card fits a phone without a scroll: smaller boxes, a clipped name */
    .bowling .scorecard th, .bowling .scorecard td { padding: .15rem .1rem; }
    .bowling .scorecard th[scope="row"] { max-width: 4.2rem; overflow: hidden; text-overflow: ellipsis; }
    .bowling .bl-mark { min-width: .8rem; height: 1rem; line-height: 1rem; font-size: .7rem; }
    .bowling .bl-running { font-size: .75rem; }
    .bowling .bowling-controls { position: sticky; bottom: 0; background: var(--ground); border-top: 1px solid var(--line);
                                 padding: 6px 0; z-index: 2; gap: 8px 12px; }
    .bowling .bl-slider input { width: 100px; }
}
@media (prefers-reduced-motion: reduce) { .bowling .layer { transition: none !important; } }

/* ---- dig (plan_dig/06) -------------------------------------------------------
   Crater's field with the walk taken out: the canvas the client draws, the
   server's silhouette that stands in for it with scripting off, a score
   list of cells a digger, the tray, the recentre button and the ring around
   Fire, which here is the power gauge and not a clock. Every class starts
   dig-; the prototype's own classes (layer, key, glyph, name, charging,
   late) are scoped under .dig. Every colour is a token.
   ------------------------------------------------------------------------ */
.dig { margin: 0 0 .8rem; }
.dig .dig-svg { display: block; width: 100%; height: auto; margin: 0 auto .6rem; border-radius: 12px;
                box-shadow: 0 3px 10px var(--shadow); background: var(--surface-2); }
.dig .dig-svg .dig-sky { fill: var(--surface-2); }
.dig .dig-svg .dig-ground { fill: var(--walnut); }
.dig .dig-svg .dig-body rect { fill: var(--peg-red); stroke: var(--walnut-deep); stroke-width: 1; }
.dig .dig-svg .dig-body.seat-2 rect { fill: var(--peg-blue); }
.dig .dig-svg .dig-body.seat-3 rect { fill: var(--peg-green); }
.dig .dig-svg .dig-body.seat-4 rect { fill: var(--peg-amber); }
.dig .dig-svg .dig-body text, .dig .dig-svg .dig-cells { fill: var(--ink); font-family: var(--body); font-weight: 700; }
.dig .dig-svg .dig-grave { fill: var(--line); }
.dig .dig-field { display: none; position: relative; width: 100%; aspect-ratio: 2 / 1; touch-action: none;
                  border-radius: 12px; overflow: hidden; background: var(--surface-2); box-shadow: 0 10px 28px var(--shadow);
                  user-select: none; -webkit-user-select: none; cursor: crosshair; }
.dig .dig-field:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.dig .layer { position: absolute; inset: 0; display: block; }
.dig.scripted .dig-field { display: block; }
.dig.scripted .dig-svg { display: none; }
.dig .dig-summary { margin: 0 0 .4rem; }
.dig .dig-scores { list-style: none; display: flex; flex-wrap: wrap; gap: .3rem 1.4rem; margin: .4rem 0 .6rem; padding: 0; }
.dig .dig-score { display: inline-flex; align-items: center; gap: .35rem; }
.dig .dig-cells-n { font-weight: 700; font-variant-numeric: tabular-nums; }
.dig .needs-script { display: none; }
.dig.scripted .needs-script { display: inline-flex; }
.dig.scripted .needs-script[hidden] { display: none; }
.dig .dig-controls { display: grid; grid-template-columns: 1fr auto auto auto; grid-template-areas: "tray rival recentre fire" "armed armed armed armed";
                     align-items: center; gap: 6px 14px; padding: 8px 0; }
/* a held button must never hand its press to the browser: no callout, no
   selection, no scroll, no long-press menu, or a hold on Fire ends early */
.dig .dig-fire, .dig .dig-tray button, .dig .dig-recentre, .dig .dig-rival {
    touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
.dig .dig-armed { grid-area: armed; margin: 0; font-size: .9rem; color: var(--muted); min-height: 1.2em; }
.dig .dig-armed:empty { display: none; }
.dig .dig-tray { grid-area: tray; flex-wrap: wrap; gap: 6px; justify-content: center; }
.dig .dig-tray button { display: inline-flex; flex-direction: column; align-items: center; gap: 3px; padding: 6px 8px; min-width: 62px;
                        font: 500 .75rem/1 var(--body); color: var(--ink); background: var(--surface-2); border: 1px solid var(--line);
                        border-radius: 8px; cursor: pointer; touch-action: manipulation; }
.dig .dig-tray button[hidden] { display: none; }
.dig .dig-tray button[aria-pressed="true"] { outline: 3px solid var(--accent); outline-offset: -1px; background: var(--surface); }
.dig .dig-tray button:disabled { opacity: .45; cursor: default; }
.dig .dig-tray button[data-left]::after { content: attr(data-left); font-size: .65rem; color: var(--muted); }
.dig .dig-tray .key { font-size: .6rem; color: var(--muted); }
.dig .dig-tray .glyph { width: 22px; height: 14px; border-radius: 3px; background: var(--ink); opacity: .8; }
.dig .dig-tray .glyph-bazooka { border-radius: 7px 3px 3px 7px; }
.dig .dig-tray .glyph-grenade { width: 14px; height: 14px; border-radius: 50%; }
.dig .dig-tray .glyph-shotgun { height: 5px; border-radius: 2px; }
.dig .dig-tray .glyph-cluster { width: 16px; height: 16px; border-radius: 50%; box-shadow: 8px -3px 0 -3px var(--ink), -8px -3px 0 -3px var(--ink); }
.dig .dig-tray .glyph-dynamite { width: 8px; height: 18px; border-radius: 2px; background: var(--danger); }
.dig .dig-recentre { grid-area: recentre; width: 44px; height: 44px; font-size: 18px; border-radius: 50%; padding: 0; }
.dig .dig-rival { grid-area: rival; width: 44px; height: 44px; font-size: 18px; border-radius: 50%; padding: 0; }
.dig .dig-fire-wrap { grid-area: fire; position: relative; width: 84px; height: 84px; }
.dig .dig-fire-wrap svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); pointer-events: none; }
.dig .dig-track { fill: none; stroke: var(--line); stroke-width: 5; }
.dig .dig-ring { fill: none; stroke: var(--accent); stroke-width: 5; stroke-linecap: round; transition: stroke-dashoffset .1s linear; }
.dig .dig-ring.late { stroke: var(--accent); }
.dig .dig-fire { position: absolute; inset: 9px; border-radius: 50%; font-size: 1rem; font-weight: 700; padding: 0; }
.dig .dig-fire.charging { background: var(--danger); border-color: var(--danger); transform: scale(.96); }
.dig .dig-fire:disabled { opacity: .45; cursor: default; }
.dig .dig-controls[data-busy="1"] .needs-script { opacity: .55; }
.dig .dig-keys { margin: .2rem 0 .6rem; font-size: .85rem; }
.dig .dig-weapons { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: .3rem 1rem; margin: .6rem 0 0; }
.dig .dig-weapon dt { font-weight: 600; }
.dig .dig-weapon dd { margin: 0; color: var(--muted); font-size: .9rem; }
@media (max-width: 640px) and (orientation: portrait), (max-width: 640px) {
    .dig .dig-field { aspect-ratio: auto; height: clamp(300px, 56vh, 620px); border-radius: 8px; }
    .dig .dig-controls { position: sticky; bottom: 0; background: var(--ground); border-top: 1px solid var(--line); padding: 6px 0; z-index: 2;
                         grid-template-columns: 1fr auto; grid-template-areas: "tray tray" "armed armed" "tray fire"; gap: 6px 8px; }
    .dig .dig-tray { justify-content: flex-start; gap: 4px; }
    .dig .dig-tray button { min-width: 0; padding: 4px 6px; }
    .dig .dig-tray .name { display: none; }
    .dig .dig-recentre { position: absolute; right: 8px; bottom: 122px; z-index: 3; opacity: .85; }
    .dig .dig-rival { position: absolute; right: 60px; bottom: 122px; z-index: 3; opacity: .85; }
    .dig .dig-armed { font-size: .85rem; }
    .dig .dig-fire-wrap { width: 76px; height: 76px; justify-self: end; }
    .dig .dig-keys { display: none; }
}
@media (prefers-reduced-motion: reduce) {
    .dig .dig-ring { transition: none; }
    .dig .dig-fire.charging { transform: none; }
}

/* ---- darts (plan_darts/07) ---------------------------------------------------------
   Every class this game draws with starts `darts-`, and every token it spends is
   a :root token; t/darts-07-page.t pins both (Yacht's lesson: `.die` was Ludo's).
   The board's own colours are literals here rather than tokens, because a
   dartboard is cream and black with red and green rings in every theme, and the
   page's CSP forbids inline styles so nothing in the SVG carries a fill of its
   own. A board and a scoreboard in the site's tokens: no felt, no chalk. */
.darts { display: grid; gap: .6rem; justify-items: center; }
.darts .darts-stack { position: relative; width: min(100%, 520px); aspect-ratio: 1; touch-action: none; border-radius: 50%;
                      user-select: none; -webkit-user-select: none; cursor: crosshair; }
.darts .darts-svg { display: block; width: 100%; height: 100%; filter: drop-shadow(0 8px 18px var(--shadow)); outline: none; }
/* The board's colours are the board's in both themes; see .game-icon for the
   forced-dark opt-out. The loupe is a third of the board, icon-sized on a phone. */
.darts .darts-svg, .darts .darts-loupe { color-scheme: only light; }
.darts .darts-svg:focus-visible { outline: 3px solid var(--accent); outline-offset: 4px; border-radius: 50%; }
.darts .darts-rim { fill: #1b1714; }
.darts .darts-seg { stroke: #d8d0be; stroke-width: 7; stroke-linejoin: round; }
.darts .darts-dark { fill: #292421; }
.darts .darts-light { fill: #efe6cf; }
.darts .darts-red { fill: #b3282d; }
.darts .darts-green { fill: #2c7a49; }
.darts .darts-numbers text { fill: #f1eadb; font: 600 230px var(--body, sans-serif); }
.darts .darts-cross circle, .darts .darts-cross line { fill: none; stroke: #fff; stroke-width: 14; }
.darts .darts-cross .darts-cross-dot { fill: #fff; stroke: none; }
.darts .darts-mark circle { fill: none; stroke: var(--accent); stroke-width: 18; stroke-dasharray: 40 24; }
/* the dart: a turned barrel on a gradient with grip rings, a nylon shaft, a
   standard flight with a lit fin and a shaded fin along the crease, a blurred
   shadow on the sisal; the gradients and the blur are <defs> in darts.tmpl */
.darts .darts-shadow { fill: #000; opacity: .38; filter: url(#darts-shadow-f); }
.darts .darts-hole { fill: #0b0908; stroke: rgba(255,255,255,.35); stroke-width: 3; }
.darts .darts-barrel { fill: url(#darts-barrel-g); stroke: #2b2e33; stroke-width: 2; }
.darts .darts-grip { stroke: rgba(20,22,26,.55); stroke-width: 3; }
.darts .darts-shaft { fill: url(#darts-shaft-g); }
.darts .darts-collar { fill: #d8dbdf; stroke: #3a3d42; stroke-width: 2; }
.darts .darts-fin { fill: #d9a441; stroke: rgba(0,0,0,.45); stroke-width: 3; stroke-linejoin: round; }
.darts .darts-fin-lit { fill: url(#darts-fin-lit-g); }
.darts .darts-fin-shade { fill: url(#darts-fin-shade-g); }
.darts .darts-fin-band { fill: #7a4b22; opacity: .9; }
.darts .darts-crease { stroke: rgba(0,0,0,.5); stroke-width: 3; }
.darts .darts-dart.p2 .darts-fin { fill: #7d9cc7; }
.darts .darts-dart.p2 .darts-fin-band { fill: #2b4566; }
.darts .darts-loupe { position: absolute; top: 4%; left: 4%; width: 32%; aspect-ratio: 1; border-radius: 50%; border: 3px solid #fff;
                      box-shadow: 0 6px 18px var(--shadow); background: #1b1714; pointer-events: none; }
.darts .darts-loupe[hidden] { display: none; }
.darts .darts-loupe.right { left: auto; right: 4%; }
.darts .darts-loupe.low { top: auto; bottom: 4%; }
.darts .darts-loupe .darts-cross circle, .darts .darts-loupe .darts-cross line { stroke-width: 10; }
.darts .darts-bar { position: relative; width: min(100%, 520px); height: 38px; border-radius: 10px; background: var(--surface-2);
                    border: 1px solid var(--line); overflow: hidden; cursor: pointer; touch-action: none; }
.darts .darts-bar:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.darts .darts-zone { position: absolute; top: 0; bottom: 0; left: 45%; width: 10%; background: var(--accent); opacity: .28; }
.darts .darts-marker { position: absolute; top: 2px; bottom: 2px; width: 6px; margin-left: -3px; border-radius: 3px; background: var(--ink); left: 0; }
.darts .darts-bar.running .darts-marker { background: var(--accent); box-shadow: 0 0 8px var(--accent); }
.darts .darts-timing { position: absolute; right: 10px; top: 0; line-height: 38px; font-variant-numeric: tabular-nums; color: var(--muted); font-size: .9rem; }
.darts .darts-controls { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .8rem; }
.darts .darts-readout { font-variant-numeric: tabular-nums; min-height: 1.3em; margin: 0; }
.darts .darts-help { margin: 0; max-width: 520px; text-align: center; }
.darts .darts-noscript { margin: 0; max-width: 520px; text-align: center; }
.darts .darts-sheet { width: min(100%, 520px); display: grid; gap: .4rem; }
.darts .darts-sheet-label { font-weight: 600; }
.darts .darts-visit { display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .6rem; }
.darts .darts-chip { display: inline-block; padding: .1rem .5rem; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--line); font-size: .9rem; }
.darts .darts-chip b { font-variant-numeric: tabular-nums; }
.darts .darts-last { margin: 0; }
.darts .darts-routes { display: grid; gap: .3rem; }
.darts .darts-route { display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .8rem; }
.darts .darts-route b { font: 400 1.4rem/1 var(--display); font-variant-numeric: tabular-nums; }
.darts .darts-checkout { color: var(--muted); }
.darts .darts-target { display: inline-block; margin-left: .35rem; padding: 0 .4rem; border-radius: 6px; background: var(--surface-2); color: var(--ink); font-size: .9rem; }
.darts .needs-script { display: none; }
.game.scripted .darts .needs-script { display: block; }
.game.scripted .darts .darts-controls { display: flex; }
.game.scripted .darts .needs-script[hidden] { display: none; }
@media (max-width: 640px) {
    .darts { gap: .4rem; }
    .darts .darts-bar { height: 46px; }
    .darts .darts-timing { line-height: 46px; }
    .darts .darts-controls { position: sticky; bottom: 0; background: var(--ground); border-top: 1px solid var(--line); padding: 6px 0; z-index: 2; width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
    .darts .darts-bar.running .darts-marker { box-shadow: none; }
}

/* Oh Hell (plan_oh_hell/07). Scoped tokens under .oh-, never generic names.
   TWO BOARD TRAPS THIS SITE HAS ALREADY SPRUNG, avoided by construction.
   `width: max-content` on anything inside the .game grid makes the grid
   column that wide and a phone then scrolls sideways, so the hand and the
   scorecard are bounded by min(100%, ...). And a percentage row gap inside
   a column of aspect-ratio cells resolves against an unknown height and
   overflows, so spacing between cards is a rem gap and never a percentage.
   The widest things on this page are the TEN-CARD HAND and the nineteen-row
   scorecard; both get their own scroller rather than widening the page. */
.oh { padding: 1rem; }
.oh-trump { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .8rem; }
.oh-trump-label { font-weight: 600; }
.oh-hand-no { margin-inline-start: auto; }
.oh-seats { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: .5rem; margin-bottom: .8rem; }
.oh-seat { display: flex; align-items: baseline; gap: .4rem; padding: .4rem .6rem; border: 1px solid var(--line); border-radius: 8px; font-size: .9rem; }
.oh-seat.on-turn { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent) inset; }
.oh-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.oh-dealer { font-size: .7rem; text-transform: uppercase; color: var(--muted); }
.oh-tally { margin-inline-start: auto; font-variant-numeric: tabular-nums; font-weight: 600; }
.oh-auction { margin: .2rem 0 .8rem; }
.oh-trick { display: grid; grid-template-columns: repeat(auto-fit, minmax(56px, 1fr)); gap: .5rem; justify-items: center;
            min-height: 78px; padding: .6rem; border: 1px solid var(--line); border-radius: 10px; }
.oh-slot { display: flex; flex-direction: column; align-items: center; gap: .25rem; min-width: 46px; }
.oh-slot-name { font-size: .75rem; }

/* THE BID PAD. The forbidden number is greyed and KEPT, never hidden: a gap
   where a number should be reads as a bug, and a greyed number carrying its
   reason teaches the rule. The forced case is a sentence, not a choice. */
.oh-bidpad { margin: 1rem 0; }
.oh-bid-legend, .oh-forced { margin: 0 0 .4rem; }
.oh-forced { font-weight: 600; }
.oh-bids { display: flex; flex-wrap: wrap; gap: .4rem; }
.oh-bid { min-width: 2.6rem; padding: .5rem .6rem; font: inherit; font-variant-numeric: tabular-nums;
          background: var(--surface); color: inherit; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; }
.oh-bid:not(:disabled):hover { border-color: var(--accent); }
.oh-bid.forbidden { opacity: .4; cursor: not-allowed; text-decoration: line-through; }
.oh-bid.forced { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent) inset; }

.oh-hand { display: flex; flex-wrap: wrap; gap: .4rem; padding-top: 16px; width: min(100%, 560px); }
.oh-hand .card.trump { box-shadow: 0 0 0 2px var(--accent); }
.oh-hand .card.going { opacity: .45; }
.oh .controls { margin-top: .8rem; display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; }

.oh-card-wrap { overflow-x: auto; margin-top: 1.4rem; width: min(100%, 100%); }
.oh-card { border-collapse: collapse; font-size: .85rem; min-width: 100%; }
.oh-card th, .oh-card td { text-align: end; padding: .25rem .5rem; border-bottom: 1px solid var(--line); white-space: nowrap; }
.oh-card th:first-child, .oh-card td:first-child { text-align: start; }
.oh-card caption { text-align: start; padding-bottom: .3rem; }
.oh-card .oh-cell { font-variant-numeric: tabular-nums; }
.oh-card td.made b { color: var(--accent); }
.oh-card td.missed .oh-cell { opacity: .55; }
.oh-suit { text-align: center; }

/* ---- quartermile (plan_drag/06) -----------------------------------------------
   Every class this game draws with starts `qm-`, and every colour it spends is a
   :root token; t/qm-07-page.t pins both. There is almost nothing here, and that
   is the point: the strip, the dashboard, the tree and the cars are painted on
   the canvas by root/static/quartermile.js, which reads the tokens itself
   through getComputedStyle, so a colour lives in one place and the canvas and
   the page around it cannot disagree about it. What is left for the stylesheet
   is the box the canvas sits in, three buttons, and the reading for somebody
   whose browser will not run any of it. */
.quartermile { display: grid; gap: .6rem; justify-items: center; }

/* Landscape, because the view is a road going away from you and a tall box
   wastes every pixel of it on sky and bonnet. touch-action is none HERE and
   nowhere else: the lower half of this box is the throttle under a thumb, and
   a browser that decides a held thumb is a scroll takes the pedal away
   mid-launch. The page outside this box scrolls normally. */
.quartermile .qm-stack { position: relative; width: min(100%, 880px); aspect-ratio: 16 / 9; touch-action: none;
                         border-radius: 12px; overflow: hidden; background: var(--hole);
                         user-select: none; -webkit-user-select: none; outline: none; }
.quartermile .qm-stack:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.quartermile .qm-field { display: block; width: 100%; height: 100%; }

/* The one line the client says out loud: a slow link, a restarted run, a driver
   who dropped. Over the picture rather than under it, because under it is off
   the bottom of a phone held sideways. */
.quartermile .qm-note { position: absolute; left: 50%; transform: translateX(-50%); bottom: 8%; margin: 0;
                        max-width: 90%; padding: .35rem .7rem; border-radius: 8px; text-align: center;
                        background: var(--surface-2); color: var(--ink); border: 1px solid var(--line);
                        font-size: .9rem; pointer-events: none; }
.quartermile .qm-cannot { max-width: 60ch; text-align: center; color: var(--muted); }


.qm-lanes { list-style: none; margin: .2rem 0 0; padding: 0; display: grid; gap: .3rem; justify-items: start; }
.qm-lanes .qm-lane { display: flex; flex-wrap: wrap; gap: .4rem; align-items: baseline; }
.qm-lanes .qm-lane[data-out="1"] { opacity: .55; }
.qm-lanes .qm-lane[data-you="1"] .qm-lane-name { font-weight: 600; }
.qm-swatch { width: .9rem; height: .9rem; border-radius: 3px; border: 1px solid var(--line); background: var(--muted); }
.qm-lane.brass     .qm-swatch { background: var(--brass); }
.qm-lane.peg-blue  .qm-swatch { background: var(--peg-blue); }
.qm-lane.peg-green .qm-swatch { background: var(--peg-green); }
.qm-lane.peg-amber .qm-swatch { background: var(--peg-amber); }
.quartermile .qm-reading { max-width: 60ch; text-align: center; }

/* One table a run. It scrolls sideways inside its own box rather than making
   the page do it: six columns of numbers on a phone held sideways is wider
   than the phone, and a page that scrolls sideways under a thumb that meant to
   press the throttle is the same bug as the one touch-action fixes above. */
.qm-runs { display: grid; gap: .8rem; width: min(100%, 880px); margin-top: 1.2rem; }
.qm-run-wrap { overflow-x: auto; }
.qm-run { border-collapse: collapse; font-size: .85rem; min-width: 100%; }
.qm-run caption { text-align: left; padding-bottom: .3rem; color: var(--muted); }
.qm-run th, .qm-run td { text-align: right; padding: .25rem .5rem; border-bottom: 1px solid var(--line); white-space: nowrap;
                         font-variant-numeric: tabular-nums; }
.qm-run th:first-child, .qm-run td:first-child,
.qm-run th:nth-child(2), .qm-run td:nth-child(2) { text-align: left; }

/* Nothing on this page moves without the canvas, and the canvas asks for this
   itself: with reduced motion the lanes are drawn where the last snapshot put
   them rather than slid between two of them. Here it is the shake at the
   lights, which is a decoration and nothing else. */
@media (prefers-reduced-motion: reduce) {
    .quartermile .qm-stack { scroll-behavior: auto; }
}

/* The ready lobby, retrofitted 25 September 2026. Lightlines' block word for
   word, because it is the same panel doing the same job, and a second set of
   sizes would be a second thing to keep in step. No colour of its own: the
   button is the shared .button.primary and this only places and sizes it,
   which is what keeps this block tokens-only. */
.qm-ready {
  display: flex;
  justify-content: center;
  margin: .8rem auto 0;
  max-width: 34rem;
}
.qm-ready-button {
  min-width: 12rem;
  padding: .7rem 1.4rem;
  font-size: 1.05rem;
}
.qm-ready-button[disabled] { opacity: .55; }

/* ---- lineup -------------------------------------------------------------
   Twenty-four faces on a phone, which is the whole layout problem
   (plan_lineup/01 mark 7). Four columns of 80px and three gaps of 6px is
   338px, so six rows fit a 375px screen under the card and the pad with the
   status line still visible. Wider than that and the board pushes the pad
   off the bottom, which is where the questions are.

   min() and not a percentage: `width: max-content` on a board inside the
   .game grid makes the grid column that wide and the phone scrolls sideways.
   And the gap between rows is a gap, never a percentage: a percentage row gap
   inside a column of aspect-ratio cells resolves against an unknown height
   and the cells overflow the board. */
.lu { display: grid; gap: .6rem; justify-items: start; }
.lu-board { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px;
            width: min(100%, 338px); }

.lu-face { position: relative; aspect-ratio: 1 / 1; padding: 0; overflow: hidden;
           border: 1px solid var(--line); border-radius: 8px; background: var(--surface);
           font: inherit; color: inherit; }
.lu-face .lu-art { display: block; width: 100%; height: 100%; }
.lu-face .lu-art svg { display: block; width: 100%; height: 100%; }
.lu-name { position: absolute; left: 0; right: 0; bottom: 0; font-size: .7rem; line-height: 1.2;
           background: rgba(20, 12, 4, .5); color: #fff; padding: 1px 0; pointer-events: none; }

/* A RULED-OUT FACE IS STILL A FACE, and still named. The deduction is the
   game, so what has gone has to stay visible: the board is the working, not
   just the answer. opacity stays 1 on the button because a disabled control
   at half opacity is unreadable; the greying is on the drawing alone. */
.lu-face.out { filter: grayscale(1); }
.lu-face.out .lu-art { opacity: .22; }
.lu-face:disabled { opacity: 1; }
.lu-face.hinted { outline: 2px solid var(--brass); outline-offset: 1px; }
.lu-face:focus-visible, .lu-q:focus-visible { outline: 2px solid var(--brass); outline-offset: 1px; }

/* The pad scrolls sideways inside its own box rather than making the page do
   it: twenty chips wrapped onto four rows costs more vertical space than the
   board can spare on a phone. */
.lu-pad { display: flex; gap: 6px; overflow-x: auto; padding-bottom: .3rem;
          width: 100%; scrollbar-width: thin; }
.lu-q { flex: 0 0 auto; border: 1px solid var(--line); border-radius: 16px;
        padding: .35rem .7rem; background: var(--surface); color: var(--ink);
        font: inherit; white-space: nowrap; }
.lu-q:disabled { opacity: 1; color: var(--muted); background: var(--surface-2); }
.lu-q[data-why] { text-decoration: line-through; }
.lu-q.hinted { border-color: var(--brass); box-shadow: inset 0 0 0 1px var(--brass); }

.lu-mine { display: flex; align-items: center; gap: .6rem; }
.lu-card { width: 56px; height: 56px; flex: 0 0 auto; border: 1px solid var(--line);
           border-radius: 8px; background: var(--surface); overflow: hidden; }
.lu-mine-say { margin: 0; }

.lu-theirs summary { cursor: pointer; }
.lu-strip { display: flex; flex-wrap: wrap; gap: 4px; margin-top: .4rem; }
.lu-tile { width: 40px; height: 40px; border-radius: 6px; overflow: hidden; background: var(--surface); }
.lu-tile .lu-art { display: block; width: 100%; height: 100%; }
.lu-tile .lu-art svg { display: block; width: 100%; height: 100%; }
.lu-tile.out { filter: grayscale(1); opacity: .25; }

.lu-rounds { list-style: none; margin: 0; padding: 0; display: grid; gap: .2rem; font-size: .85rem; }
.lu-round { color: var(--muted); }
.lu-round-mine, .lu-round-theirs { font-variant-numeric: tabular-nums; }
.lu-round-theirs { color: var(--muted); }

.lu-confirm { border: 1px solid var(--line); border-radius: 10px; background: var(--surface);
              color: var(--ink); padding: 1rem; max-width: 18rem; }
.lu-confirm .lu-card { width: 96px; height: 96px; margin: 0 auto .6rem; }
.lu-confirm-say { margin: 0 0 .8rem; }
.lu-confirm-actions { display: flex; gap: .5rem; justify-content: center; }

/* ---- circuit (plan_circuit/06) --------------------------------------------
   The track is a canvas the client draws fifty times a second; nothing here
   paints a metre of tarmac. What this block owns is the frame around it, the
   pad under it, the seat list, and the two sentences for a connection that
   cannot carry a race. Every colour is a token: circuit.js reads these same
   custom properties off the page and draws the karts from --brass,
   --peg-blue, --peg-green and --peg-amber, so the palette is in one place
   and the stylesheet is the one place it is written down.
   ------------------------------------------------------------------------ */

/* THE TRACK IS DARK and LANDSCAPE. Dark for Snake's reason, that a screen
   you look into is not a page you read; landscape at 3 by 2 because a kart
   at full speed covers the short side of a square in under a second and the
   camera would be doing all the work. The canvas inside is made by
   render.js, not by the template, so this styles whatever it appends. */
.circuit-stack {
  position: relative;
  width: min(100%, 44rem);
  margin: 0 auto;
  aspect-ratio: 3 / 2;
  border: 2px solid var(--walnut);
  border-radius: .4rem;
  background: var(--hole);
  overflow: hidden;
}
.circuit-stack:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

/* The one surface a drag must not scroll the page from, because its halves
   are the steering. Scoped to the canvas: the rest of the page scrolls as
   it always did. */
.circuit-stack canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

.circuit-note {
  position: absolute;
  left: 50%;
  bottom: .6rem;
  transform: translateX(-50%);
  margin: 0;
  padding: .3rem .7rem;
  border-radius: .3rem;
  background: var(--walnut-deep);
  color: var(--card);
  font-size: .9rem;
  max-width: 90%;
  text-align: center;
}

/* The lap and the place in text. The canvas says the same thing for the
   person racing; this is what a screen reader has, and what is left when
   the canvas is a grey box because the socket never opened. */
.circuit-hud {
  max-width: 44rem;
  margin: .5rem auto 0;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  min-height: 1.4em;
}

.circuit-cannot {
  margin: .8rem auto;
  max-width: 44rem;
  padding: .6rem .8rem;
  border-left: 3px solid var(--peg-red);
  background: var(--surface-2);
}

/* THE PAD SHIPS HIDDEN. The halves of the canvas are the scheme the phone
   starts in and the pad is the other one the feel gate was given, so P
   swaps them and the owner decides which survives. Sticky at the bottom in
   portrait, where a thumb is, rather than under a track that has scrolled
   off the top. */
.circuit-pad {
  display: flex;
  gap: .5rem;
  justify-content: center;
  max-width: 44rem;
  margin: .5rem auto;
}
.circuit-key {
  flex: 1 1 0;
  max-width: 8rem;
  min-height: 3rem;
  font-size: 1.3rem;
  border: 1px solid var(--line);
  border-radius: .4rem;
  background: var(--surface);
  color: var(--ink);
  touch-action: none;
}
.circuit-key[aria-pressed="true"] { background: var(--accent); color: var(--accent-ink); }
.circuit-extra { max-width: 44rem; margin: .4rem auto; text-align: center; }

@media (max-width: 40rem) and (orientation: portrait) {
  .circuit-pad { position: sticky; bottom: .4rem; z-index: 2; }
}

.circuit-seats {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: .3rem 1.4rem;
  margin: .7rem auto;
  padding: 0;
  max-width: 44rem;
}

/* A row reads as one sentence, so it is inline content in a block and not
   a flex container: Snake learned that the hard way, where every text node
   between the spans became an anonymous flex item and the row read as four
   fragments. */
.circuit-seat { line-height: 1.5; }
.circuit-seat[data-you="1"] .circuit-seat-name { font-weight: 600; }
.circuit-seat[data-out="1"] { opacity: .65; }
.circuit-seat-lap { margin-left: .3rem; font-variant-numeric: tabular-nums; }

.circuit-swatch {
  display: inline-block;
  width: .7rem;
  height: .7rem;
  margin-right: .35rem;
  vertical-align: -1px;
  border-radius: .2rem;
  background: var(--peg-blue);
}

/* The seat order the client draws with: this seat is always the first
   colour, so your own kart is brass in every chair. */
.circuit-seat.brass     .circuit-swatch { background: var(--brass); }
.circuit-seat.peg-blue  .circuit-swatch { background: var(--peg-blue); }
.circuit-seat.peg-green .circuit-swatch { background: var(--peg-green); }
.circuit-seat.peg-amber .circuit-swatch { background: var(--peg-amber); }

.circuit-reading { max-width: 44rem; margin: .6rem auto; }
.circuit-laps { max-width: 44rem; margin-inline: auto; }

/* The race still runs live under reduced motion, because a race cannot be
   shown settled. What goes is the easing: the client stops sliding rivals
   between snapshots and stops predicting, so every kart is drawn where the
   last snapshot put it, and the countdown is words rather than digits that
   pop. That is all in circuit.js; what is here is the note, which is the
   only thing this stylesheet moves. */
@media (prefers-reduced-motion: reduce) {
  .circuit-note { transition: none; }
}


/* The ready lobby, retrofitted 25 September 2026. Lightlines' block word for
   word, because it is the same panel doing the same job, and a second set of
   sizes would be a second thing to keep in step. No colour of its own: the
   button is the shared .button.primary and this only places and sizes it,
   which is what keeps this block tokens-only. */
.circuit-ready {
  display: flex;
  justify-content: center;
  margin: .8rem auto 0;
  max-width: 34rem;
}
.circuit-ready-button {
  min-width: 12rem;
  padding: .7rem 1.4rem;
  font-size: 1.05rem;
}
.circuit-ready-button[disabled] { opacity: .55; }
/* ---- sniper (plan_sniper/06) -----------------------------------------------
   The range is a canvas the client draws fifty times a second; nothing here
   paints a plate or a hillside. What this block owns is the frame around the
   scope, the pad under it, the rival bar, the stage tables and the two
   sentences for a connection that cannot carry a live range.

   THIS BLOCK DECLARES NO PALETTE, and that is the point rather than an
   oversight. Sniper's colours are not in the stylesheet at all: render.js
   carries two full palettes, dusk and noon, as arrays of integer triples,
   because the world inside the disc is a hillside painted on a canvas and not
   site chrome. So there are no local custom properties to hide a literal in,
   the four seat colours come from the tokens that already exist, and this
   block needs no exemption from the colour rule the way bowling and the rink
   did.
   ------------------------------------------------------------------------ */

/* THE SCOPE IS A DISC IN A DARK FRAME, and square. Dark for Snake's reason,
   that a screen you look into is not a page you read; square because the
   scope shows a circle and a letterbox would crop the top and bottom of it
   for nothing. The canvas inside is made by render.js and not by the
   template, so this styles whatever it appends. */
.sn-stack {
  position: relative;
  width: min(100%, 40rem);
  margin: 0 auto;
  aspect-ratio: 1 / 1;
  border: 2px solid var(--walnut);
  border-radius: .4rem;
  background: var(--hole);
  overflow: hidden;
  /* THE DRAG SURFACE, and the listeners are on this element rather than on
     the canvas inside it, so the rule belongs here too: a drag that began on
     the frame rather than on the picture scrolled the page instead of panning
     the scope. */
  touch-action: none;
}
.sn-stack:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

/* The one surface a drag must not scroll the page from, because a drag on it
   is a pan. Scoped to the canvas: the rest of the page scrolls as it did. */
.sn-stack canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

.sn-note {
  position: absolute;
  left: 50%;
  bottom: .6rem;
  transform: translateX(-50%);
  margin: 0;
  padding: .3rem .7rem;
  border-radius: .3rem;
  background: var(--walnut-deep);
  color: var(--card);
  font-size: .9rem;
  max-width: 90%;
  text-align: center;
}

/* THE PAD STICKS TO THE BOTTOM IN PORTRAIT, because a thumb is at the bottom
   of a phone and the disc is not. `overflow-x: hidden` belongs on `html` and
   never on `body` or a sticky child stops sticking, which is what Circuit's
   probe found; it is set in the layout block and is not repeated here. */
.sn-pad {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  width: min(100%, 40rem);
  margin: .6rem auto 0;
  padding: .4rem 0;
}
.sn-dpad {
  display: grid;
  grid-template-columns: repeat(3, 2.6rem);
  grid-template-rows: repeat(3, 2.6rem);
  gap: .2rem;
}
.sn-zone {
  border: 1px solid var(--line);
  border-radius: .3rem;
  background: var(--surface-2);
  padding: 0;
  touch-action: none;
}
.sn-up { grid-column: 2; grid-row: 1; }
.sn-left { grid-column: 1; grid-row: 2; }
.sn-right { grid-column: 3; grid-row: 2; }
.sn-down { grid-column: 2; grid-row: 3; }
.sn-breath, .sn-fire { touch-action: none; }

@media (orientation: portrait) {
  .sn-pad {
    position: sticky;
    bottom: 0;
    z-index: 2;
    background: var(--surface);
  }
}

/* Plates left per seat and the points. The canvas says the same thing for the
   person shooting; this is what a screen reader has, and what is left when
   the canvas is a grey box because the socket never opened. */
.sn-rival {
  list-style: none;
  max-width: 40rem;
  margin: .6rem auto 0;
  padding: 0;
  font-variant-numeric: tabular-nums;
}
.sn-shooter {
  display: flex;
  align-items: center;
  gap: .4rem;
  flex-wrap: wrap;
  padding: .2rem 0;
  border-bottom: 1px solid var(--line);
}
.sn-shooter[data-out] { opacity: .55; }
.sn-shooter[data-you] .sn-name { font-weight: 600; }
.sn-swatch {
  width: .8rem;
  height: .8rem;
  border-radius: 50%;
  background: var(--brass);
  flex: none;
}
.sn-shooter.peg-blue .sn-swatch { background: var(--peg-blue); }
.sn-shooter.peg-green .sn-swatch { background: var(--peg-green); }
.sn-shooter.peg-amber .sn-swatch { background: var(--peg-amber); }

/* THE COUNTDOWN AFTER THE FIRST CLEAR is the whole tension of a parallel
   race, so it is the loudest thing on the page after the disc. */
.sn-grace {
  max-width: 40rem;
  margin: .4rem auto 0;
  padding: .4rem .6rem;
  border-left: 3px solid var(--accent);
  background: var(--surface-2);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.sn-reading {
  max-width: 40rem;
  margin: .5rem auto 0;
  color: var(--muted);
}

/* THE LOBBY SITS BETWEEN THE DISC AND THE PAD, where the hand already is: the
   press that starts the match is made with the same thumb that is about to
   shoot. It is centred and wide rather than tucked in a corner, because the
   range behind it is drawn and running and a small button in front of a live
   picture reads as a decoration. The disabled state is the seat that has
   already pressed, which is why it stays on the page and does not vanish. */
.sn-ready {
  display: flex;
  justify-content: center;
  margin: .8rem auto 0;
  max-width: 40rem;
}
.sn-ready-button {
  min-width: 12rem;
  padding: .7rem 1.4rem;
  font-size: 1.05rem;
}
.sn-ready-button[disabled] { opacity: .55; }

.sn-cannot {
  margin: .8rem auto;
  max-width: 40rem;
  padding: .6rem .8rem;
  border-left: 3px solid var(--peg-red);
  background: var(--surface-2);
}

/* A stage table is six columns and a phone is not six columns wide, so each
   scrolls INSIDE ITS OWN BOX. A percentage margin and never a percentage row
   gap: a gap resolves against a height nobody knows here. */
.sn-stages { max-width: 40rem; margin: 1rem auto 0; }
.sn-stage-wrap { overflow-x: auto; margin: 0 0 .8rem; }
.sn-stage {
  width: 100%;
  min-width: 22rem;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
.sn-stage caption {
  text-align: left;
  font-weight: 600;
  padding: .2rem 0;
}
.sn-stage th, .sn-stage td {
  text-align: left;
  padding: .2rem .4rem;
  border-bottom: 1px solid var(--line);
}

/* Nothing in this block animates for a reader who asked for less motion. The
   shimmer and the recoil inside the disc are stilled by render.js, which is
   told through the view rather than through the stylesheet. */
@media (prefers-reduced-motion: reduce) {
  .sn-stack, .sn-zone, .sn-shooter, .sn-grace, .sn-note {
    transition: none !important;
    animation: none !important;
  }
}

/* ---- penalties (plan_penalties/06) ------------------------------------------------
   Every class this game draws with starts `pk-`, and every token it spends is a
   :root token; t/penalties-06-page.t pins both. The picture's own colours are
   literals, because grass is green and a net is white in every theme, and the
   page's CSP forbids inline styles so nothing in the SVG carries a fill of its
   own. A goal and a tally in the site's tokens: no stadium, no bookmaker. */
.pk { display: grid; gap: .6rem; justify-items: center; }
/* the picture: the aim rectangle plus the grass, 7700 x 3200 */
.pk .pk-stack { position: relative; width: min(100%, 720px); aspect-ratio: 7700 / 3200; touch-action: none; border-radius: 10px; overflow: hidden;
                user-select: none; -webkit-user-select: none; cursor: crosshair; box-shadow: 0 8px 18px var(--shadow); }
.pk.pk-keeping .pk-stack { cursor: default; }
.pk .pk-svg { display: block; width: 100%; height: 100%; outline: none; }
.pk .pk-svg, .pk .pk-pad { color-scheme: only light; }
.pk .pk-svg:focus-visible { outline: 3px solid var(--accent); outline-offset: 4px; }
.pk .pk-sky { fill: #c9d6df; } .pk .pk-stand { fill: #8d8579; } .pk .pk-grass { fill: #4f8a3c; }
.pk .pk-line { stroke: #e8f0e4; stroke-width: 60; }
.pk .pk-netbg { fill: rgba(30,40,30,.28); } .pk .pk-net-line { stroke: rgba(255,255,255,.55); stroke-width: 8; fill: none; }
.pk .pk-frame rect { fill: #f4f2ec; stroke: #9a968c; stroke-width: 12; }
.pk .pk-zone { fill: rgba(255,255,255,.10); stroke: rgba(255,255,255,.45); stroke-width: 10; stroke-dasharray: 80 60; }
.pk .pk-zone.mine { fill: rgba(255,255,255,.28); stroke: #fff; stroke-dasharray: none; }
/* the keeper: the kit colour is the seat's, everything else is the same player */
.pk .pk-ground-shadow { fill: #000; opacity: .35; }
.pk .pk-thigh { stroke: #2a2622; }
.pk .pk-shorts { fill: #2a2622; stroke: #15120f; stroke-width: 14; stroke-linejoin: round; }
.pk .pk-sock-line { stroke: #4a453d; } .pk .pk-sock { stroke: #f2efe6; } .pk .pk-sock-band { stroke: #d9a441; }
.pk .pk-knee { fill: #f2efe6; stroke: #c9c3b6; stroke-width: 10; }
.pk .pk-boot { fill: #1c1917; stroke: #000; stroke-width: 10; stroke-linejoin: round; } .pk .pk-sole { stroke: #3a3632; stroke-width: 26; stroke-linecap: round; }
.pk .pk-stud { fill: #7a746c; } .pk .pk-boot-stripe { stroke: #f2efe6; stroke-width: 22; stroke-linecap: round; }
.pk .pk-shirt { fill: #d9a441; stroke: #2a2622; stroke-width: 16; stroke-linejoin: round; }
.pk .pk-shirt-shade { fill: url(#pk-shade); }
.pk .pk-collar { fill: #2a2622; }
.pk .pk-sleeve-line { stroke: #2a2622; } .pk .pk-sleeve { stroke: #d9a441; } .pk .pk-sleeve-fill { fill: #d9a441; }
.pk .pk-cuff { stroke: #2a2622; }
.pk .pk-glove-back { fill: #e6eaee; stroke: #2a2622; stroke-width: 14; } .pk .pk-finger { fill: #86b06e; stroke: #2a2622; stroke-width: 12; }
.pk .pk-palm { fill: #86b06e; } .pk .pk-strap { stroke: #2a2622; stroke-width: 34; stroke-linecap: round; }
.pk .pk-skin { fill: #e9c9a0; stroke: #6a4a30; stroke-width: 12; } .pk .pk-hair { fill: #3b2a1e; stroke: #241810; stroke-width: 10; }
.pk .pk-eye-white { fill: #fff; } .pk .pk-eye { fill: #1c1917; } .pk .pk-brow, .pk .pk-nose, .pk .pk-mouth { fill: none; stroke: #4a3020; stroke-width: 16; stroke-linecap: round; }
.pk .pk-number { fill: #fff; font: 700 400px var(--body, sans-serif); opacity: .92; }
.game.pk-keeper-2 .pk .pk-shirt, .game.pk-keeper-2 .pk .pk-sleeve-fill { fill: #7d9cc7; }
.game.pk-keeper-2 .pk .pk-sleeve, .game.pk-keeper-2 .pk .pk-sock-band { stroke: #7d9cc7; }
/* the ball: every revealed kick in the net, and the one in flight */
.pk .pk-ball-body { fill: #f7f5ef; stroke: #2a2622; stroke-width: 10; }
.pk .pk-ball-ring { fill: none; stroke: #d9a441; stroke-width: 22; }
.pk .pk-kick.p2 .pk-ball-ring { stroke: #7d9cc7; }
.pk .pk-ball-shadow { fill: #000; opacity: .3; }
.pk .pk-cross circle, .pk .pk-cross line { fill: none; stroke: #fff; stroke-width: 30; }
.pk .pk-cross .pk-cross-dot { fill: #fff; stroke: none; }
/* the pad: six buttons over the zones, placed by the client, no words */
.pk .pk-pad { position: absolute; inset: 0; }
.pk .pk-pad[hidden] { display: none; }
.pk .pk-zone-btn { position: absolute; margin: 0; padding: 0; border: 2px solid #fff; border-radius: 6px; background: rgba(255,255,255,.14); cursor: pointer; }
.pk .pk-zone-btn:hover, .pk .pk-zone-btn:focus-visible { background: rgba(255,255,255,.30); outline: none; }
.pk .pk-zone-btn.chosen { background: var(--accent); border-color: #fff; opacity: .85; }
/* the bar */
.pk .pk-barwrap { width: min(100%, 520px); }
.pk .pk-barwrap[hidden] { display: none; }
.pk .pk-bar { position: relative; height: 40px; border-radius: 10px; background: var(--surface-2); border: 1px solid var(--line); overflow: hidden; cursor: pointer; touch-action: none; }
.pk .pk-bar:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.pk .pk-soft { position: absolute; top: 0; bottom: 0; left: 0; width: 60%; background: var(--muted); opacity: .12; }
.pk .pk-sweet { position: absolute; top: 0; bottom: 0; left: 72%; width: 6%; background: var(--accent); opacity: .32; }
.pk .pk-sweet::after { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 2px; margin-left: -1px; background: var(--accent); opacity: .8; }
.pk .pk-marker { position: absolute; top: 2px; bottom: 2px; width: 6px; margin-left: -3px; border-radius: 3px; background: var(--ink); left: 0; }
.pk .pk-bar.running .pk-marker { background: var(--accent); box-shadow: 0 0 8px var(--accent); }
.pk .pk-power { position: absolute; right: 10px; top: 0; line-height: 40px; font-variant-numeric: tabular-nums; color: var(--muted); font-size: .9rem; }
.pk .pk-barlabels { display: flex; justify-content: space-between; font-size: .8rem; color: var(--muted); padding: 2px 4px 0; }
.pk .pk-controls { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .8rem; }
.pk .pk-controls .button[hidden] { display: none; }
.pk .pk-help { margin: 0; max-width: 520px; text-align: center; }
.pk .pk-noscript { margin: 0; max-width: 520px; text-align: center; }
/* the sheet: the tally and the kick list */
.pk .pk-sheet { width: min(100%, 520px); display: grid; gap: .4rem; }
.pk .pk-tally { display: grid; gap: .3rem; }
.pk .pk-row { display: flex; align-items: baseline; gap: .6rem; }
.pk .pk-who { font-weight: 600; }
.pk .pk-dots { display: inline-flex; gap: .3rem; align-items: center; margin-inline-start: auto; }
.pk .pk-dot { display: inline-block; width: .8rem; height: .8rem; border-radius: 50%; border: 2px solid var(--line); background: transparent; }
.pk .pk-dot.goal { background: var(--accent); border-color: var(--accent); }
.pk .pk-dot.miss { background: var(--muted); border-color: var(--muted); }
.pk .pk-dot.pending { opacity: .5; }
.pk .pk-goals { font: 400 1.4rem/1 var(--display); font-variant-numeric: tabular-nums; min-width: 1.4rem; text-align: end; }
.pk .pk-round { margin: 0; }
.pk .pk-list { margin: 0; padding-inline-start: 1.2rem; font-size: .9rem; }
.pk .pk-list li { margin: 0 0 .15rem; }
.pk .needs-script { display: none; }
.game.scripted .pk .needs-script { display: block; }
.game.scripted .pk .pk-controls { display: flex; }
.game.scripted .pk .pk-pad { display: block; }
.game.scripted .pk .needs-script[hidden] { display: none; }
@media (max-width: 640px) {
    .pk { gap: .4rem; }
    .pk .pk-bar { height: 48px; }
    .pk .pk-power { line-height: 48px; }
    .pk .pk-controls { position: sticky; bottom: 0; background: var(--ground); border-top: 1px solid var(--line); padding: 6px 0; z-index: 2; width: 100%; justify-content: center; }
}
@media (prefers-reduced-motion: reduce) {
    .pk .pk-bar.running .pk-marker { box-shadow: none; }
}

/* ---- crazy eights (plan_crazy_eights/07) -------------------------------------
   Scoped tokens under .ce-, never generic names. TWO BOARD TRAPS THIS SITE HAS
   ALREADY SPRUNG, avoided by construction: `width: max-content` on anything
   inside the .game grid makes the grid column that wide and a phone then
   scrolls sideways, so the hand is bounded by min(100%, ...) and WRAPS; and a
   percentage row gap inside a column of aspect-ratio cells resolves against an
   unknown height and overflows, so spacing between cards is a rem gap. The
   widest thing on this page is a hand that has grown on draws: the soak saw
   twenty-six cards, so the hand wraps to as many rows as it needs and never
   scrolls. The card faces are the site's .card rules. */
.ce { padding: 1rem; }
.ce-seats { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: .5rem; margin-bottom: .8rem; }
.ce-seat { display: flex; align-items: baseline; gap: .4rem; padding: .4rem .6rem; border: 1px solid var(--line); border-radius: 8px; font-size: .9rem; }
.ce-seat.on-turn { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent) inset; }
.ce-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ce-dealer { font-size: .7rem; text-transform: uppercase; color: var(--muted); }
.ce-count { margin-inline-start: auto; font-variant-numeric: tabular-nums; font-weight: 600; }
.ce-total { font-variant-numeric: tabular-nums; }

/* THE TABLE: the pile and the stock side by side, which fits at 375 px with
   room for the badge. The stock is a face-down card that is a BUTTON, enabled
   only when `legal` holds a draw; disabled it keeps its face and loses its
   cursor, so the shape of the table never changes under the fingers. */
.ce-table { display: flex; align-items: flex-start; gap: 1.2rem; flex-wrap: wrap; margin-bottom: 1rem; }
.ce-pile { display: flex; align-items: center; gap: .6rem; min-height: 78px; }
.ce-named { display: inline-flex; align-items: center; gap: .3rem; padding: .3rem .6rem; border: 1px solid var(--accent);
            border-radius: 999px; font-weight: 600; }
.ce-named-glyph { font-size: 1.2em; }
.ce-stock { display: flex; align-items: center; gap: .5rem; }
.ce-stock-btn { cursor: pointer; }
.ce-stock-btn:disabled { cursor: default; opacity: 1; }
.ce-stock-btn:not(:disabled) { box-shadow: 0 0 0 2px var(--accent); }
.ce-stock-count { font-variant-numeric: tabular-nums; font-weight: 600; }

/* THE PICKER: four suits, hidden until an eight is tapped. It relies on the
   global [hidden] rule at the top of this file; the buttons are at least 44
   px tall so a thumb can hit them. */
.ce-picker { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; margin: .6rem 0 1rem;
             padding: .6rem; border: 1px solid var(--line); border-radius: 10px; }
.ce-picker[hidden] { display: none !important; }
.ce-picker-prompt { flex: 1 0 100%; margin: 0 0 .2rem; }
.ce-suits { display: flex; flex-wrap: wrap; gap: .4rem; }
.ce-suit { min-height: 44px; min-width: 5.5rem; padding: .5rem .8rem; font: inherit; background: var(--surface); color: inherit;
           border: 1px solid var(--line); border-radius: 8px; cursor: pointer; display: inline-flex; align-items: center; gap: .3rem; }
.ce-suit:hover { border-color: var(--accent); }
.ce-suit.red { color: var(--card-red, #b3261e); }
.ce-suit-glyph { font-size: 1.3em; }

.ce-hand { display: flex; flex-wrap: wrap; gap: .4rem; padding-top: 16px; width: min(100%, 560px); }
.ce .controls { margin-top: .8rem; display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; }

.ce-card-wrap { overflow-x: auto; margin-top: 1.4rem; width: min(100%, 100%); }
.ce-card { border-collapse: collapse; font-size: .85rem; min-width: 100%; }
.ce-card th, .ce-card td { text-align: end; padding: .25rem .5rem; border-bottom: 1px solid var(--line); white-space: nowrap; }
.ce-card th:first-child, .ce-card td:first-child { text-align: start; }
.ce-card caption { text-align: start; padding-bottom: .3rem; }
/* TWO NUMBERS A CELL, KEPT APART: the points that hand (what the seat had
   left, or +what it scored) and the running total. The first draft set them
   side by side with nothing between, so "22" left and a total of "0" read as
   "220". The total sits in its own slot behind a hair rule with a fixed
   width, so the totals line up down the column and never touch the points. */
.ce-card td { white-space: nowrap; }
.ce-card .ce-cell { font-variant-numeric: tabular-nums; display: inline-block; min-width: 2.6em; text-align: end; }
.ce-card .ce-total { font-variant-numeric: tabular-nums; display: inline-block; min-width: 2.6em; text-align: end;
                     margin-inline-start: .7rem; padding-inline-start: .6rem; border-inline-start: 1px solid var(--line); }
.ce-card td.out .ce-cell { color: var(--accent); }
.ce-card td.left .ce-cell { opacity: .55; }
/* ---- differences (plan_differences/06) ------------------------------------------
   Spot the Difference. Every class starts `sd-`. ONE PICTURE AT A TIME: the
   stack holds two wrappers and the server fills the one the phase allows, so
   the stack is one 4:3 picture tall; at the reveal both are shown, stacked,
   because that page is read and not played. The picture must be whole on
   screen with the bar in both orientations of a 375 x 667 phone, so its width
   is bounded by min(100%, 720px) and, in landscape, by the height left under
   the bar (plan_differences/01 mark 8 measured 359 x 269 portrait and 353 x
   265 landscape). The picture's own colours are literals in the markup, as the
   prototype's draw.js emits them, and colour-scheme is forced light so an
   Android auto-dark never inverts the scene (index_web_template). No inline
   styles: the CSP has no style-src. The ring, the reveal and the cursor are
   the only things this stylesheet colours. */
.sd { display: grid; gap: .5rem; justify-items: center; }
.sd .sd-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .5rem .9rem;
              width: min(100%, 720px); min-height: 2.4rem; font-variant-numeric: tabular-nums; }
.sd .sd-phase { text-transform: uppercase; letter-spacing: .06em; font-size: .8rem; color: var(--muted); }
.sd .sd-count { font-weight: 700; font-size: 1.3rem; min-width: 3.2ch; text-align: center; }
.sd .sd-strikes { display: inline-flex; gap: .2rem; font-size: .9rem; color: var(--accent); }
.sd .sd-dot.spent { color: var(--line); }
.sd .sd-counts { display: inline-flex; gap: .6rem; }
.sd .sd-seat { font-weight: 600; }
.sd .sd-seat.you .sd-found { text-decoration: underline; }
.sd .sd-lock { font-size: .85rem; color: var(--muted); }
.sd .sd-ready { min-width: 8rem; }
/* the stack: ONE picture tall, bounded both ways. In landscape the width is
   capped by the height left under the bar (90px: the bar is 38 and the rest
   is the gap and a margin), so at 375 px tall the picture is 380 x 285 and
   the smallest target 47.5 px (arithmetic: headless Chrome's 667 x 375
   window has a 232 px viewport, which no phone has, and measured 136 wide
   there, the cap doing its job). At a phone width the picture BLEEDS 12 px
   into the page's 16 px padding each side: 343 px wide the smallest target
   was 42.9 px, under the 44 px floor, and 367 wide it is 45.9 (measured 23
   Sep 2026, squeezed to 375). The grid centres an item wider than its track
   symmetrically, so the bleed is even. */
.sd .sd-stack { display: grid; gap: .5rem; width: min(100%, 720px, calc((100vh - 90px) * 4 / 3)); }
@media (max-width: 520px) { .sd .sd-stack { width: min(calc(100% + 24px), calc((100vh - 90px) * 4 / 3)); } }
.sd .sd-stack.both { width: min(100%, 720px); }
.sd .sd-wrap { position: relative; width: 100%; aspect-ratio: 4 / 3; border-radius: 10px; overflow: hidden; box-shadow: 0 8px 18px var(--shadow); }
.sd .sd-pic { display: block; width: 100%; height: 100%; color-scheme: only light; touch-action: manipulation;
              -webkit-touch-callout: none; user-select: none; -webkit-user-select: none; outline: none; }
.sd #sd-second { cursor: crosshair; }
.sd #sd-second:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.sd .sd-stack.locked .sd-pic { filter: grayscale(.7) brightness(.85); cursor: wait; }
.sd .sd-ring { fill: none; stroke: #ffd400; stroke-width: 6; pointer-events: none; }
.sd .sd-reveal { fill: none; stroke-width: 6; stroke-dasharray: 14 10; pointer-events: none; }
.sd .sd-reveal-found { stroke: #ffd400; } .sd .sd-reveal-missed { stroke: #ff3b30; }
.sd .sd-hint { fill: rgba(255,212,0,.25); stroke: #ffd400; stroke-width: 4; pointer-events: none; }
.sd .sd-cursor { fill: none; stroke: #fff; stroke-width: 3; pointer-events: none; }
.sd .sd-stack.shake { animation: sd-shake .3s linear 1; }
@keyframes sd-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-6px); } 75% { transform: translateX(6px); } }
@media (prefers-reduced-motion: reduce) { .sd .sd-stack.shake { animation: none; } }
.sd .sd-picno { margin: 0; }
.sd .sd-reveals { margin: 0; padding-inline-start: 1.2rem; font-size: .9rem; color: var(--muted); }
.sd .sd-reveals:empty { display: none; }
.sd .sd-tally { width: min(100%, 720px); border-collapse: collapse; font-variant-numeric: tabular-nums; }
.sd .sd-tally th, .sd .sd-tally td { padding: .3rem .5rem; text-align: start; border-bottom: 1px solid var(--line); }
.sd .sd-tally tr.you td { font-weight: 600; }

/* ---- airhockey (plan_air_hockey/06) -----------------------------------------
   The table is a canvas the client draws fifty times a second from the
   arbiter's snapshots; render.js paints the laminate, the rail and the
   strikers from the site's tokens, read off the root at resize. What this
   block owns is the frame the canvas fills, the score line above it, the
   lobby panel, the rallies list and the two sentences for a connection that
   cannot carry a live table.

   THIS BLOCK DECLARES NO PALETTE: the field colour is the one custom
   property render.js asks for, and it is defined in the tokens block with
   the rest of the site's colours, not here.
   ------------------------------------------------------------------------ */

/* THE STACK IS THE DRAG SURFACE, and the listeners are on it rather than on
   the canvas inside it, so touch-action belongs here too. Its shape decides
   the orientation: taller than wide and render.js draws the table portrait
   with this seat's goal at the bottom, wider than tall and it draws it
   landscape. A phone in portrait gets a tall box; a desk gets a wide one.
   The height is capped against the viewport less the page's chrome, so the
   whole table is on screen with the mallet under the thumb. */
.ah-stack {
  position: relative;
  width: min(100%, 26rem);
  height: min(calc(100dvh - 9rem), 48rem);
  min-height: 18rem;
  margin: 0 auto;
  border: 2px solid var(--walnut);
  border-radius: .4rem;
  background: var(--surface-2);
  overflow: hidden;
  touch-action: none;
}
.ah-stack:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.ah-stack canvas {
  display: block;
  margin: 0 auto;
  touch-action: none;
}
@media (orientation: landscape) {
  .ah-stack {
    width: min(100%, 60rem);
    height: min(calc(100dvh - 10rem), 30rem);
  }
}

.ah-note {
  position: absolute;
  left: 50%;
  bottom: .6rem;
  transform: translateX(-50%);
  margin: 0;
  padding: .3rem .7rem;
  border-radius: .3rem;
  background: var(--walnut-deep);
  color: var(--card);
  font-size: .9rem;
  max-width: 90%;
  text-align: center;
}

/* The score, the stall countdown beside it. The names are the server's and
   the figures the client's, so the figure sits in its own element. */
.ah-score {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: .3rem 1.2rem;
  width: min(100%, 26rem);
  margin: 0 auto .5rem;
  font: 500 1.3rem/1.2 var(--display);
  font-variant-numeric: tabular-nums;
}
.ah-score-seat.peg-blue .ah-score-name { color: var(--peg-blue); }
.ah-score-seat.brass .ah-score-name { color: var(--brass); }
.ah-count {
  min-width: 2ch;
  color: var(--danger);
  font-weight: 600;
}

/* THE LOBBY SITS UNDER THE TABLE, where the thumb already is: the press
   that starts the match is made with the thumb that is about to play. The
   disabled state is the seat that has already pressed, which is why it stays
   on the page and does not vanish. */
.ah-ready {
  display: flex;
  justify-content: center;
  margin: .8rem auto 0;
  max-width: 26rem;
}
.ah-ready-button {
  min-width: 12rem;
  padding: .7rem 1.4rem;
  font-size: 1.05rem;
}
.ah-ready-button[disabled] { opacity: .55; }

.ah-cannot {
  margin: .8rem auto;
  max-width: 40rem;
  padding: .6rem .8rem;
  border-left: 3px solid var(--peg-red);
  background: var(--surface-2);
}

.ah-reading {
  max-width: 40rem;
  margin: .5rem auto 0;
  color: var(--muted);
}

.ah-rallies {
  max-width: 40rem;
  margin: 1rem auto 0;
  font-variant-numeric: tabular-nums;
}
.ah-rallies li[data-end="stall"] { color: var(--muted); }

/* Nothing in this block animates for a reader who asked for less motion.
   The smear and the goal flash are stilled by render.js, which is told
   through the view rather than through the stylesheet; the countdown is
   written in words by the client for the same reader. */
@media (prefers-reduced-motion: reduce) {
  .ah-stack, .ah-note, .ah-count, .ah-ready-button {
    transition: none !important;
    animation: none !important;
  }
}

/* the framed wish list (/wishlist): the frame takes the width and most of
   the viewport, so the list scrolls inside itself rather than the page */
.wishlist-frame { margin: 1rem 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--surface-2); }
.wishlist-frame iframe { display: block; width: 100%; height: min(75vh, 900px); border: 0; }
.wishlist-go { margin: 0; }

/* ---- horde (plan_horde/06) --------------------------------------------------
   The field is a canvas the client draws twelve times a second from the
   arbiter's snapshots and predicts its own survivor on at twenty-five;
   nothing here paints a cell of it. What this block owns is THE SCREEN: the
   stack the canvas fills, which is the whole viewport under the shared head
   either way up, the two sentences for a connection that cannot carry a
   live game, the lobby press, the seat list and the round tables.

   THIS BLOCK DECLARES NO PALETTE. render.js reads the four seat tokens and
   the site's ink, card, walnut and hole off :root and paints the night
   field from integer triples of its own, so there is no custom property
   here to hide a literal in; t/horde-07-page.t asserts both.
   ------------------------------------------------------------------------ */

/* THE STACK IS THE GAME, EITHER WAY UP (the owner's second ask). Full
   width, and the viewport's height less the shared head, so the canvas is
   the screen on a phone held upright or sideways; the camera inside it puts
   VIEW cells across whichever side is shorter. `100vh` first as the fallback
   for a browser without dynamic units, then `100dvh`, which is the height
   left once a mobile browser's own bars have gone. The head's real height
   comes from the client (`--horde-head`), because the shared head wraps to
   two lines at a phone's width and a fixed number ran the field off the
   bottom of the prototype's frame. The safe-area paddings keep a stick out
   from under a notch or a home bar; they are zero unless the layout's
   viewport meta says viewport-fit=cover, which it does. */
.horde-stack {
  position: relative;
  display: block;
  width: auto;
  height: calc(100vh - var(--horde-head, 6rem) - env(safe-area-inset-top) - env(safe-area-inset-bottom));
  height: calc(100dvh - var(--horde-head, 6rem) - env(safe-area-inset-top) - env(safe-area-inset-bottom));
  min-height: 18rem;
  /* THE INSETS ARE MARGINS, NOT PADDING: render.js sizes its canvas to the
     stack's border box, so a padding would put the canvas's right and
     bottom edges under the inset it was meant to avoid */
  margin: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
  /* a shadow and not a border, for the same reason: a border is inside the
     box render.js measures and the canvas would be two pixels too wide */
  border: 0;
  box-shadow: 0 0 0 2px var(--walnut);
  border-radius: .4rem;
  background: var(--hole);
  overflow: hidden;
  user-select: none;
  -webkit-user-select: none;
}
.horde-stack:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

/* FULL BLEED where the layout is one column (the sidebar appears at 1100px
   and above): on a phone the stack is the viewport's whole width, out past
   the page's own padding, less the insets a notch or a home bar take. Above
   that it stays inside the column beside the sidebar. */
@media (max-width: 1099px) {
  .horde-stack {
    width: calc(100vw - env(safe-area-inset-left) - env(safe-area-inset-right));
    margin-left: calc(50% - 50vw + env(safe-area-inset-left));
    margin-right: calc(50% - 50vw + env(safe-area-inset-right));
    border-radius: 0;
  }
}

/* THE ONE SURFACE A DRAG MUST NOT SCROLL THE PAGE FROM, because a thumb on
   it is a stick. The canvas only: it fills the stack, so the frame is never
   under a thumb, and the rest of the page scrolls as it always did.

   OUT OF FLOW, and that is load-bearing. render.js gives its canvas a pixel
   width equal to the stack's, the stack is as wide as the main column, and
   a column's minimum width is its widest child: an in-flow canvas made the
   column as wide as the last canvas, the observer re-fitted, and the page
   grew four pixels a frame until it scrolled sideways (the probe found it
   at 528 in 500). Absolute, the canvas asks the column for nothing. */
.horde-stack canvas {
  position: absolute;
  left: 0;
  top: 0;
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

/* while a round runs the page does not bounce under a thumb that slid off
   the canvas: the client puts the class on the body and takes it off */
body.horde-live { overscroll-behavior: none; }

/* In landscape the shared head collapses to one line, so the field gets the
   height: the seats and the meta line sit side by side and clip rather than
   wrap. The promo and the footer are BELOW the fold, never hidden: the
   banner is on every page by the owner's deal, and pushing it down is not
   the same as removing it. In portrait the head keeps its two lines. */
@media (orientation: landscape) {
  .horde-live .game-head {
    display: flex;
    align-items: baseline;
    gap: 1rem;
    flex-wrap: nowrap;
    overflow: hidden;
    white-space: nowrap;
  }
  .horde-live .game-head .scores { flex: 1 1 auto; min-width: 0; overflow: hidden; }
  .horde-live .game-head .meta { flex: 0 0 auto; margin: 0; }
}

.horde-note {
  position: absolute;
  left: 50%;
  bottom: .6rem;
  transform: translateX(-50%);
  margin: 0;
  padding: .3rem .7rem;
  border-radius: .3rem;
  background: var(--walnut-deep);
  color: var(--card);
  font-size: .9rem;
  max-width: 90%;
  text-align: center;
  z-index: 2;
}

/* the fullscreen button, in the corner the HUD leaves free; the client
   shows it only where the browser has the API */
.horde-full {
  position: absolute;
  right: .5rem;
  bottom: .5rem;
  z-index: 2;
  width: 2.4rem;
  height: 2.4rem;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: .4rem;
  background: var(--surface);
  color: var(--ink);
  font-size: 1.2rem;
  line-height: 1;
  opacity: .8;
}

/* The wave and the hits in text. The canvas says the same thing for the
   person playing; this is what a screen reader has, and what is left when
   the canvas is a grey box because the socket never opened. */
.horde-hud {
  margin: .5rem auto 0;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  min-height: 1.4em;
}

.horde-cannot {
  margin: .8rem auto;
  padding: .6rem .8rem;
  border-left: 3px solid var(--peg-red);
  background: var(--surface-2);
}

/* THE LOBBY SITS UNDER THE FIELD, where the thumb already is: the press
   that starts the match is made with the thumb that is about to walk. Wide
   and centred, because a small button in front of a live picture reads as a
   decoration. The disabled state is the seat that has already pressed, which
   is why it stays on the page and does not vanish. */
.horde-ready {
  display: flex;
  justify-content: center;
  margin: .8rem auto 0;
}
.horde-ready-button {
  min-width: 12rem;
  padding: .7rem 1.4rem;
  font-size: 1.05rem;
}
.horde-ready-button[disabled] { opacity: .55; }

.horde-seats {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: .3rem 1.4rem;
  margin: .7rem auto;
  padding: 0;
}
/* A row reads as one sentence, so it is inline content in a block and not
   a flex container: every text node between the spans would otherwise be
   an anonymous flex item and the row would read as four fragments. */
.horde-seat { line-height: 1.5; font-variant-numeric: tabular-nums; }
.horde-seat[data-you="1"] .horde-seat-name { font-weight: 600; }
.horde-seat[data-out="1"] { opacity: .65; }
.horde-seat-live { margin-left: .3rem; }
.horde-swatch {
  display: inline-block;
  width: .7rem;
  height: .7rem;
  margin-right: .35rem;
  vertical-align: -1px;
  border-radius: 50%;
  background: var(--peg-blue);
}
/* your own survivor is brass in every chair, as in every live game here */
.horde-seat.brass     .horde-swatch { background: var(--brass); }
.horde-seat.peg-blue  .horde-swatch { background: var(--peg-blue); }
.horde-seat.peg-green .horde-swatch { background: var(--peg-green); }
.horde-seat.peg-amber .horde-swatch { background: var(--peg-amber); }

.horde-reading { margin: .5rem auto 0; color: var(--muted); }

/* A round table is five columns and a phone is not five columns wide, so
   each scrolls INSIDE ITS OWN BOX. */
.horde-rounds { margin: 1rem auto 0; }
.horde-round-wrap { overflow-x: auto; margin: 0 0 .8rem; }
.horde-round {
  width: 100%;
  min-width: 20rem;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
.horde-round caption {
  text-align: left;
  font-weight: 600;
  padding: .2rem 0;
}
.horde-round th, .horde-round td {
  text-align: left;
  padding: .2rem .4rem;
  border-bottom: 1px solid var(--line);
}

/* Nothing in this block animates for a reader who asked for less motion.
   The shake, the flashes and the fades inside the canvas are stilled by
   render.js, which is told through the view rather than the stylesheet. */
@media (prefers-reduced-motion: reduce) {
  .horde-stack, .horde-note, .horde-full, .horde-ready-button, .horde-seat {
    transition: none !important;
    animation: none !important;
  }
}

/* ---- mahjong (plan_mahjong/09) ------------------------------------------------
   The tile body is CSS and the face is one `use` of the sprite in
   root/templates/_mahjong_tiles.tmpl, which is the .dom-tile idea: the
   sheet carries forty-two shapes and nothing about how a tile looks.

   THE MATERIAL TOKENS, with an explicit ink. --card and --card-ink are the
   pair that flip together between light and dark; a tile that took --ink on
   a fixed --card background is white on ivory the moment the theme changes,
   which is the mistake Oware made twice.

   THE INDEX DIGIT IS HIDDEN BY `color`, NOT BY A CLASS ON THE GLYPH. The
   sprite draws the small corner digit in `currentColor`, because a <use>
   clone's internals cannot be reached by a CSS selector at all: a rule for
   `.mj-index` inside the symbol matches nothing once it is cloned. So the
   tile's own `color` is the switch, and `.mj-show-index` on the section is
   the way to turn the digits on if the owner wants them.

   AND THE COLOUR IT SWITCHES TO IS --card-ink AND NOT --muted, which is
   Oware's dark-mode lesson in this game's own hand. A TILE IS IVORY IN BOTH
   THEMES: --card and --card-ink are deliberately NOT redefined in the dark
   block, because a playing tile is a physical object and does not turn
   black at dusk. --muted IS redefined there, from #6d655a to #a79d8f, so a
   digit painted with it sat light grey on fixed ivory and was all but
   invisible in dark mode. NEVER A FLIPPING TOKEN ON A FIXED BACKGROUND:
   t/mahjong-05-page.t reads this block and fails on any rule under
   `.mj-tile` that paints with a token the dark block moves. */
.mj {
  --mj-w: 44px;
  --mj-sm: 30px;
  display: flex;
  flex-direction: column;
  gap: .6rem;
  /* `min-width: 0` IS LOAD-BEARING, and it belongs HERE rather than only on
     the scroll container inside. `article.game` is a GRID, so this section
     is a grid item, and a grid item's automatic minimum size is its
     content's minimum, not zero. The scorecard table carries
     `min-width: 22rem`; that 352px travelled up through the scroll
     container and made this whole section 352 wide inside a 328 track, so
     a 360px phone scrolled sideways and EVERY block on the page, the
     shared status line included, sat 8px too wide. `overflow-x: auto` on
     the table's wrapper cannot help until the item above it is allowed to
     be narrower than what is inside it. Measured in headless Chrome at
     360px, 24 Sep 2026. */
  min-width: 0;
}
.mj-tile {
  width: var(--mj-w);
  aspect-ratio: 3 / 4;
  flex: 0 0 auto;
  padding: 0;
  display: inline-block;
  background: var(--card);
  color: transparent;                  /* the index digit, off by default */
  border: 1px solid var(--card-edge);
  border-radius: 6px;
  box-shadow: 0 1px 2px var(--shadow);
  overflow: hidden;
}
.mj-show-index .mj-tile { color: var(--card-ink); }
.mj-tile.sm { width: var(--mj-sm); }
.mj-face { display: block; width: 100%; height: 100%; }

/* A concealed kong of another seat: four backs, because the view sends no
   tiles for it until the hand ends. Nothing is hidden here that the page
   was ever told. */
.mj-tile.back {
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 2px var(--card-edge), 0 1px 2px var(--shadow);
}

/* ---- the other three seats ---- */
.mj-seats { display: flex; flex-wrap: wrap; gap: .5rem; min-width: 0; }
.mj-seat {
  flex: 1 1 14rem;
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: .4rem .5rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .3rem .5rem;
}
.mj-seat.on-turn { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.mj-name { font-weight: 600; }
.mj-wind { font-variant-caps: small-caps; color: var(--muted); }
.mj-dealer { font-size: .8rem; border: 1px solid var(--line); border-radius: 4px; padding: 0 .25rem; color: var(--muted); }
.mj-total { font-variant-numeric: tabular-nums; margin-left: auto; }
.mj-melds, .mj-flowers { display: flex; flex-wrap: wrap; gap: .35rem; flex-basis: 100%; }
.mj-meld { display: inline-flex; gap: 1px; }
.mj-meld.mj-concealed { opacity: .92; }
.mj-melds:empty, .mj-flowers:empty { display: none; }

/* ---- the centre ---- */
.mj-centre { display: flex; flex-wrap: wrap; gap: .25rem 1rem; font-variant-numeric: tabular-nums; }

/* ---- the pools ----
   Rows of six, which is the table layout the rules describe (3.6.4), so a
   player counts a pool the way they would count one in front of them.
   A FIXED GAP AND NOT A PERCENTAGE ONE: a percentage row gap resolves
   against the container's own height, which a grid of unknown row count
   does not have yet, and the rows collapsed onto each other. */
.mj-pools { display: flex; flex-wrap: wrap; gap: .5rem; min-width: 0; }
.mj-pool { flex: 1 1 11rem; min-width: 0; }
.mj-pool-name { display: block; font-size: .85rem; }
.mj-pool-tiles {
  display: grid;
  grid-template-columns: repeat(6, var(--mj-sm));
  gap: 2px;
  justify-content: start;
}
.mj-tile.mj-last { box-shadow: 0 0 0 2px var(--accent), 0 1px 2px var(--shadow); }
.mj-tile.mj-lifted { transform: translateY(-4px); }

/* ---- your own melds, and the rack ----
   FOURTEEN SLOTS. At 360px fourteen tiles at 44px wrap to two rows of
   seven with no horizontal scroll, which is what phase 01's prototype was
   measured for. The rack is capped rather than stretched, because a rack
   spread across a desktop is harder to read than a compact one. */
.mj-mine { display: flex; flex-wrap: wrap; gap: .5rem; align-items: flex-start; }
.mj-rack {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem;
  width: min(100%, 640px);
  min-width: 0;
}
.mj-rack .mj-tile { cursor: pointer; transition: transform .12s ease; }
.mj-rack .mj-tile[disabled] { cursor: default; opacity: .72; }
.mj-rack .mj-tile.mj-sel,
.mj-rack .mj-tile[aria-pressed="true"] {
  transform: translateY(-8px);
  box-shadow: 0 0 0 2px var(--accent), 0 3px 5px var(--shadow);
}
/* The tile just drawn is set apart, not coloured: a colour would be the
   only thing telling a colour-blind player which tile is new. */
.mj-rack .mj-tile.mj-drawn { margin-left: .9rem; }

/* ---- the window prompt ----
   It names the seat that threw the tile and nobody else. Which seats were
   asked is not in the view and must not become guessable from here. */
.mj-window {
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: .5rem .6rem;
  display: flex;
  flex-direction: column;
  gap: .4rem;
}
.mj-window-buttons { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; min-width: 0; }
.mj-claim { min-height: 44px; display: inline-flex; align-items: center; gap: .4rem; }
.mj-run { display: inline-flex; gap: 1px; }
.mj-run .mj-tile { width: 22px; }

/* ---- the scorecard ----
   `min-width: 0` IS LOAD-BEARING. A flex item's automatic minimum size is
   its content's minimum, not zero, so a scroll container holding a table
   with `min-width: 22rem` refuses to shrink below 352px and pushes the
   whole column past a 360px phone: the page then scrolls sideways and
   every other block on it is 8px too wide. The overflow rule cannot do its
   job until the item is allowed to be narrower than what is inside it.
   Measured at 360px in headless Chrome, 24 Sep 2026. */
.mj-card-wrap { overflow-x: auto; min-width: 0; }
.mj-card { width: 100%; min-width: 22rem; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.mj-card caption { text-align: left; font-weight: 600; padding: .2rem 0; }
.mj-card th, .mj-card td { text-align: left; padding: .25rem .4rem; border-bottom: 1px solid var(--line); vertical-align: top; }
.mj-card .mj-delta span { display: block; font-size: .85rem; }
.mj-card .mj-won { font-weight: 600; }
.mj-fan-list { display: block; font-size: .85rem; }

@media (max-width: 640px) {
  .mj { --mj-w: 40px; --mj-sm: 26px; }
  .mj-seat { flex-basis: 100%; }
  .mj-pool { flex-basis: 100%; }
}

/* Nothing here moves for a reader who asked for less motion; the client
   also skips the whole pacing chain, which is told by the same query. */
@media (prefers-reduced-motion: reduce) {
  .mj-rack .mj-tile, .mj-tile.mj-lifted { transition: none !important; }
}

/* ---- match (plan_match/06) --------------------------------------------------------
   Match. Every class starts `mt-`. TWENTY BUTTONS IN A FOUR BY FIVE GRID, and
   the logical grid is four by five in BOTH orientations, because a memory of
   where a tile was must survive turning the phone. The grid's width is bounded
   by the height it has, so the whole board is on screen with the bar and
   without scrolling: plan_match/01 mark 8 measured 84.4 px tiles in portrait at
   375 x 667 and 63.7 px in landscape at 667 x 375, both over the 44 px floor.

   THE LANDSCAPE RULE COMES AFTER THE PHONE RULE and must stay there. Written
   before it, the phone rule's width won and the tiles read 49.8 px (01's
   finding). In a short landscape the bar sits BESIDE the grid, because a bar
   above it leaves 42 px tiles.

   NOT A SCRATCH CARD. The back is two flat tokens and a border: no gradient
   that reads as foil, no gold, no shine, and no 3D transform anywhere, which
   would be the single most scratch-card-like thing this page could do
   (docs/product.md, plan_match/00). The turn is a short fade on opacity and
   nothing else. No inline styles: the CSP has no style-src.

   THE HEIGHT BUDGET IS A VARIABLE, `--mt-vh`, whose value is 100vh. Headless
   Chrome refuses a window smaller than about 500 x 520, so while the
   arithmetic reads 100vh directly no probe can measure this grid at a phone's
   height at all. With the budget named, t/match-08-service.t sets it to the
   phone's real height and measures THE SHIPPED FORMULA instead of a copy of
   it (the copied-verifier trap, index_tests_that_lie). Nothing else in this
   block may read 100vh.

   AND `min-width: 0`, because article.game is a grid and this section is a
   grid item, whose automatic minimum is its CONTENT'S minimum and not zero:
   without it the tally's minimum width pushes every block on the page
   sideways on a narrow phone, the shared status line included (semantic-68's
   measurement on Mahjong, passed on by semantic-93, 24 Sep 2026). */
.mt { display: grid; gap: .5rem; justify-items: center; min-width: 0; --mt-vh: 100vh; }
.mt .mt-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .5rem .9rem;
              width: min(100%, 440px); min-height: 2.4rem; font-variant-numeric: tabular-nums; }
.mt .mt-phase { text-transform: uppercase; letter-spacing: .06em; font-size: .8rem; color: var(--muted); }
.mt .mt-count { font-weight: 700; font-size: 1.3rem; min-width: 3.2ch; text-align: center; }
.mt .mt-counts { display: inline-flex; gap: .6rem; }
.mt .mt-seat { font-weight: 600; }
.mt .mt-seat.you .mt-pairs { text-decoration: underline; }
.mt .mt-lock { font-size: .85rem; color: var(--muted); }
.mt .mt-ready { min-width: 8rem; }

/* the grid: square tiles, a percentage gap so it resolves against the width,
   and a width capped by the height left under the bar */
.mt .mt-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 2%;
               width: min(100%, 440px, calc((var(--mt-vh) - 130px) * 4 / 5)); margin: 0 auto;
               touch-action: manipulation; -webkit-touch-callout: none; user-select: none; -webkit-user-select: none; }
.mt .mt-tile { position: relative; display: block; width: 100%; aspect-ratio: 1; padding: 0;
               border: 2px solid var(--mt-back-line); border-radius: 10%; background: var(--mt-back);
               box-shadow: 0 2px 4px var(--shadow); cursor: pointer;
               font: 600 .75rem/1 var(--body); color: var(--mt-num); }
.mt .mt-tile[data-state="up"], .mt .mt-tile[data-state="miss"],
.mt .mt-tile[data-state="matched"], .mt .mt-tile[data-state="shown"] {
    background: var(--mt-face-bg); border-color: var(--mt-face-line); animation: mt-turn .18s ease-out; }
.mt .mt-tile[data-state="matched"] { border-color: var(--mt-matched); box-shadow: inset 0 0 0 2px var(--mt-matched); }
.mt .mt-tile[data-state="miss"] { border-color: var(--mt-miss); }
.mt .mt-tile:disabled { cursor: default; opacity: 1; }
.mt .mt-tile.mt-hinted { outline: 3px dashed var(--brass); outline-offset: 2px; }
.mt .mt-tile:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.mt .mt-face { display: block; width: 78%; height: 78%; margin: 6% auto 0; pointer-events: none; color-scheme: only light; }
.mt .mt-num { position: absolute; right: 8%; bottom: 5%; pointer-events: none; }
.mt .mt-grid.locked .mt-tile[data-state="down"] { filter: brightness(.7); cursor: wait; }
@keyframes mt-turn { from { opacity: .3; } to { opacity: 1; } }
@keyframes mt-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-6px); } 75% { transform: translateX(6px); } }
.mt .mt-grid.shake { animation: mt-shake .28s ease-in-out; }

.mt .mt-boardno { font-size: .85rem; }
.mt .mt-reveals { margin: 0; padding-left: 1.2rem; font-size: .85rem; color: var(--muted); }
.mt .mt-tally { border-collapse: collapse; width: min(100%, 440px); font-variant-numeric: tabular-nums; }
.mt .mt-tally th, .mt .mt-tally td { text-align: left; padding: .2rem .4rem .2rem 0; border-bottom: 1px solid var(--line); font-size: .9rem; }
.mt .mt-tally th { color: var(--muted); font-weight: 600; }

/* a phone: the grid takes the width it has under a shorter bar */
@media (max-width: 520px) {
  .mt .mt-grid { width: min(100%, calc((var(--mt-vh) - 110px) * 4 / 5)); }
}
/* A SHORT LANDSCAPE: the bar beside the grid, not above it. THIS RULE MUST
   FOLLOW THE ONE ABOVE: written before it, the phone width wins and the tiles
   read 49.8 px instead of 63.7 (plan_match/01 mark 8). */
@media (max-height: 520px) and (orientation: landscape) {
  .mt { grid-auto-flow: column; justify-items: start; align-items: start; gap: .6rem; }
  .mt .mt-bar { flex-direction: column; align-items: flex-start; width: 150px; min-height: 0; }
  .mt .mt-grid { width: min(100%, calc((var(--mt-vh) - 36px) * 4 / 5)); margin: 0; }
  .mt .mt-boardno, .mt .mt-reveals, .mt .mt-tally { display: none; }
}

/* the tokens, the site's three-way override: the dark set by default under a
   dark scheme, the light set under a light one, and the explicit choice last */
.mt { --mt-back: #2f4c8a; --mt-back-line: #23396a; --mt-face-bg: #f7f3ea; --mt-face-line: #c4bdae;
      --mt-matched: #1f7a4d; --mt-miss: #b3261e; --mt-num: #6d655a; }
:root:not([data-theme="light"]) .mt { --mt-back: #3a4a6e; --mt-back-line: #2b3852; --mt-matched: #6fc59a; }
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .mt { --mt-back: #2f4c8a; --mt-back-line: #23396a; --mt-matched: #1f7a4d; } }
:root[data-theme="dark"] .mt { --mt-back: #3a4a6e; --mt-back-line: #2b3852; --mt-matched: #6fc59a; }

/* Nothing here moves for a reader who asked for less motion. */
@media (prefers-reduced-motion: reduce) {
  .mt .mt-grid.shake, .mt .mt-tile[data-state] { animation: none !important; }
}

/* ---- rummy (plan_rummy/07) -------------------------------------------------
 * Scoped under .rum-*, never generic names and never .gin-*: gin rummy is a
 * different game on this roster with its own block above.
 *
 * TWO BOARD TRAPS AVOIDED BY CONSTRUCTION, both recorded in
 * plan_peer2peergames. `width: max-content` on anything inside the .game grid
 * makes the grid column that wide and a phone scrolls sideways, so the hand
 * and the table are `min(100%, Npx)` and wrap. And a PERCENTAGE ROW GAP in a
 * column of aspect-ratio cells resolves against an unknown height and
 * overflows, so spacing here is a rem gap throughout.
 */
/* min-width: 0 IS LORD-BEARING AND NOT TIDINESS. article.game is a grid, so
 * this section is a GRID ITEM, and a grid item's automatic minimum size is its
 * CONTENT'S minimum rather than zero. The scorecard's width would travel up
 * through its own overflow-x wrapper, widen the whole section past the track,
 * and push every block on the page, the shared status line included, off a
 * 360px phone. The inner overflow-x does nothing until this line is here.
 * Measured by the Mahjong session, 24 Sep 2026. */
.rum { display: flex; flex-direction: column; gap: 0.8rem; min-width: 0; }

.rum-seats { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.rum-seat { display: flex; align-items: baseline; gap: 0.35rem; padding: 0.2rem 0.5rem;
            border: 1px solid var(--line); border-radius: 0.4rem; }
.rum-seat.on-turn { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
.rum-name { font-weight: 600; }
.rum-dealer { font-size: 0.8em; opacity: 0.7; }
.rum-count { font-variant-numeric: tabular-nums; }
.rum-count::before { content: "\00a0"; }
.rum-total { font-variant-numeric: tabular-nums; }

/* THE MELDS ON THE TABLE. A column of rows that wrap, each row a button so it
 * can be laid off on. A row belongs to whoever laid it and says so, because
 * anybody may add to anybody's. */
.rum-melds-wrap { width: min(100%, 560px); }
.rum-melds { display: flex; flex-direction: column; gap: 0.4rem; }
.rum-meld { display: flex; align-items: center; flex-wrap: wrap; gap: 0.2rem;
            padding: 0.3rem 0.4rem; background: none; border: 1px dashed var(--line);
            border-radius: 0.4rem; text-align: left; cursor: default; }
.rum-meld.mine { border-style: solid; }
.rum-meld:enabled { cursor: pointer; border-color: var(--ink); }
.rum-meld:enabled:hover { background: var(--surface-2); }
.rum-meld-who { flex: 0 0 auto; margin-right: 0.3rem; font-size: 0.8em; }

.rum-table { display: flex; align-items: flex-start; gap: 1rem; flex-wrap: wrap; }
.rum-pile { display: flex; align-items: center; gap: 0.3rem; }
.rum-pile-rest { display: flex; flex-wrap: wrap; gap: 0.15rem; max-width: min(100%, 180px); }
.rum-top { cursor: default; }
.rum-top:enabled { cursor: pointer; }
/* AN EMPTY PILE IS A REAL POSITION, from the first turn of every hand: the
 * first seat may take the only card on it. The slot stays, as an outline. */
.rum-top.empty { background: none; border: 1px dashed var(--line); box-shadow: none; }
.rum-stock { display: flex; flex-direction: column; align-items: center; gap: 0.2rem; }
.rum-stock-btn { min-width: 44px; min-height: 60px; }
.rum-stock-count { font-variant-numeric: tabular-nums; }

/* A SELECTED CARD LIFTS, which is Hearts' rule for its three-card pass. */
.rum-hand { display: flex; flex-wrap: wrap; gap: 0.3rem; width: min(100%, 560px); }
.rum-hand .card[aria-pressed="true"] { transform: translateY(-0.4rem); box-shadow: 0 0.3rem 0.5rem rgba(0,0,0,0.25); }

/* The scorecard. THE POINTS AND THE RUNNING TOTAL SIT IN SEPARATE RULED SLOTS:
 * Crazy Eights shipped them side by side and "22" beside a total of "0" read
 * as "220" in a played match. */
.rum-card-wrap { overflow-x: auto; }
.rum-card { border-collapse: collapse; }
.rum-card th, .rum-card td { padding: 0.2rem 0.5rem; border: 1px solid var(--line); text-align: right; }
.rum-card td.out { font-weight: 600; }
.rum-cell { display: inline-block; min-width: 2.5em; text-align: right; }
.rum-total { display: inline-block; min-width: 3em; margin-left: 0.5rem; padding-left: 0.5rem;
             border-left: 1px solid var(--line); text-align: right; font-variant-numeric: tabular-nums; }
.rum-blocked { font-size: 0.8em; }

/* ---- landlord (plan_landlord/08) --------------------------------------------------------------
 *
 * FORTY SQUARES IN A RING, which is the densest board this site draws, on a
 * page that also carries a holdings strip, a build pad, two sheets and a
 * scorecard. Two board traps this file has already sprung are avoided by
 * construction: nothing here is `max-content` wide, because that widens the
 * .game grid column and a phone then scrolls sideways; and spacing inside
 * the ring is a rem gap and never a percentage row gap, which resolves
 * against an unknown height and overflows.
 *
 * THE RING IS ELEVEN BY ELEVEN AND EVERY CELL IS THE SAME SIZE, corners
 * included. The board people remember has bigger corners, and that is
 * exactly what stops one fitting in a phone's width: equal cells make the
 * ring square, so `width: min(92vw, 34rem)` is all the sizing it needs.
 *
 * THE SET COLOURS ARE THEIR OWN TOKENS and never the seat tokens. A seat is
 * red, blue, green or amber wherever it appears on this site; a set is one
 * of eight bands on one board, and the two must not be confusable on a
 * square that has both an owner swatch and a colour bar.
 */
:root {
  --ll-set-a: #8d6e52; --ll-set-b: #7d9ec0; --ll-set-c: #b5739b;
  --ll-set-d: #d98f4e; --ll-set-e: #c0504d; --ll-set-f: #d8c15a;
  --ll-set-g: #5a9367; --ll-set-h: #3f5b8a;
}

/* min-width: 0 IS LOAD-BEARING. `article.game` is a grid, so this section is
   a grid item and its automatic minimum size is its CONTENT'S minimum, not
   zero. Without it the widest thing inside (the ring, or the scorecard at
   four seats) sets the track, every block on the page is pushed sideways
   including the shared status line, and the `overflow-x: auto` on the strip
   below does nothing at all. semantic-68 measured this on Mahjong and
   semantic-c5 hit it again on Match. */
.ll { display: flex; flex-direction: column; gap: 0.75rem; align-items: center;
      min-width: 0; max-width: 100%; }

/* the holdings strip: a card a seat, its own scroller rather than a wider page */
.ll-seats { display: flex; gap: 0.5rem; width: min(100%, 34rem);
            overflow-x: auto; padding-bottom: 0.25rem; }
.ll-seat { flex: 0 0 auto; min-width: 7.5rem; padding: 0.4rem 0.6rem;
           border: 1px solid var(--line); border-radius: 0.4rem; background: var(--surface); }
.ll-seat.on-turn { box-shadow: inset 0 0 0 2px var(--brass); }
.ll-seat.out { opacity: 0.5; }
.ll-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ll-cash, .ll-worth { display: inline-block; min-width: 2.5em; text-align: right;
                      font-variant-numeric: tabular-nums; }
.ll-worth { margin-left: 0.4rem; }
/* THE HOLDINGS MARKS CARRY THE SET'S COLOUR, which is the whole point of
   them: a glance at a seat's card should say which sets are nearly whole.
   They were left grey because the only colour rules written were for the
   ring's bars, and a mark with no colour is a mark that says only "some
   number of squares". A ferry and the works get the ink, being setless. */
.ll-holds { display: flex; flex-wrap: wrap; gap: 2px; margin-top: 0.3rem; }
.ll-hold { width: 0.55rem; height: 0.55rem; border-radius: 1px; background: var(--muted); }
.ll-hold.set-a { background: var(--ll-set-a); } .ll-hold.set-b { background: var(--ll-set-b); }
.ll-hold.set-c { background: var(--ll-set-c); } .ll-hold.set-d { background: var(--ll-set-d); }
.ll-hold.set-e { background: var(--ll-set-e); } .ll-hold.set-f { background: var(--ll-set-f); }
.ll-hold.set-g { background: var(--ll-set-g); } .ll-hold.set-h { background: var(--ll-set-h); }
/* Not a set, so not a set colour: a shape instead, so the two read apart
   without relying on colour alone (site.css's rule for every swatch here). */
.ll-hold.sq-ferry { background: var(--muted); border-radius: 50%; }
.ll-hold.sq-works { background: var(--muted); border-radius: 50% 1px; }

/* THE RING, AND THE ONE LINE THAT MAKES IT A RING: minmax(0, 1fr).
 *
 * `repeat(11, 1fr)` is `repeat(11, minmax(AUTO, 1fr))`, and a track's auto
 * minimum is its content's minimum. Every cell here carries a square's name,
 * so the track grew to fit the longest word in it and the eleven columns came
 * out 44, 44, 70 and 49 wide instead of equal. `aspect-ratio: 1` on the cell
 * then made it worse rather than better: a name that wrapped to two lines set
 * the cell's height, the ratio turned that height back into width, and the
 * board measured 544 by 780 with the rows half again as tall as the columns.
 *
 * So the tracks are minmax(0, 1fr) in both axes and the CELL HAS NO RATIO OF
 * ITS OWN. The board is square and the tracks are equal, which makes every
 * cell square by arithmetic; asking each cell to be square as well is what
 * let the content win. `min-width: 0` on the cell says the same thing again
 * for the cell's own contents, and it is not redundant: without it a long
 * name still sets a minimum inside a track that no longer grows, and the
 * name overflows the cell instead of being clipped by it.
 */
.ll-board { position: relative; display: grid; grid-template-rows: repeat(11, minmax(0, 1fr));
            width: min(94vw, 44rem); aspect-ratio: 1; gap: 1px; background: var(--line);
            border: 1px solid var(--line); border-radius: 0.3rem; overflow: hidden; }
.ll-row { display: grid; grid-template-columns: repeat(11, minmax(0, 1fr)); gap: 1px;
          min-width: 0; min-height: 0; }
.ll-cell { min-width: 0; min-height: 0; overflow: hidden; }
.ll-board .sq { position: relative; display: flex; flex-direction: column;
                background: var(--surface); overflow: hidden;
                font-size: clamp(5px, 1.15vw, 10px); line-height: 1.08; padding: 0 1px 1px; }
.ll-board .inside { background: transparent; }

/* THE KIND CLASS IS `sq-<kind>`, NEVER THE BARE KIND. `.card` is this site's
   playing card, 66px wide at 5/7, and Landlord's six card squares were being
   drawn as playing cards: 70 by 95 inside a 63px track. That one collision
   blew out six rows and six columns and was most of why the ring did not
   look like a ring. Board::grid carries the reason.

   The four corners and the squares that are not for sale read differently
   from a street at a glance, which is most of what makes a board legible. */
.ll-board .sq-start, .ll-board .sq-jail,
.ll-board .sq-rest, .ll-board .sq-to_jail { background: var(--line); }
/* THE FOUR CORNERS ARE WHERE A PLAYER ORIENTS THE BOARD, so they carry the
   site's accent and a heavier name rather than being one more grey square. */
.ll-board .sq-start .ll-cell-name, .ll-board .sq-jail .ll-cell-name,
.ll-board .sq-rest .ll-cell-name, .ll-board .sq-to_jail .ll-cell-name {
  color: var(--accent); font-weight: 700; letter-spacing: 0.02em; }
.ll-board .sq-tax, .ll-board .sq-card { background: var(--line); opacity: 0.92; }
.ll-board .sq-ferry, .ll-board .sq-works { background: var(--surface); }

.ll-bar { display: block; flex: 0 0 auto; height: 0.5em; min-height: 4px;
          margin: 0 -1px 1px; background: transparent; }
.ll-board .sq-street .ll-bar { background: var(--line); }
.sq-street.set-a .ll-bar { background: var(--ll-set-a); } .sq-street.set-b .ll-bar { background: var(--ll-set-b); }
.sq-street.set-c .ll-bar { background: var(--ll-set-c); } .sq-street.set-d .ll-bar { background: var(--ll-set-d); }
.sq-street.set-e .ll-bar { background: var(--ll-set-e); } .sq-street.set-f .ll-bar { background: var(--ll-set-f); }
.sq-street.set-g .ll-bar { background: var(--ll-set-g); } .sq-street.set-h .ll-bar { background: var(--ll-set-h); }

/* THE NAME IS OFF AT PHONE WIDTH AND IN THE MARKUP AT EVERY WIDTH, because
 * a screen reader wants it whether or not there is room to print it.
 *
 * Where it IS printed it must never be able to push the cell: `min-width: 0`
 * lets the flex item shrink below its longest word, and the break allows a
 * name like Kingfisher Reach to wrap rather than to run out of the square. */
.ll-cell-name { display: none; min-width: 0; overflow: hidden; text-align: center;
                overflow-wrap: anywhere; hyphens: auto; }
@media (min-width: 600px) { .ll-cell-name { display: block; } }

/* INSET FROM THE EDGE, not flush to it. Flush, the badge reads as belonging
   to the cell below it, which is what the board looked like the first time
   it was drawn: a row of little discs floating between the rows. */
.ll-owner { position: absolute; right: 2px; bottom: 2px; width: 1.05em; height: 1.05em;
            border-radius: 50%; font-size: 0.85em; line-height: 1.05em; text-align: center;
            font-weight: 700; color: #fff; background: var(--ink);
            box-shadow: 0 0 0 1px var(--surface); }
.ll-owner.seat-1 { background: var(--peg-red); border-radius: 50%; }
.ll-owner.seat-2 { background: var(--peg-blue); border-radius: 1px; }
.ll-owner.seat-3 { background: var(--peg-green); border-radius: 50% 1px; }
.ll-owner.seat-4 { background: var(--peg-amber); border-radius: 1px 50%; }
/* THE HOUSES SIT UNDER THE COLOUR BAR, which is where a house goes on a
   board, and the mortgage mark went to the OTHER bottom corner from the owner
   badge. Both used to sit where the name is: "Cairn Way M" with the mark
   printed through the last letter, and a house pip through the A of Alder.
   IN THE FLOW, NOT ABSOLUTE: absolutely positioned at the top left they sat
   on the first letter of every name that reached the edge of its cell. The
   cell is a flex column, so an order puts them between the bar and the name
   and the name starts below them. */
.ll-bar { order: -2; }
.ll-pips { order: -1; display: flex; justify-content: center; gap: 2px;
           min-height: 4px; margin-bottom: 1px; }
.ll-pip { width: 4px; height: 4px; border-radius: 1px; background: var(--peg-green);
          box-shadow: 0 0 0 1px rgba(0, 0, 0, .35); }
.ll-pip.hotel { width: 10px; background: var(--peg-red); }
.ll-mort { position: absolute; left: 2px; bottom: 2px; padding: 0 2px; border-radius: 2px;
           font-size: 0.8em; font-weight: 700; line-height: 1.3;
           color: var(--accent-ink); background: var(--peg-red); }

/* ---- the middle of the board ---------------------------------------------
   The name, the dice and the roll. A board's middle is where a hand goes, so
   it is where the one button that starts a turn belongs; it was an empty
   rectangle the size of a postcard until the owner looked at it. */
.ll-middle { position: absolute; inset: 15%; display: flex; flex-direction: column;
             align-items: center; justify-content: center; gap: 0.6rem; text-align: center; }
.ll-brand { margin: 0; font-family: var(--display); letter-spacing: 0.1em;
            font-size: clamp(0.9rem, 3.4vw, 2rem); color: var(--muted); opacity: 0.5; }

/* THE DICE ARE PIPS, NOT NUMERALS. Nine slots a die and the face lights the
   right ones, which gives the tumble something to animate and makes a die
   read as a die rather than as a number in a box. */
.ll-dice { display: flex; gap: 0.6rem; }
.ll-die { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr);
          gap: 2px; padding: 4px; width: 2.4rem; height: 2.4rem;
          border: 1px solid var(--line); border-radius: 0.45rem; background: var(--surface);
          box-shadow: 0 2px 0 var(--line); }
.ll-die i { border-radius: 50%; background: var(--ink); visibility: hidden; }
.ll-die[data-face="1"] i:nth-child(5),
.ll-die[data-face="2"] i:nth-child(1), .ll-die[data-face="2"] i:nth-child(9),
.ll-die[data-face="3"] i:nth-child(1), .ll-die[data-face="3"] i:nth-child(5), .ll-die[data-face="3"] i:nth-child(9),
.ll-die[data-face="4"] i:nth-child(1), .ll-die[data-face="4"] i:nth-child(3),
.ll-die[data-face="4"] i:nth-child(7), .ll-die[data-face="4"] i:nth-child(9),
.ll-die[data-face="5"] i:nth-child(1), .ll-die[data-face="5"] i:nth-child(3), .ll-die[data-face="5"] i:nth-child(5),
.ll-die[data-face="5"] i:nth-child(7), .ll-die[data-face="5"] i:nth-child(9),
.ll-die[data-face="6"] i:nth-child(1), .ll-die[data-face="6"] i:nth-child(3), .ll-die[data-face="6"] i:nth-child(4),
.ll-die[data-face="6"] i:nth-child(6), .ll-die[data-face="6"] i:nth-child(7), .ll-die[data-face="6"] i:nth-child(9)
  { visibility: visible; }
/* Face 0 is NOT a face: it is the die before this seat has rolled, and it is
   deliberately blank rather than showing a one. */
.ll-die[data-face="0"] { opacity: 0.5; }

@keyframes ll-tumble {
  0%   { transform: rotate(0deg)   translateY(0); }
  25%  { transform: rotate(-14deg) translateY(-14%); }
  50%  { transform: rotate(11deg)  translateY(0); }
  75%  { transform: rotate(-7deg)  translateY(-7%); }
  100% { transform: rotate(0deg)   translateY(0); }
}
@keyframes ll-settle { from { transform: scale(1.22); } to { transform: scale(1); } }
.ll-dice.rolling .ll-die { animation: ll-tumble 0.3s linear infinite; opacity: 1; }
.ll-die.settled { animation: ll-settle 0.24s ease-out; }
.ll-roll { font-size: 1rem; padding: 0.45rem 1.5rem; }
.ll-last { margin: 0; font-size: 0.9rem; min-height: 1.2em; }

/* A tumbling die is decoration, and decoration is the first thing to go. */
@media (prefers-reduced-motion: reduce) {
  .ll-dice.rolling .ll-die, .ll-die.settled { animation: none; }
}

/* THE TOKENS: an attribute moves them and the stylesheet tweens it. The
   forty position rules are GENERATED from Board::grid by
   bin/landlord-token-css.pl and sit below; without them every token resolves
   to its static position, which is the board's top-left corner, and all four
   seats sit on Start for the whole game. That is exactly what shipped until
   the owner looked at the board. */
.ll-token { position: absolute; z-index: 2; width: 1.4em; height: 1.4em; border-radius: 50%;
            font-size: clamp(7px, 1.4vw, 12px); line-height: 1.4em; text-align: center;
            font-weight: 700; color: #fff; background: var(--ink);
            box-shadow: 0 0 0 1.5px var(--surface);
            transform: translate(-50%, -50%);
            transition: left .45s cubic-bezier(.4, 0, .2, 1), top .45s cubic-bezier(.4, 0, .2, 1); }
/* FOUR SEATS ON ONE SQUARE IS ORDINARY, not an edge case: jail fills up and
   everybody passes Start. Each seat sits in its own quarter of the cell, so
   four tokens on one square are four discs rather than one. */
.ll-token.seat-1 { background: var(--peg-red);   margin: -0.35em 0 0 -0.35em; }
.ll-token.seat-2 { background: var(--peg-blue);  margin: -0.35em 0 0  0.35em; }
.ll-token.seat-3 { background: var(--peg-green); margin:  0.35em 0 0 -0.35em; }
.ll-token.seat-4 { background: var(--peg-amber); margin:  0.35em 0 0  0.35em; }
.ll-token[hidden] { display: none !important; }
@media (prefers-reduced-motion: reduce) { .ll-token { transition: none; } }

/* ---- landlord token positions: GENERATED, do not edit ---- */

/* One rule a square, from Board::grid. Regenerate with

     perl -Ilib bin/landlord-token-css.pl

   A token sits across its cell horizontally and LOW in it vertically.
   There are 11 tracks, so cell N spans N/11 to (N+1)/11; the
   centre of one is (N + 0.5) and a token at that height covers the
   square's name, which is printed at the top of the cell. The bias
   below drops it into the empty lower part instead, between the
   mortgage badge in one bottom corner and the owner badge in the other.
   The translate in .ll-token takes the token back by half its own size,
   which a percentage of the board cannot know. */
.ll-token[data-pos="0"] { left: 4.5455%; top: 6.0000%; }
.ll-token[data-pos="1"] { left: 13.6364%; top: 6.0000%; }
.ll-token[data-pos="2"] { left: 22.7273%; top: 6.0000%; }
.ll-token[data-pos="3"] { left: 31.8182%; top: 6.0000%; }
.ll-token[data-pos="4"] { left: 40.9091%; top: 6.0000%; }
.ll-token[data-pos="5"] { left: 50.0000%; top: 6.0000%; }
.ll-token[data-pos="6"] { left: 59.0909%; top: 6.0000%; }
.ll-token[data-pos="7"] { left: 68.1818%; top: 6.0000%; }
.ll-token[data-pos="8"] { left: 77.2727%; top: 6.0000%; }
.ll-token[data-pos="9"] { left: 86.3636%; top: 6.0000%; }
.ll-token[data-pos="10"] { left: 95.4545%; top: 6.0000%; }
.ll-token[data-pos="11"] { left: 95.4545%; top: 15.0909%; }
.ll-token[data-pos="12"] { left: 95.4545%; top: 24.1818%; }
.ll-token[data-pos="13"] { left: 95.4545%; top: 33.2727%; }
.ll-token[data-pos="14"] { left: 95.4545%; top: 42.3636%; }
.ll-token[data-pos="15"] { left: 95.4545%; top: 51.4545%; }
.ll-token[data-pos="16"] { left: 95.4545%; top: 60.5455%; }
.ll-token[data-pos="17"] { left: 95.4545%; top: 69.6364%; }
.ll-token[data-pos="18"] { left: 95.4545%; top: 78.7273%; }
.ll-token[data-pos="19"] { left: 95.4545%; top: 87.8182%; }
.ll-token[data-pos="20"] { left: 95.4545%; top: 96.9091%; }
.ll-token[data-pos="21"] { left: 86.3636%; top: 96.9091%; }
.ll-token[data-pos="22"] { left: 77.2727%; top: 96.9091%; }
.ll-token[data-pos="23"] { left: 68.1818%; top: 96.9091%; }
.ll-token[data-pos="24"] { left: 59.0909%; top: 96.9091%; }
.ll-token[data-pos="25"] { left: 50.0000%; top: 96.9091%; }
.ll-token[data-pos="26"] { left: 40.9091%; top: 96.9091%; }
.ll-token[data-pos="27"] { left: 31.8182%; top: 96.9091%; }
.ll-token[data-pos="28"] { left: 22.7273%; top: 96.9091%; }
.ll-token[data-pos="29"] { left: 13.6364%; top: 96.9091%; }
.ll-token[data-pos="30"] { left: 4.5455%; top: 96.9091%; }
.ll-token[data-pos="31"] { left: 4.5455%; top: 87.8182%; }
.ll-token[data-pos="32"] { left: 4.5455%; top: 78.7273%; }
.ll-token[data-pos="33"] { left: 4.5455%; top: 69.6364%; }
.ll-token[data-pos="34"] { left: 4.5455%; top: 60.5455%; }
.ll-token[data-pos="35"] { left: 4.5455%; top: 51.4545%; }
.ll-token[data-pos="36"] { left: 4.5455%; top: 42.3636%; }
.ll-token[data-pos="37"] { left: 4.5455%; top: 33.2727%; }
.ll-token[data-pos="38"] { left: 4.5455%; top: 24.1818%; }
.ll-token[data-pos="39"] { left: 4.5455%; top: 15.0909%; }

/* ---- end generated ---- */


/* the sheets: flex containers, so each needs the site's [hidden] rule to win */
.ll-sheet { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem;
            width: min(100%, 34rem); padding: 0.6rem; border: 1px solid var(--line);
            border-radius: 0.4rem; background: var(--surface); }
.ll-sheet-head { width: 100%; margin: 0; font-weight: 600; }
.ll-field { display: inline-flex; align-items: center; gap: 0.35rem; }
.ll-field input { width: 5rem; }
.ll-trade-cols { display: flex; flex-wrap: wrap; gap: 1rem; width: 100%; }
.ll-trade-col { flex: 1 1 10rem; }
.ll-check { display: flex; align-items: center; gap: 0.3rem; }
.ll-debt { width: min(100%, 34rem); margin: 0; padding: 0.5rem 0.6rem;
           border-left: 3px solid var(--peg-red); background: var(--surface); }

/* the build pad: a row a square, four buttons, each at least a thumb */
.ll-pad-wrap { width: min(100%, 34rem); overflow-x: auto; }
.ll-pad { width: 100%; border-collapse: collapse; }
.ll-pad th, .ll-pad td { padding: 0.15rem 0.3rem; border-bottom: 1px solid var(--line);
                         text-align: left; font-size: 0.9rem; }
.ll-mini { min-width: 44px; min-height: 44px; border: 1px solid var(--line);
           border-radius: 0.25rem; background: var(--surface); }
.ll-mini:disabled { opacity: 0.4; cursor: not-allowed; text-decoration: line-through; }

.ll-deed { width: min(100%, 34rem); padding: 0.6rem; border: 1px solid var(--line);
           border-radius: 0.4rem; background: var(--surface); }
.ll-deed-list { display: grid; grid-template-columns: auto 1fr; gap: 0.15rem 0.6rem; margin: 0.4rem 0; }
.ll-deed-list dt { font-weight: 600; }
.ll-deed-list dd { margin: 0; font-variant-numeric: tabular-nums; }

.ll-ledger-wrap { width: min(100%, 34rem); }
.ll-ledger { margin: 0.2rem 0 0; padding-left: 1.1rem; font-size: 0.9rem; }

.ll-card-wrap { width: min(100%, 34rem); overflow-x: auto; }
.ll-card { min-width: 100%; border-collapse: collapse; white-space: nowrap; }
.ll-card th, .ll-card td { padding: 0.15rem 0.4rem; border-bottom: 1px solid var(--line);
                           text-align: right; font-size: 0.9rem; }
.ll-card th:first-child, .ll-card td:first-child { text-align: left; }
.ll-total { display: inline-block; min-width: 3em; text-align: right;
            font-variant-numeric: tabular-nums; }

/* ---- lightlines (plan_lightlines/06) ---------------------------------------
   The field is a square canvas the client draws; nothing here lights a cell.
   What this block owns is the frame around the field, the seat list under it,
   and the two sentences for a connection that cannot carry the game. Every
   colour is a token: lightlines.js reads --brass, --peg-blue, --peg-green and
   --peg-amber from these same properties and lifts them toward the ivory
   before drawing them on the dark ground, a line further than a head, so the
   palette is in one place and the swatches below show it unlifted.

   THE PREFIX IS lgl- AND NOT ll-. Landlord owns `.ll-*` and its block is a
   hundred lines further down this file; two games sharing a prefix is a bug
   that only appears when both pages are open in one session's stylesheet,
   which is always. Lightlines moved because its CSS was the one still
   unwritten (relayed by semantic-f4, 24 Sep 2026).

   THERE IS NO BAR ABOVE THE FIELD, which is where this block parts company
   with Paintfield's. Paintfield's round is sixty seconds of cells changing
   hands and the bar carries that state in one glance; a Lightlines round has
   no running score at all, only who is still moving, and the field already
   says that with perfect fidelity. A bar here would be a bar of nothing.
   ------------------------------------------------------------------------ */

/* `article.game` is a grid and this section is a grid item, so its automatic
   minimum size is its CONTENT'S and not zero: anything inside it wider than
   the track pushes every block on the page sideways, the shared status line
   included, and an inner `overflow-x: auto` does nothing until this rule is
   here. Measured in headless Chrome by semantic-68 and semantic-c5 and
   relayed by semantic-f4 on 24 Sep 2026; nothing in this block is wide
   today, which is exactly why it is cheap to write down now. */
.lightlines { min-width: 0; max-width: 100%; }

/* The field is DARK: --hole is the site's recessed brown, the cribbage
   board's, and the frame is the same walnut as every other board here. */
.lgl-wrap {
  position: relative;
  width: min(100%, 34rem);
  margin: 0 auto;
  aspect-ratio: 1 / 1;
  border: 2px solid var(--walnut);
  border-radius: .4rem;
  background: var(--hole);
  overflow: hidden;
}

/* The one surface a drag must not scroll the page from. Scoped to the
   canvas: the rest of the page scrolls as it always did. */
.lgl-field {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

.lgl-note {
  position: absolute;
  left: 50%;
  bottom: .6rem;
  transform: translateX(-50%);
  margin: 0;
  padding: .3rem .7rem;
  border-radius: .3rem;
  background: var(--walnut-deep);
  color: var(--card);
  font-size: .9rem;
  max-width: 90%;
  text-align: center;
}

/* The lobby. Nobody rides until every rider has pressed this, so the
   countdown never runs against a page that is still loading: here that is
   not a wasted stage but a wasted ROUND, because an unsteered rider is in a
   wall in half a second and its line is on the field for everybody else
   until the round ends. Directly under the field, where a thumb already is.
   Sniper's panel in the same words. */
.lgl-ready {
  display: flex;
  justify-content: center;
  margin: .8rem auto 0;
  max-width: 34rem;
}
.lgl-ready-button {
  min-width: 12rem;
  padding: .7rem 1.4rem;
  font-size: 1.05rem;
}
.lgl-ready-button[disabled] { opacity: .55; }

.lgl-cannot {
  margin: .8rem auto;
  max-width: 34rem;
  padding: .6rem .8rem;
  border-left: 3px solid var(--peg-red);
  background: var(--surface-2);
}

.lgl-seats {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: .3rem 1.4rem;
  margin: .7rem auto;
  padding: 0;
  max-width: 34rem;
}

/* A row reads as one sentence: "Alice (you) starts on the left, 0 rounds".
   NOT a flex container: every text node between the spans would become an
   anonymous flex item and the row would read as a list of fragments, which
   is a bug the owner found on Snake's page. */
.lgl-seat { line-height: 1.5; }
.lgl-seat[data-you="1"] .lgl-seat-name { font-weight: 600; }

.lgl-swatch {
  display: inline-block;
  width: .7rem;
  height: .7rem;
  margin-right: .35rem;
  vertical-align: -1px;
  border-radius: .2rem;
  background: var(--peg-blue);
}

/* The seat order the client draws with: this seat is always the first
   colour, so your own line is brass in every chair. */
.lgl-seat.brass     .lgl-swatch { background: var(--brass); }
.lgl-seat.peg-blue  .lgl-swatch { background: var(--peg-blue); }
.lgl-seat.peg-green .lgl-swatch { background: var(--peg-green); }
.lgl-seat.peg-amber .lgl-swatch { background: var(--peg-amber); }

.lgl-reading { max-width: 34rem; margin: .6rem auto; }
.lgl-rounds { max-width: 34rem; margin-inline: auto; }

/* Nothing on this page animates in CSS: the field, the riders and the one
   dark flash where a rider went out are all the client's, on a canvas, and it
   turns its own slide off under the same preference. The rule is here so the
   next hand to add a transition to a seat row finds the preference already
   answered rather than having to remember it. */
@media (prefers-reduced-motion: reduce) {
  .lgl-note { transition: none; }
}

/* ---- jackpot (plan_jackpot/06) --------------------------------------------
 *
 * A WOODEN BOX, because the Thai game is played on one (plan_jackpot/00, D7):
 * a shallow teak tray with the nine tiles hinged along one edge and the dice
 * thrown in beside them. NOT a casino table: no baize, no reel, no chip, no
 * gilt. docs/product.md bans the appearance as well as the substance.
 *
 * EVERY SELECTOR AND EVERY CUSTOM PROPERTY HERE IS PREFIXED jp-, and that is
 * not tidiness. `.die`, `.die i`, `.die.rolled` and `.die-wrap` at :727-736
 * are LUDO'S, `.bg-dice .die` at :810 is backgammon's, and `--pip` at :950 is
 * scoped to .dom-tile and is NOT a :root token. Yacht shipped both of those
 * bugs and the owner caught them by looking at the page.
 *
 * AND THE RULE IS NOT THE PREFIX, IT IS THE REASON FOR IT: a token named for
 * a THING must not be read by anything that is not that thing. The prototype
 * proved that twice in an afternoon. The pips were painted in the page's ink,
 * which is light in the dark theme, onto a die face that is light in BOTH
 * themes because a die is a white cube: the faces were right, the pips were
 * drawn, and they were invisible. Then the buttons went light-on-light for
 * the same reason. So the die has --jp-die, --jp-die-edge and --jp-pip, and
 * the tiles have their own, and neither reads the other's.
 */
:root {
  --jp-wood: #b5823f; --jp-wood-dark: #8a5d27; --jp-wood-edge: #6e4a1d;
  --jp-tray: #b8863c;
  --jp-tile: #f3e7d0; --jp-tile-edge: #d9c49b; --jp-tile-ink: #3a2a14;
  --jp-lit: #2f6f4f; --jp-litbg: #fff6dd;
  --jp-down: #9c6d30; --jp-letter: #f0d9a8;
  /* the die is light in both themes, so the pip is dark in both */
  --jp-die: #fffdf8; --jp-die-edge: #b9b2a4; --jp-pip: #23201b;
}
@media (prefers-color-scheme: dark) {
  :root {
    --jp-wood: #6d4a20; --jp-wood-dark: #513716; --jp-wood-edge: #3a2711;
    --jp-tray: #6b4a20;
    --jp-tile: #e4d5b8; --jp-tile-edge: #a98f63; --jp-tile-ink: #2e2110;
    --jp-lit: #e8c46a; --jp-litbg: #fff3d2;
    --jp-down: #5b3e19; --jp-letter: #f3dda9;
    --jp-die: #ece7dc; --jp-die-edge: #8d8779; --jp-pip: #23201b;
  }
}

/* min-width: 0 IS LOAD BEARING. `article.game` is a grid (:403), so this
 * section is a grid item, and a grid item's automatic minimum is its
 * CONTENT'S minimum rather than zero. Without this a wide box pushes every
 * block on the page sideways, the shared status line included. */
.jp-table { min-width: 0; }

.jp-frame {
  min-width: 0;
  background:
    repeating-linear-gradient(90deg, rgba(0,0,0,.055) 0 2px, rgba(255,255,255,.035) 2px 5px, rgba(0,0,0,0) 5px 11px),
    linear-gradient(160deg, var(--jp-wood) 0%, var(--jp-wood-dark) 68%, var(--jp-wood) 100%);
  border: 1px solid var(--jp-wood-edge);
  border-radius: 12px;
  padding: 10px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.22), inset 0 -2px 6px rgba(0,0,0,.30), 0 2px 5px rgba(0,0,0,.28);
}

/* The tray. The canvas lies OVER the server-rendered pips: with no script the
 * pips are the dice, which is why they are in the markup at all. */
.jp-tray { position: relative; height: 106px; }
.jp-canvas { display: block; width: 100%; height: 100%; border-radius: 7px;
  box-shadow: inset 0 3px 7px rgba(0,0,0,.42), inset 0 -1px 0 rgba(255,255,255,.10); }
.jp-dice { position: absolute; inset: 0; display: flex; align-items: flex-end;
  justify-content: center; gap: 2.2rem; padding-bottom: .5rem;
  background: var(--jp-tray); border-radius: 7px;
  box-shadow: inset 0 3px 7px rgba(0,0,0,.42); }
.jp-die { width: 44px; height: 44px; border-radius: 7px;
  background: var(--jp-die); border: 1px solid var(--jp-die-edge); padding: 4px; }
.jp-pips { display: grid; grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr); width: 100%; height: 100%; place-items: center; }
.jp-pips i { width: 6px; height: 6px; border-radius: 50%; background: transparent; }
.jp-pips i.on { background: var(--jp-pip); }

/* the lid rail the tiles hinge from */
.jp-rail { height: 5px; margin: 9px 2px 7px;
  background: linear-gradient(var(--jp-wood-edge), var(--jp-wood-dark));
  border-radius: 3px; box-shadow: 0 1px 0 rgba(255,255,255,.14); }

/* ONE ROW where there is width, as the real box has. The phone rule below
 * folds it to three rows of three. THE ARITHMETIC: one row needs
 * 9 * 44 + 8 * 6 = 444 px inside the frame before a tile meets the 44 px tap
 * floor, and the frame and the page each pad 10 a side, so the viewport must
 * be at least 484. The fold is at 520 with headroom. plan_jackpot/01 measured
 * a first draft that folded at 420 and was 41.3 px wide at EVERY width. */
.jp-box { display: grid; grid-template-columns: repeat(9, 1fr); gap: 6px; min-width: 0; }
.jp-tile {
  font: 700 1.3rem/1 Georgia, "Times New Roman", serif; color: var(--jp-tile-ink);
  background: linear-gradient(var(--jp-tile), #e8d9bc);
  border: 1px solid var(--jp-tile-edge); border-radius: 4px;
  min-height: 60px; min-width: 0; padding: 0; cursor: pointer; touch-action: manipulation;
  box-shadow: 0 2px 0 rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.75);
  transition: transform .12s ease, background .12s ease;
}
.jp-tile.lit { border-color: var(--jp-lit); background: linear-gradient(var(--jp-litbg), #f6e6b8);
  box-shadow: 0 0 0 2px var(--jp-lit), 0 2px 0 rgba(0,0,0,.22); }
.jp-tile[disabled] { cursor: default; }

/* A SHUT TILE HAS FLIPPED DOWN, so what shows is the floor of the box beneath
 * it rather than a greyed-out button. */
.jp-tile.down {
  background: repeating-linear-gradient(90deg, rgba(0,0,0,.07) 0 2px, rgba(255,255,255,.03) 2px 5px, rgba(0,0,0,0) 5px 11px), var(--jp-down);
  color: var(--jp-letter); border-color: var(--jp-wood-edge);
  box-shadow: inset 0 3px 5px rgba(0,0,0,.42);
  transform: scaleY(.86); transform-origin: top;
  text-shadow: 0 -1px 0 rgba(0,0,0,.55), 0 1px 0 rgba(255,255,255,.18);
}

/* the ornament tiles, the 1 and the 9: a pip cut into the floor rather than
 * a letter, so JACKPOT sits centred under tiles 2 to 8. */
.jp-tile.down.orn { color: transparent; text-shadow: none; position: relative; }
.jp-tile.down.orn::after {
  content: ''; position: absolute; left: 50%; top: 50%; width: 9px; height: 9px;
  margin: -4.5px 0 0 -4.5px; border-radius: 50%;
  background: rgba(0,0,0,.42); box-shadow: 0 1px 0 rgba(255,255,255,.16);
}
/* THE PAYOFF. Every tile down is a jackpot, and the whole word lights. */
.jp-box.shut .jp-tile.down { color: #fff4d2;
  box-shadow: inset 0 3px 5px rgba(0,0,0,.42), 0 0 10px rgba(232,196,106,.45); }
.jp-box.shut .jp-tile.down.orn::after { background: rgba(255,244,210,.55); }

.jp-controls { display: flex; gap: .6rem; align-items: center; margin: .6rem 0 .2rem; min-height: 44px; }
.jp-say { margin: .2rem 0; min-height: 2.4em; }
.jp-tally, .jp-note { margin: .2rem 0; }

@media (prefers-reduced-motion: reduce) { .jp-tile { transition: none; } }

/* The phone rule, THEN the landscape rule. The order matters: with the
 * landscape rule first the phone's width wins at 667 x 375 and the tiles read
 * under 44 px. plan_match's prototype cost a run to find that. */
@media (max-width: 520px) {
  .jp-tray { height: 92px; }
  .jp-box { grid-template-columns: repeat(3, 1fr); gap: 7px; }
  .jp-tile { min-height: 54px; }
  .jp-dice { gap: 1.6rem; }
}
@media (max-height: 460px) and (orientation: landscape) {
  .jp-tray { height: 74px; }
  .jp-die { width: 38px; height: 38px; }
  .jp-frame { padding: 8px; }
  .jp-rail { margin: 6px 2px 5px; }
  .jp-tile { min-height: 46px; font-size: 1.1rem; }
  .jp-say { min-height: 1.4em; }
}

/* ---- snakes and ladders ---------------------------------------------------------------
   ONE SVG and not a grid with an overlay: nothing on this board is tappable (the die is
   the only control), and one coordinate system is what lets a token slide ALONG a drawn
   snake rather than across the board. It also steps around both of the recorded grid
   traps rather than navigating them: there is no max-content to widen the .game column,
   and no percentage row gap resolving against an unknown height. */
.sl { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.sl-board { width: min(100%, 520px); height: auto; display: block; margin: 0 auto;
            touch-action: manipulation; user-select: none; -webkit-user-select: none; }
.sl-cell { fill: var(--surface); stroke: var(--line); stroke-width: .012; }
.sl-cell.alt { fill: var(--surface-2); }
/* the number carries a halo of the cell colour, because a long snake lies over a dozen
   of them and the board has to stay readable under it */
.sl-num { fill: var(--muted); font: 400 .26px var(--body, system-ui, sans-serif);
          paint-order: stroke; stroke: var(--surface); stroke-width: .05; }
.sl-pad-rect { fill: var(--surface-2); stroke: var(--line); stroke-width: .012; }
.sl-pad-label { fill: var(--muted); font: 400 .3px var(--body, system-ui, sans-serif); }
.sl-ladder-rail { stroke: var(--peg-amber); stroke-width: .10; fill: none; stroke-linecap: round; }
.sl-ladder-rung { stroke: var(--brass); stroke-width: .075; stroke-linecap: round; }
.sl-snake-body { stroke: var(--peg-green); stroke-width: .24; fill: none; stroke-linecap: round; opacity: .92; }
.sl-snake-belly { stroke: color-mix(in srgb, var(--peg-green) 45%, white);
                  stroke-width: .09; fill: none; stroke-linecap: round; stroke-dasharray: .18 .22; }
.sl-snake-head { fill: var(--peg-green); }
.sl-snake-eye { fill: var(--surface); }
.sl-spine { fill: none; stroke: none; }
.sl-token { stroke: rgba(0, 0, 0, .35); stroke-width: .03; }
.sl-token.red { fill: var(--peg-red); } .sl-token.blue { fill: var(--peg-blue); }
.sl-token.green { fill: var(--peg-green); } .sl-token.amber { fill: var(--peg-amber); }
.sl-token.grey { fill: var(--muted); }
.sl-token.me { stroke: var(--ink); stroke-width: .06; }

/* THE DIE IS THE CONTROL: the thing a person taps is the thing that rolls. 72px where
   there is room and never under 44, which is mark 7 of plan_snakes_and_ladders/01. */
.sl-dierow { display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
             justify-content: center; min-height: 84px; }
.sl-die { width: 72px; height: 72px; padding: 6px; border-radius: 14px;
          background: var(--surface); border: 2px solid var(--line);
          box-shadow: 0 3px 6px var(--shadow); cursor: pointer;
          display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); gap: 2px; }
.sl-die span { display: block; }
.sl-die i { display: block; border-radius: 50%; background: var(--ink); width: 78%; height: 78%;
            align-self: center; justify-self: center; }
.sl-die:disabled { opacity: .5; cursor: default; }
.sl-die.tumbling { animation: sl-tumble .42s linear infinite; }
@keyframes sl-tumble {
  0% { transform: rotate(0deg) scale(1); } 25% { transform: rotate(90deg) scale(1.06); }
  50% { transform: rotate(180deg) scale(1); } 75% { transform: rotate(270deg) scale(1.06); }
  100% { transform: rotate(360deg) scale(1); }
}
.sl-die.landed { animation: sl-land .22s ease-out; }
@keyframes sl-land { from { transform: scale(1.14); } to { transform: scale(1); } }
.sl-seats { display: flex; gap: 6px 16px; flex-wrap: wrap; justify-content: center;
            list-style: none; margin: 0; padding: 0; font-variant-numeric: tabular-nums; }
.sl-seats li { display: inline-flex; align-items: center; gap: 6px; }
.sl-seats li.on-turn { font-weight: 600; }
.sl-seats .dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
.sl-say { min-height: 2.6em; text-align: center; margin: 0; }
@media (max-width: 560px) { .sl-die { width: 60px; height: 60px; } .sl-dierow { min-height: 70px; gap: 10px; } }
/* Reduced motion is handled by turning the beat's timings to nothing in the client, so
   there is one beat and not two; these two rules stop the CSS half. */
@media (prefers-reduced-motion: reduce) { .sl-die.tumbling, .sl-die.landed { animation: none; } }

/* ---- pig ------------------------------------------------------------------------------ */
/* EVERY SELECTOR AND CUSTOM PROPERTY IS PREFIXED `pig-`. Not tidiness: the
   generic `.die` rules at the top of this file belong to Ludo, and an
   unprefixed `.die` here would have restyled that board. Jackpot prefixed its
   whole block for the same reason.

   THE COLOURS ARE THE SHARED TOKENS AND NEVER LITERALS, so the dark theme
   works by construction rather than by a second block that has to be
   remembered. Jackpot's dice rendered BLANK in the dark theme and no assertion
   saw it; the pips here are var(--card-ink) on var(--card), which is the one
   pairing the theme guarantees is legible in both. */
.pig-table { display: flex; flex-direction: column; align-items: center; gap: .7rem; }

/* The settled numbers: one row of seats, each a name over a score. */
.pig-seats { display: flex; gap: .5rem; width: 100%; max-width: 26rem; }
.pig-seat { flex: 1; text-align: center; padding: .4rem .2rem; border-radius: 8px;
            background: color-mix(in srgb, var(--ink) 4%, transparent); }
.pig-seat.on { box-shadow: inset 0 0 0 2px var(--accent); }
.pig-seat b { display: block; font-size: .7rem; font-weight: 600; color: var(--muted);
              text-transform: uppercase; letter-spacing: .08em; }
.pig-seat .pig-score { font-size: 1.7rem; font-weight: 600; font-variant-numeric: tabular-nums; }

/* THE NUMBER AT RISK, and it is deliberately unlike a score: larger, lighter,
   alone, and it changes colour as it grows. plan_pig/06 mark 10 is somebody
   checking on a handset that the two are never confused. */
.pig-turn { text-align: center; padding: .3rem 0 0; }
.pig-turn b { display: block; font-size: .7rem; font-weight: 600; color: var(--muted);
              text-transform: uppercase; letter-spacing: .08em; }
.pig-turn .pig-total { font-size: 3rem; font-weight: 300; line-height: 1;
                       font-variant-numeric: tabular-nums; }
.pig-turn.risky .pig-total { color: var(--accent); }

/* A die is pips and never a numeral. */
.pig-die { width: 4.6rem; height: 4.6rem; border-radius: 12px; padding: .55rem;
           background: var(--card); border: 1px solid var(--line);
           box-shadow: 0 2px 4px color-mix(in srgb, var(--ink) 18%, transparent); }
.pig-pips { display: grid; grid-template-columns: repeat(3, 1fr);
            grid-template-rows: repeat(3, 1fr); width: 100%; height: 100%; }
.pig-pips i { width: .62rem; height: .62rem; border-radius: 50%;
              align-self: center; justify-self: center; background: transparent; }
.pig-pips i.on { background: var(--card-ink); }
.pig-unthrown { opacity: .45; }
.pig-die.pig-rolling { animation: pig-tumble .52s ease-out; }
@keyframes pig-tumble {
  0%   { transform: rotate(0) scale(1); }
  45%  { transform: rotate(170deg) scale(.86); }
  100% { transform: rotate(360deg) scale(1); }
}

/* Two buttons, both over the 44px tap floor, both reachable with one thumb in
   portrait without shifting grip. */
.pig-controls { display: flex; gap: .6rem; width: 100%; max-width: 20rem; }
.pig-btn { flex: 1; min-height: 3rem; border: 0; border-radius: 10px;
           font: 600 1.05rem/1 inherit; color: var(--accent-ink); cursor: pointer; }
.pig-roll { background: var(--accent); }
.pig-hold { background: color-mix(in srgb, var(--ink) 62%, var(--card)); }
.pig-btn:disabled { opacity: .45; cursor: default; }
.pig-say { min-height: 1.2rem; text-align: center; margin: 0; }
.pig-note { text-align: center; max-width: 34ch; margin: .2rem auto 0; font-size: .8rem; }

@media (prefers-reduced-motion: reduce) { .pig-die.pig-rolling { animation: none; } }

/* ---- durak (plan_durak/08) --------------------------------------------------

   Scoped under .dk, and it restates NOTHING of .card. The house card markup at
   the top of this file is what a durak card is: .card, .card.red, .card.sm,
   .card .idx, .card .pip, .card.back and .card.static, unchanged. site.css has
   already paid once for a game block that redefined .card and damaged two games
   that knew nothing about it.

   Appended, never written back from a copy read minutes ago: the Hex block a
   thousand lines up records being lost exactly that way. */

.dk { display: flex; flex-direction: column; align-items: center; gap: .8rem; }

/* THE TOP ROW: the talon with the turned up trump lying crosswise under it, the
   trump in words, the heap, and how many cards the other seat holds. Four items
   that wrap on a narrow screen rather than shrinking. */
.dk-top { display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
          gap: .9rem 1.2rem; width: 100%; }

.dk-talon { position: relative; display: inline-flex; align-items: center;
            min-height: 96px; padding-left: 34px; }

/* The turn-up sits UNDER the stack and turned, which is how the card lies on a
   real table: the rank stays readable while the talon covers the rest of it. It
   is the last card drawn, so it is also how a player sees the talon end. */
.dk-turnup { position: absolute; left: -10px; top: 12px; transform: rotate(90deg);
             transform-origin: center; z-index: 0; }
.dk-stack { position: relative; z-index: 1; }
.dk-count { font-variant-numeric: tabular-nums; font-weight: 600; margin-left: .35rem; }

.dk-trump { display: inline-flex; align-items: baseline; gap: .3rem; margin: 0; }
.dk-trump-glyph { font-size: 1.3rem; line-height: 1; }
.dk-trump-glyph.red { color: var(--card-red); }

.dk-heap, .dk-theirs { display: inline-flex; align-items: center; }

/* THE BOUT. Each pair is an attack card with its answer laid over it, offset so
   both ranks are readable: that is the source's own layout and it is the one
   that matters, because the rank of every card on the table is what may be
   thrown in next. The row wraps; it never scrolls sideways. */
.dk-bout { display: flex; flex-wrap: wrap; justify-content: center; align-items: flex-start;
           gap: .7rem; min-height: 104px; width: 100%;
           background: linear-gradient(170deg, #6a4529, var(--walnut) 40%, var(--walnut-deep));
           border-radius: 10px; padding: .7rem .6rem;
           box-shadow: inset 0 2px 8px rgba(0,0,0,.35), 0 3px 10px var(--shadow); }

.dk-pair { position: relative; display: inline-block; }
.dk-pair .dk-beat { position: absolute; left: 14px; top: 16px; }

/* An attack nobody has answered is RINGED, not labelled: a label would be a
   sentence to translate for something the layout already says, and there is
   never more than one of these outside a pile-on.

   --brass AND NOT --accent, and --card and not --ink below, for the rule this
   file states in the mahjong block: NEVER A FLIPPING TOKEN ON A FIXED
   BACKGROUND. The felt is --walnut, which the dark theme does not redefine;
   --accent does flip, from #7a4b22 to #d3aa55, so a ring painted with it is
   brown on brown in the LIGHT theme and only shows up at dusk. --brass and
   --card are fixed in both themes, like the walnut they sit on. */
.dk-pair.open > .card { outline: 2px solid var(--brass); outline-offset: 2px; }

.dk-empty { margin: auto; color: var(--card); opacity: .85; }

/* A hand reaches eighteen cards after two big bouts are taken. It WRAPS: a
   horizontal scroller hides cards a player has to count, and counting them is
   the game. */
.dk-hand { display: flex; flex-wrap: wrap; justify-content: center; gap: .4rem;
           width: 100%; }

/* A six card attack is twelve cards on the table. Below 420px they go to the
   small face and the answers sit tighter, which keeps a full bout inside the
   width rather than pushing the hand off the bottom of a phone. */
@media (max-width: 420px) {
  .dk-bout .card { --w: 46px; }
  .dk-pair .dk-beat { left: 10px; top: 11px; }
  .dk-bout { min-height: 84px; gap: .5rem; }
  .dk-talon { padding-left: 26px; min-height: 80px; }
  .dk-turnup { left: -8px; top: 8px; }
}

/* ---- mineduel (plan_mine_duel/06) -----------------------------------------
 *
 * Every class and every custom property is prefixed `mdu-`, and the game's own
 * colours are tokens scoped to `.mdu` rather than added to :root, so a flag
 * colour cannot repaint another game's board. `md-` and `mn-` were both free
 * too; `mdu-` was chosen because Lightlines had to rename `ll-` to `lgl-` for
 * exactly the reason a two-letter prefix is a bad idea.
 *
 * THERE IS NO --mdu-reach AND THERE MUST NOT BE. Dots and Boxes grows a tap
 * target past its paint with a ::before that reaches into the boxes beside it
 * (--dab-reach, about two thousand lines up), and that works because a LINE
 * reaches into BOXES: two different elements that do not compete for the same
 * pixels. Here every cell's neighbour is another cell, so the same trick on all
 * 256 would overlap and the cell a thumb hits would stop being decidable. In a
 * dense grid the tap target cannot exceed the cell, full stop. The COMMIT
 * BUTTON is the 44px target instead, which is the whole of the arm-then-commit
 * rule: a tap on the grid only arms, and a mis-tap costs nothing.
 * ------------------------------------------------------------------------- */
:root { --mdu-vh: 100vh; }

.mdu { --mdu-hidden: #cfc7b5; --mdu-open: #f6f3ec; --mdu-armed: #b8860b;
       --mdu-raise: rgba(255,255,255,.75); --mdu-sink: rgba(0,0,0,.22);
       --mdu-c1: #2874a6; --mdu-c2: #1e8449; --mdu-c3: #b03a2e; --mdu-c4: #8e44ad;
       --mdu-c5: #117864; --mdu-c6: #6c3483; --mdu-c7: #935116; --mdu-c8: #424949;
       min-width: 0; margin: 0 0 .8rem; }
:root:not([data-theme="light"]) .mdu {
       --mdu-hidden: #575048; --mdu-open: #201e1a;  --mdu-armed: #d4a017;
       --mdu-raise: rgba(255,255,255,.16); --mdu-sink: rgba(0,0,0,.45);
       --mdu-c1: #5dade2; --mdu-c2: #58d68d; --mdu-c3: #ec7063; --mdu-c4: #bb8fce;
       --mdu-c5: #45b39d; --mdu-c6: #af7ac5; --mdu-c7: #d68910; --mdu-c8: #aab7b8; }

/* THE ARITHMETIC, because the first draft got it wrong by a gap. Sixteen cells
 * at the 44px floor need 16*44 + 15 gaps + 2 padding = 721px inside the frame,
 * so 44rem (704px) gives 42.9 and misses; 46rem is 736 and gives 44.9. A 375px
 * phone gives 21.6 and cannot meet the floor at all.
 *
 * AND grid-template-rows IS NOT OPTIONAL: `aspect-ratio: 1` sizes the BOX, but
 * auto rows grow to their content and overflow it. The prototype measured cells
 * 42.9 wide by 55.3 TALL on a board that was supposed to be square. */
.mdu-board { display: grid; grid-template-columns: repeat(16, 1fr);
  grid-template-rows: repeat(16, 1fr); gap: 1px;
  width: min(100%, 46rem); aspect-ratio: 1; margin: 0 auto;
  background: var(--line); padding: 1px; border-radius: 4px;
  touch-action: manipulation; }

/* A COVERED CELL IS RAISED AND AN OPEN ONE IS FLAT, and that difference is not
 * carried by colour alone. The prototype shipped #3b372f against #2e2b26 in the
 * dark theme, thirteen points apart on one channel, and an opened ZERO was
 * indistinguishable from a covered cell: every number was right, every
 * assertion passed, and what a player could not see was which parts of the
 * board had already been cleared. Found by LOOKING at the page, which is how
 * Jackpot lost its pips and Yacht lost a build twice. */
.mdu-cell { display: flex; align-items: center; justify-content: center; padding: 0;
  border: 0; border-radius: 1px; background: var(--mdu-hidden); color: var(--ink);
  font: inherit; font-weight: 700; line-height: 1; cursor: pointer; min-width: 0;
  font-size: clamp(.5rem, 1.7vw, .95rem);
  -webkit-user-select: none; user-select: none;
  box-shadow: inset 1px 1px 0 var(--mdu-raise), inset -1px -1px 0 var(--mdu-sink); }
.mdu-cell:disabled { cursor: default; }
.mdu-open { background: var(--mdu-open); box-shadow: none; }
.mdu-armed { outline: 3px solid var(--mdu-armed); outline-offset: -3px; z-index: 1; }

/* A FLAG IS DRAWN AND IS NEVER A CHARACTER. A Unicode flag glyph renders
 * differently on every platform and not at all on some, which is the Goofspiel
 * suit mojibake Yacht recorded; and a percentage BORDER WIDTH is invalid CSS,
 * so the prototype's first triangle silently rendered as nothing at all. A
 * clip-path sized off the cell survives both. The shape matters as well as the
 * colour: four seat colours are not four distinguishable colours to every
 * reader. */
.mdu-claim { color: #fff; position: relative; }
.mdu-claim::after { content: ""; position: absolute; inset: 24% 28%;
  background: #fff; clip-path: polygon(0 0, 100% 50%, 0 100%); }
.mdu-claim-p1 { background: var(--peg-red); }
.mdu-claim-p2 { background: var(--peg-blue); }
.mdu-claim-p3 { background: var(--peg-green); }
.mdu-claim-p4 { background: var(--peg-amber); }

.mdu-n1 { color: var(--mdu-c1); } .mdu-n2 { color: var(--mdu-c2); }
.mdu-n3 { color: var(--mdu-c3); } .mdu-n4 { color: var(--mdu-c4); }
.mdu-n5 { color: var(--mdu-c5); } .mdu-n6 { color: var(--mdu-c6); }
.mdu-n7 { color: var(--mdu-c7); } .mdu-n8 { color: var(--mdu-c8); }

.mdu-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin: .6rem 0; }
.mdu-commit { min-width: 44px; min-height: 44px; }
/* --surface-2 AND NOT --panel. The first draft wrote var(--panel), and there
 * is no --panel token in this file: an UNDEFINED custom property fails as a
 * silent inherit, not as an error, so the readout would have taken whatever
 * background it inherited and could have been ink-on-ink in one theme and fine
 * in the other. semantic-76 lost two rules to exactly this on the same day.
 * `does this token exist` and `does it work in both themes` are one check. */
.mdu-read { margin: .4rem 0; min-height: 2.6em; padding: .4rem .6rem;
  border-radius: 6px; background: var(--surface-2); color: var(--ink); }
.mdu-note { margin: .2rem 0; min-height: 1.3em; font-size: .9rem; }
.mdu-tally { margin: .4rem 0; font-size: .92rem; }

@media (max-width: 520px) { .mdu-read { font-size: .9rem; } }

/* THE LANDSCAPE RULE COMES AFTER THE PHONE RULE, or the phone width wins and
 * the cells shrink; Match measured that exact failure.
 *
 * AND IT EXISTS BECAUSE A SQUARE 256-CELL BOARD CANNOT SHARE A 375px TALL
 * VIEWPORT WITH ITS OWN CONTROLS. The prototype measured a 643px board in a
 * 375px viewport, which put Commit 797px down the page: on a phone held
 * sideways the button that makes the move was off screen. So the board is
 * capped by the HEIGHT and the controls sit beside it.
 *
 * `grid-row: 1 / -1` DOES NOT DO THAT: -1 is the last line of the EXPLICIT
 * grid, of which there is none here, so the board collapses into row 1, sets
 * that row's height, and the controls stack underneath it anyway. Use a span. */
@media (orientation: landscape) and (max-height: 560px) {
  .mdu { display: grid; grid-template-columns: auto minmax(190px, 1fr);
         column-gap: 10px; align-items: start; }
  /* THREE WAYS, AND THE THIRD IS THE ONE THIS SHIPPED WITHOUT. The board is
   * capped by the viewport HEIGHT here, which is the whole point of the rule;
   * but column 2 is `minmax(190px, 1fr)`, so a board sized off the height and a
   * controls column with a 190px floor can add up to more than the viewport is
   * WIDE, and then the page scrolls sideways. Measured on the real page at
   * 500 x 497 (a resized window, or a small tablet in a split view): the board
   * came out 479, the grid 679, and the document 697px in a 500px viewport.
   * A real phone in landscape never hit it - 667 x 375 gives 357 + 200 = 557 -
   * which is why looking at a phone would not have found it.
   *
   * `100vw` and not `100%`: column 1 is `auto`, so a percentage here resolves
   * against a column that is sized by this very declaration. Checkers and
   * Reversi write the same three-way min for the same reason. */
  .mdu-board { grid-column: 1; grid-row: 1 / span 20; align-self: start;
               min-width: 0;
               width: min(calc(var(--mdu-vh) - 18px), calc(100vw - 220px), 46rem); }
  .mdu > *:not(.mdu-board) { grid-column: 2; }
  .mdu-read { min-height: 0; }
}

/* ---- Word Duel (plan_jotto/06) ----------------------------------------------
   THE MARK MUST SURVIVE LOSING THIS BLOCK. Every cell carries its state three
   ways: data-mark for these rules, a glyph that is a character in the
   document, and an sr-only sentence from the catalogue. Delete this file and
   the board is still readable, which is mark 12 of the gate and an
   accessibility requirement first.

   So NO RULE BELOW IS THE ONLY CARRIER OF A STATE. The colours are the site's
   own peg tokens, not a green and yellow chosen to look like somebody else's
   game. */
.wd-columns { display: flex; gap: 1.25rem; flex-wrap: wrap; }
.wd-side { flex: 1 1 15rem; min-width: 0; }
.wd-who { font-family: var(--display); font-size: 1rem; margin: 0 0 .35rem; }

.wd-word, .wd-rows { list-style: none; margin: 0 0 .6rem; padding: 0; }
.wd-word { display: flex; gap: .25rem; }
.wd-rows { display: flex; flex-direction: column; gap: .25rem; }
.wd-row { display: flex; gap: .25rem; }

.wd-cell {
  display: inline-flex; align-items: center; justify-content: center;
  position: relative;
  width: 2.1rem; height: 2.1rem;
  border: 1px solid var(--line); border-radius: 3px;
  background: var(--surface); color: var(--ink);
  font-family: var(--display); font-size: 1.05rem; text-transform: uppercase;
}
.wd-letter { line-height: 1; }

/* The glyph is in the DOCUMENT, not generated here: with the stylesheet gone
   it must still be read. All this does is tuck it into a corner. */
.wd-glyph {
  position: absolute; right: 2px; bottom: 0;
  font-size: .6rem; line-height: 1; opacity: .75;
}

.wd-cell[data-mark="right"] { background: var(--peg-green); color: var(--accent-ink); border-color: var(--peg-green); }
.wd-cell[data-mark="moved"] { background: var(--peg-amber); color: var(--hole); border-color: var(--peg-amber); }
.wd-cell[data-mark="absent"] { background: var(--ground); color: var(--muted); }
.wd-cell.unknown, .wd-cell.empty { background: var(--surface); color: var(--muted); border-style: dashed; }

/* A cracked row is marked by weight as well as by every cell being green. */
.wd-row.got .wd-cell { box-shadow: 0 0 0 2px var(--brass) inset; }

.wd-word.hidden-word .wd-cell.unknown { border-style: dashed; }

.wd-setup { margin: 0 0 1rem; }
.wd-setup-actions { display: flex; gap: .5rem; flex-wrap: wrap; margin: .5rem 0; }

.wd-entry { margin: 1rem 0 .5rem; }
.wd-entry .wd-word.entry { margin-bottom: .6rem; }

.wd-keys { display: flex; flex-wrap: wrap; gap: .25rem; max-width: 26rem; }
.wd-key {
  flex: 1 0 1.9rem;
  min-width: 44px; min-height: 44px;     /* the phone floor, mark 17 */
  border: 1px solid var(--line); border-radius: 3px;
  background: var(--surface); color: var(--ink);
  font-family: var(--body); font-size: .95rem; text-transform: uppercase;
  cursor: pointer;
}
.wd-key.wide { flex: 2 0 4rem; }
.wd-key.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.wd-key[disabled] { opacity: .5; cursor: default; }

/* THE KEYBOARD REMEMBERS, and the state came from the stash. The key is
   marked the same three ways the cells are: colour here, and the letter plus
   its state in the DOM for everything else. */
.wd-key[data-mark="right"] { background: var(--peg-green); color: var(--accent-ink); border-color: var(--peg-green); }
.wd-key[data-mark="moved"] { background: var(--peg-amber); color: var(--hole); border-color: var(--peg-amber); }
.wd-key[data-mark="absent"] { background: var(--ground); color: var(--muted); text-decoration: line-through; }

@media (max-width: 30rem) {
  /* NOT MEASURED YET, AND THIS COMMENT SAYS SO RATHER THAN IMPLYING OTHERWISE.
     Two grids plus a 26-key keyboard is more than Word Grid's 349px board,
     which was already tight under 220px of site chrome, so the cells and the
     gaps shrink here as a first cut. Whether the board clears the keyboard on
     a real phone needs the page over HTTP and headless Chrome at 375px, which
     cannot run until the register line exists: it is mark 17 of the gate and
     an open item in plan_jotto/07. If it does not fit, the opponent's column
     collapses to its heading and its last row, and that decision is taken
     with the measurement in hand. */
  .wd-columns { gap: .6rem; }
  .wd-cell { width: 1.8rem; height: 1.8rem; font-size: .95rem; }
  .wd-keys { max-width: none; }
}

/* ---- Big Two (plan_big_two/07) --------------------------------------------
   The roster's first climbing game. One column: the other seats, the
   combination in play, your hand, two buttons.

   `min-width: 0` ON THE SECTION IS LOAD-BEARING. article.game is a grid, and a
   grid item's automatic minimum is its CONTENT's, so the `overflow-x: auto`
   below does nothing without it and an eighteen-card hand pushes the whole
   SITE off a 360px phone. Measured, not guessed
   (reference_grid_item_minimum_is_its_content). */
.bt { min-width: 0; display: flex; flex-direction: column; gap: .7rem; }

.bt-seats { display: flex; flex-wrap: wrap; gap: .4rem; }
.bt-seat { display: flex; align-items: baseline; gap: .35rem;
           padding: .25rem .5rem; border: 1px solid var(--line); border-radius: .3rem; }
.bt-seat.on-turn { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
.bt-seat.is-out { opacity: .6; }
.bt-seat.me { background: var(--paper-2, transparent); }
.bt-name { font-weight: 600; }
.bt-count { font-variant-numeric: tabular-nums; }
.bt-note { font-size: .85em; }

.bt-table-wrap { display: flex; flex-direction: column; align-items: center; gap: .3rem;
                 padding: .6rem; border: 1px solid var(--line); border-radius: .4rem; }
.bt-combo { font-weight: 600; text-transform: lowercase; }
.bt-table { display: flex; gap: .2rem; }
.bt-table-who { font-size: .85em; }
.bt-lead-note { text-align: center; }

/* THE HAND WRAPS RATHER THAN SCROLLING. A long hand is the NORMAL case at three
   seats, where one chair is dealt eighteen, and a horizontal scroller hides
   cards a player has to count to play well. */
.bt-hand { display: flex; flex-wrap: wrap; gap: .2rem; }

/* A CARD IS A TOGGLE. `picked` lifts it rather than recolouring it, so the suit
   colour still reads and the state survives a colour-blind eye. */
.bt-card { cursor: pointer; transition: transform .08s ease-out; }
.bt-card.picked { transform: translateY(-.5rem); box-shadow: 0 .15rem .3rem rgba(0,0,0,.25); }
.bt-card[disabled] { cursor: default; }

.bt-must { text-align: center; }
.bt-controls { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
.bt-why { flex: 1 1 100%; font-size: .9em; }

.bt-order-wrap { border-top: 1px solid var(--line); padding-top: .5rem; }
.bt-order-head { font-size: 1rem; margin: 0 0 .3rem; }
.bt-order { margin: 0; padding-left: 1.2rem; }
.bt-order-row { display: flex; gap: .5rem; }
.bt-order-place { font-variant-numeric: tabular-nums; min-width: 1.2rem; }

@media (max-width: 30rem) {
  /* 44px TAP TARGETS on a phone. The site's shared .button is 41px, which is
     an open item for the owner across several games; this game does not change
     a site-wide rule on its own, so its OWN buttons are sized here. */
  .bt-controls .button { min-height: 44px; }
  .bt-card { min-width: 44px; min-height: 60px; }
  .bt-seats { gap: .3rem; }
  .bt-seat { padding: .2rem .35rem; font-size: .9em; }
}

/* ---- Ninegrid (plan_sudoku/06) --------------------------------------------
 *
 * A grid of eighty-one buttons. The box edges are marked in the MARKUP
 * (`edge-r`, `edge-b` from the stash) rather than by nth-child arithmetic here,
 * so a mistake in them is visible in the HTML and testable.
 *
 * THE BOARD IS SIZED BY ITS COLUMN, NOT BY THE VIEWPORT, and the cell has no
 * width floor. Both of those were the other way round and the ninth column was
 * cut off the right of every phone (found by the owner, 27 Sep 2026). `92vw` is
 * measured against the window while the board sits inside body's 1rem padding,
 * so it was already 2px too wide at 375px; then `min-width: 2.5rem` on a
 * content-box cell made each column 42px whatever the container said, nine of
 * them 378px inside 341px, and a flex item at its minimum does not shrink. A
 * floor that the container cannot honour does not make the target bigger, it
 * moves the last column off the screen, so the grid takes the width it actually
 * has and the cells divide it.
 */
.ng { display: flex; flex-direction: column; align-items: center; gap: .75rem; }

.ng-grid { display: flex; flex-direction: column;
           border: 2px solid var(--card-ink); background: var(--card);
           box-sizing: border-box; width: 100%; max-width: 27rem; }
.ng-row  { display: flex; }

.ng-cell { flex: 1 1 0; aspect-ratio: 1 / 1; min-width: 0; box-sizing: border-box;
           display: flex; align-items: center; justify-content: center; position: relative;
           border: 1px solid var(--card-edge); background: transparent; cursor: pointer;
           font: inherit; font-size: 1.25rem; line-height: 1; color: var(--peg-blue);
           padding: 0; }
.ng-cell.edge-r { border-right-width: 2px; border-right-color: var(--card-ink); }
.ng-cell.edge-b { border-bottom-width: 2px; border-bottom-color: var(--card-ink); }

/* A printed digit is the puzzle's, in the body colour and bold; the player's own
 * are in the accent, so the two are never confused. */
.ng-cell.given   { color: var(--card-ink); font-weight: 700; cursor: default; }
.ng-cell.selected { outline: 3px solid var(--accent); outline-offset: -3px; }
.ng-cell.clash   { background: var(--peg-red); }
.ng-cell.hinted  { background: color-mix(in srgb, var(--accent) 22%, transparent); }
.ng-cell.noting  { box-shadow: inset 0 0 0 2px var(--card-ink); }

.ng-d { display: block; }
.ng-marks { position: absolute; inset: 2px 2px auto 2px;
            font-size: .55rem; letter-spacing: .04em; line-height: 1;
            color: color-mix(in srgb, var(--card-ink) 55%, transparent); pointer-events: none; }
.ng-marks.has { opacity: 1; }

/* TEN COLUMNS, so nine digits and the erase sit on ONE row under the grid and
 * line up with it. As a wrapping flexbox the tenth key dropped to a second row
 * on a narrow screen, which the owner saw straight away. */
.ng-pad { display: grid; grid-template-columns: repeat(10, 1fr); gap: .3rem;
          box-sizing: border-box; width: 100%; max-width: 27rem; }
.ng-key { min-height: 2.75rem; min-width: 0; box-sizing: border-box; position: relative;
          border: 1px solid var(--card-edge); background: var(--card); cursor: pointer;
          font: inherit; font-size: 1.15rem; color: var(--card-ink); }
.ng-key.done { opacity: .45; }
.ng-kn { position: absolute; top: 2px; right: 4px; font-size: .55rem;
        color: color-mix(in srgb, var(--card-ink) 55%, transparent); }
.ng-erase { font-size: 1.3rem; }

.ng-bars { list-style: none; margin: 0; padding: 0;
           box-sizing: border-box; width: 100%; max-width: 27rem;
           display: flex; flex-direction: column; gap: .3rem; }
.ng-bar  { display: flex; align-items: center; gap: .5rem; font-size: .85rem; }
.ng-name { flex: 0 0 6rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ng-track { flex: 1 1 auto; height: .5rem; background: var(--line); position: relative; }
.ng-fill  { display: block; height: 100%; background: var(--accent); transition: width .2s; }
.ng-count { flex: 0 0 4.5rem; text-align: right; color: var(--muted);
            font-variant-numeric: tabular-nums; }
.ng-bar[data-status="finished"] .ng-fill { background: var(--ink); }

@media (prefers-reduced-motion: reduce) {
    .ng-fill { transition: none; }
}

/* WHILE THE RACE IS NOT LIVE the board says so, because a tap that would do
 * nothing should look like it would do nothing. Before the Ready button existed
 * the page looked fully playable in the lobby and every tap came back 422. */
.ng.waiting .ng-grid, .ng.waiting .ng-pad { opacity: .55; }
.ng.waiting .ng-cell, .ng.waiting .ng-key { cursor: default; }
/* ---- ultimate tic-tac-toe ---------------------------------------------------------------- */
/* Nine boards of nine. The one thing a player must never have to work out is
   which board is live, so a live board differs from a shut one in BORDER
   WEIGHT and BACKGROUND LIGHTNESS as well as hue: a hue-only cue is invisible
   to a colour-blind reader and to anybody outdoors. Gate mark 15 checks it
   with hue removed.

   THE GUTTER BETWEEN BOARDS IS PART OF THE BOARD, not a gap between targets.
   `gap` on the outer grid puts dead space between two live buttons, and a
   finger that lands in it does nothing while looking as though it should have
   worked. So the separation is drawn with the board's own padding and a
   border, and the outer grid has no gap at all.

   box-sizing is set PER RULE on this site and never globally, which is what
   made Maker and Breaker's .mb-side compute to 380px. Every box here sets it. */
.utt { display: block; }
.utt-meta { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0;
            width: min(100%, 396px); margin: 0 0 .8rem; box-sizing: border-box;
            background: var(--walnut); border-radius: 10px; padding: 3px;
            box-shadow: inset 0 2px 8px rgba(0,0,0,.35), 0 3px 10px var(--shadow); }
.utt-board { position: relative; display: grid; grid-template-columns: repeat(3, 1fr);
             gap: 1px; padding: 3px; box-sizing: border-box;
             border: 2px solid transparent; border-radius: 6px;
             background: var(--felt-line, #1a4f35); }
.utt-cell { aspect-ratio: 1; border: 0; border-radius: 3px; padding: 0;
            background: var(--card); color: var(--card-ink); box-sizing: border-box;
            font-family: var(--display); font-size: clamp(.75rem, 3.4vw, 1.5rem);
            line-height: 1; cursor: pointer; min-width: 0;
            transition: background .15s ease; }
.utt-cell:disabled { cursor: default; opacity: 1; }   /* a taken square is not a greyed control */
.utt-cell:disabled:empty { opacity: .82; }
.utt-x { color: var(--peg-red); }
.utt-o { color: var(--peg-blue); }
.utt-cell:focus-visible { outline: 3px solid var(--accent); outline-offset: -1px; }
.utt-hinted { outline: 3px dashed var(--brass); outline-offset: -3px; }

/* A live board: brighter ground, a solid bright border, and a lift. Three
   cues, only one of which is hue. */
.utt-live { border-color: var(--brass); background: var(--felt, #14663f);
            box-shadow: 0 0 0 2px var(--brass); }

/* A decided board: darker, flat, and stamped with its owner. The cells stay
   readable because the game is still about them. */
.utt-shut { background: rgba(0,0,0,.42); border-color: transparent; }
.utt-shut .utt-cell { opacity: .45; }
.utt-stamp { position: absolute; inset: 0; display: none; align-items: center;
             justify-content: center; pointer-events: none;
             font-family: var(--display); font-size: 3rem; line-height: 1; }
.utt-shut .utt-stamp { display: flex; }
.utt-won-x .utt-stamp { color: var(--peg-red); }
.utt-won-o .utt-stamp { color: var(--peg-blue); }
.utt-drawn .utt-stamp { color: var(--ink-dim, #9aa); }
.utt-win { box-shadow: 0 0 0 3px var(--brass); }

/* PORTRAIT HEIGHT CAP. This is the first board on the site with 81 cells, and
   at a phone width the square grid is as tall as it is wide, so the status
   line and the controls can be pushed off the fold by the board alone. The cap
   is on the board's WIDTH, because the grid is square and capping the width is
   what caps the height. */
@media (max-height: 760px) {
    .utt-meta { width: min(100%, min(396px, 62vh)); }
}

/* ---- the front page's time control (user, 25 Sep 2026) --------------------
   Right of the grid's top edge, above the tiles: how long a move gets in
   whatever is started from the roster.

   The same shape as `.lang-switch` in the footer and for the same reason: a
   real form with a real button, which app.js hides once its change handler is
   attached. The button is NOT hidden by a rule here, because that would leave
   anybody whose JavaScript failed to load with a form and no way to submit it.

   `justify-content: flex-end` and not `text-align: right`: flex-end is the end
   of the writing direction, so the control sits on the left in Arabic and
   Hebrew, which is where the end of a line is there.

   `.limit-pick` on the house convention of a per-feature prefix. Every token
   is one :root declares - checked, not assumed: an undefined custom property
   is a silent inherit and draws wrong without erroring. */
.limit-pick { display: flex; align-items: center; justify-content: flex-end;
              flex-wrap: wrap; gap: .4rem; margin: 0 0 .8rem; }
.limit-pick label { color: var(--muted); font-size: .9rem; }
.limit-pick select { font: inherit; font-size: .9rem; color: var(--ink);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 6px; padding: .15rem .3rem; }
.limit-pick button[hidden] { display: none; }

/* ---- fusegrid (plan_fusegrid/06) ------------------------------------------
   Fusegrid owns THE SCREEN here and not a cell of the field: the field is a
   canvas the client draws, so this block is the stack the canvas fills, the two
   thumbs, the lobby press, the seat list and the round list.

   THE PREFIX IS THE WHOLE KEY, `.fusegrid-`, and it was claimed by message on
   25 Sep 2026, the day the game was named, rather than when this block was
   written. A short prefix is the bug Lightlines and Landlord found in each
   other's way: both shorten to `ll` just as naturally, Landlord took `.ll-` and
   its eight --ll-set-* properties, and Lightlines moved to `.lgl-` before either
   block existed. site.css is ONE stylesheet at runtime whatever it looks like in
   the file, so a shared prefix shows up months later as one game's page wearing
   the other's colours, with every test green.

   IT DECLARES NO PALETTE. Every colour here is a var() off :root, and the
   renderer reads the same tokens for the canvas, so the page and the field
   cannot drift apart. t/fusegrid-07-page.t asserts both halves: no hex literal
   in this block, and the renderer taking its tokens from the document.

   NO vw, vh OR WIDTH QUERY SIZES THE FIELD. The canvas is sized by the client
   from its STACK's box, so the page test can measure the field at a chosen width
   by constraining the stack, which is the only way to measure a narrow viewport
   in headless Chrome: --window-size is the outer window and Chrome clamps its
   minimum width to about 500px, so asking for 320 gives an innerWidth of 500
   (measured 25 Sep 2026, and by semantic-99 independently on the same day). A
   width media query would fire off the real viewport and lie about the frame.
   The one query below is on HEIGHT, which the window does control.
   -------------------------------------------------------------------------- */

.fusegrid {
    display: flex;
    flex-direction: column;
    gap: .6rem;
}

/* The stack the canvas fills. THE CANVAS IS ABSOLUTE INSIDE IT and its border is
   a box-shadow: an in-flow canvas sized by the renderer to this box makes the
   column's min-content grow by the border on every ResizeObserver pass until the
   page scrolls sideways (plan_horde/06's first browser finding). */
.fusegrid-stack {
    position: relative;
    width: 100%;
    aspect-ratio: 1 / 1;
    max-height: calc(100dvh - var(--fusegrid-head, 14rem));
    max-width: calc(100dvh - var(--fusegrid-head, 14rem));
    margin: 0 auto;
    background: var(--hole);
    border-radius: 6px;
    box-shadow: inset 0 0 0 2px var(--walnut-deep), 0 1px 3px var(--shadow);
    overflow: hidden;
}

/* 100vh BEFORE 100dvh, so a browser without dvh gets a usable line rather than
   the declaration being dropped and the stack growing to its aspect ratio. */
@supports not (height: 100dvh) {
    .fusegrid-stack {
        max-height: calc(100vh - var(--fusegrid-head, 14rem));
        max-width: calc(100vh - var(--fusegrid-head, 14rem));
    }
}

.fusegrid-field {
    position: absolute;
    inset: 0;
    display: block;
    touch-action: none;
}

/* The thumbs: a four-way pad bottom left and a place button bottom right, both
   in the safe area and OFF the field, so a thumb never covers a cell. They are
   hidden where there is no coarse pointer, because a mouse has the keys. */
.fusegrid-thumbs { display: none; }

@media (pointer: coarse) {
    .fusegrid-thumbs {
        display: block;
        position: fixed;
        inset: auto 0 0 0;
        height: 8.5rem;
        pointer-events: none;
        z-index: 3;
    }
    .fusegrid-pad,
    .fusegrid-place {
        position: absolute;
        bottom: calc(env(safe-area-inset-bottom, 0px) + .75rem);
        pointer-events: auto;
        border-radius: 999px;
        background: color-mix(in srgb, var(--surface) 72%, transparent);
        box-shadow: 0 1px 3px var(--shadow), inset 0 0 0 1px var(--line);
        touch-action: none;
    }
    .fusegrid-pad {
        left: calc(env(safe-area-inset-left, 0px) + .75rem);
        width: 7.5rem;
        height: 7.5rem;
    }
    .fusegrid-place {
        right: calc(env(safe-area-inset-right, 0px) + .75rem);
        width: 5rem;
        height: 5rem;
        background: color-mix(in srgb, var(--brass) 55%, var(--surface));
    }
    /* the pad's four arms, drawn with the ink token and no image */
    .fusegrid-pad::before,
    .fusegrid-pad::after {
        content: "";
        position: absolute;
        inset: 0;
        margin: auto;
        background: color-mix(in srgb, var(--ink) 34%, transparent);
    }
    .fusegrid-pad::before { width: 2.4rem; height: .4rem; border-radius: 2px; }
    .fusegrid-pad::after  { width: .4rem; height: 2.4rem; border-radius: 2px; }
    .fusegrid-place::before {
        content: "";
        position: absolute;
        inset: 0;
        margin: auto;
        width: 1.7rem;
        height: 1.7rem;
        border-radius: 999px;
        background: color-mix(in srgb, var(--ink) 40%, transparent);
    }
}

/* A SHORT SCREEN LOSES THE THUMBS' SIZE AND NOT THE FIELD. The 14rem above is a
   GUESS and the client replaces it: it measures how far the stack actually sits
   from the top of the viewport and writes that back as --fusegrid-head, because
   the shared head wraps to two lines on a phone and no stylesheet can know it.
   Measured 25 Sep 2026 in a 357-tall landscape viewport: a fixed 8rem reserve
   left a 17 px cell where the screen could afford 23, and the only thing that
   knew the difference was the page itself. What is below the field scrolls,
   which is what a page does. */
@media (max-height: 560px) {
    .fusegrid-thumbs { height: 6.5rem; }
    .fusegrid-pad { width: 6rem; height: 6rem; }
    .fusegrid-place { width: 4rem; height: 4rem; }
}

.fusegrid-note,
.fusegrid-cannot {
    margin: 0;
    padding: .45rem .7rem;
    border-radius: 4px;
    background: var(--surface-2);
    color: var(--ink);
    text-align: center;
}

.fusegrid-cannot { background: color-mix(in srgb, var(--danger) 12%, var(--surface)); }

.fusegrid-ready { display: flex; justify-content: center; }

.fusegrid-seats {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem 1rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.fusegrid-seat { display: flex; align-items: center; gap: .35rem; }
.fusegrid-seat[data-you="1"] .fusegrid-seat-name { font-weight: 600; }

/* The swatch is the seat's token and the renderer draws the seat in the same
   one. Four seats, four tokens, all off :root. */
.fusegrid-swatch {
    width: .85rem;
    height: .85rem;
    border-radius: 2px;
    background: var(--brass);
    box-shadow: inset 0 0 0 1px var(--card-edge);
}
.fusegrid-seat[data-seat="p2"] .fusegrid-swatch { background: var(--peg-blue); }
.fusegrid-seat[data-seat="p3"] .fusegrid-swatch { background: var(--peg-red); }
.fusegrid-seat[data-seat="p4"] .fusegrid-swatch { background: var(--peg-green); }

.fusegrid-carries { font-variant-numeric: tabular-nums; }

.fusegrid-rounds { font-variant-numeric: tabular-nums; }

.fusegrid-reading { margin: 0; }

/* ---- xiangqi ---------------------------------------------------------------
   plan_xiangqi/13. A piece sits on an INTERSECTION of nine files and ten ranks,
   with a river across the middle and a palace at each end.

   EVERY COLOUR HERE IS A TOKEN THE DARK BLOCK DOES NOT REDEFINE, and that is the
   whole of why the palette looks like this. Oware shipped an unreadable board
   twice over for the same reason: --accent-ink flips from cream to near-black and
   --ink flips too, so a board painted with one and lettered with the other
   vanishes in one theme and looks fine in the other. The tokens used below
   (--walnut, --walnut-deep, --brass, --card, --card-edge, --card-red, --card-ink,
   --hole) are material colours and are defined once on :root.

   The page test asserts that INVARIANT rather than the colours. A test that pins
   the colour endorses the next instance of the same bug, which is what happened
   to Oware's.

   NO INLINE style= ANYWHERE, so every number is here: Punk::Plugin::CSP emits no
   style-src on an ordinary page, styles fall back to default-src 'self', and an
   inline style attribute is refused. */

.xq { display: flex; flex-direction: column; gap: 4px; align-items: center; }

/* THE BOARD MAY NOT SIT INSIDE A PADDED WRAPPER. At 375 the page leaves 359 and
   the board wants 356: three pixels of room. A container taking 8 of padding puts
   max-width to work and the discs arrive at 34.7 instead of 36.0, with nothing
   erroring and nobody noticing until a thumb misses. So the frame has no padding
   and the width is capped at what phase 01 measured. */
.xq-frame {
    width: 100%;
    max-width: 356px;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: 1fr;
    gap: 2px;
}

.xq-board {
    --xq-cols: 9;
    --xq-rows: 10;
    position: relative;
    display: grid;
    grid-template-columns: repeat(var(--xq-cols), minmax(0, 1fr));
    grid-template-rows: repeat(var(--xq-rows), minmax(0, 1fr));
    aspect-ratio: 9 / 10;
    background: var(--walnut);
    border: 1px solid var(--walnut-deep);
    border-radius: 2px;
    overflow: hidden;

    /* THE TWO PALACES' DIAGONALS, and they are on the board rather than on the
       points ON PURPOSE. A palace is three points by three, which is a TWO BY TWO
       block of cells, and the line from one corner to the other passes corner to
       corner through the middle cell. Four corner points drawing a half each
       leaves the middle undrawn; making the centre draw it too means five points
       must agree about one line, and that is the class of bug that put Reversi's
       discs nowhere at all.

       The arithmetic, once: a cell is 100/9 wide and 100/10 tall. A palace X runs
       from the centre of column 3 to the centre of column 5, so it is 2 cells
       wide (22.222%) and 2 tall (20%), starting at x 3.5/9 = 38.889% and y
       0.5/10 = 5% for the near palace and y 7.5/10 = 75% for the far one.

       background-position with percentages aligns the image's own point to the
       container's, so an offset of 38.889% for a 22.222% image is
       38.889 / (100 - 22.222) = 50%, and 5% for a 20% image is 5 / 80 = 6.25%,
       and 75% is 93.75%. Those four numbers are why they are not the same as the
       four above. */
    background-image:
        linear-gradient(to bottom right, transparent calc(50% - 0.5px), var(--brass) calc(50% - 0.5px), var(--brass) calc(50% + 0.5px), transparent calc(50% + 0.5px)),
        linear-gradient(to top right,    transparent calc(50% - 0.5px), var(--brass) calc(50% - 0.5px), var(--brass) calc(50% + 0.5px), transparent calc(50% + 0.5px)),
        linear-gradient(to bottom right, transparent calc(50% - 0.5px), var(--brass) calc(50% - 0.5px), var(--brass) calc(50% + 0.5px), transparent calc(50% + 0.5px)),
        linear-gradient(to top right,    transparent calc(50% - 0.5px), var(--brass) calc(50% - 0.5px), var(--brass) calc(50% + 0.5px), transparent calc(50% + 0.5px));
    background-size: 22.222% 20%, 22.222% 20%, 22.222% 20%, 22.222% 20%;
    background-position: 50% 6.25%, 50% 6.25%, 50% 93.75%, 50% 93.75%;
    background-repeat: no-repeat;
}

/* THE RIVER'S WORDS. The gap in the lines is drawn by the points either side of
   it; this is only the lettering a real board prints there. */
.xq-river {
    position: absolute;
    left: 0; right: 0;
    top: 40%; height: 10%;
    display: flex; align-items: center; justify-content: space-around;
    pointer-events: none;
    color: var(--brass);
    font-size: 11px;
    letter-spacing: 0.4em;
}

/* A POINT IS A TRANSPARENT SQUARE CENTRED ON A CROSSING, and it draws its own half
   of the two lines that meet there, so the lines pass through the pieces' centres
   BY CONSTRUCTION. A grid of bordered cells would be a chessboard. */
.xq-pt {
    position: relative;
    margin: 0; padding: 0;
    border: 0;
    background: transparent;
    -webkit-appearance: none;
    appearance: none;
    color: inherit;
    font: inherit;
    line-height: 0;
    touch-action: manipulation;
}
.xq-pt:not(:disabled) { cursor: pointer; }
.xq-pt:disabled { opacity: 1; }            /* a point is not a greyed control */

/* the two half-lines: ::before is the file, ::after is the rank */
.xq-pt::before, .xq-pt::after {
    content: '';
    position: absolute;
    background: var(--walnut-deep);
}
.xq-pt::before { left: 50%; top: 0; bottom: 0; width: 1px; transform: translateX(-0.5px); }
.xq-pt::after  { top: 50%; left: 0; right: 0; height: 1px; transform: translateY(-0.5px); }

/* THE OUTERMOST LINES STOP AT THE BOARD'S EDGE rather than running half a cell
   past it: the outer line IS the edge of the playing area. */
.xq-first-row::before { top: 50%; }
.xq-last-row::before  { bottom: 50%; }
.xq-first-col::after  { left: 50%; }
.xq-last-col::after   { right: 50%; }

/* THE RIVER IS A GAP IN THE FILES. The point above it draws nothing downward and
   the point below it nothing upward. The two OUTER files are exempt, because the
   board's own edge runs the whole way down, which is why seven files break and not
   nine. Drawing all nine through the gap is drawing a different board. */
.xq-river-above::before { bottom: 50%; }
.xq-river-below::before { top: 50%; }

/* the piece: a disc with the baked outline on it */
.xq-disc, .xq-mark { position: absolute; display: block; pointer-events: none; }
.xq-disc {
    left: 50%; top: 50%;
    width: 90%; height: 90%;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: transparent;
}
.xq-pc .xq-disc {
    background: var(--card);
    border: 1.5px solid currentColor;
    box-shadow: 0 1px 0 var(--walnut-deep);
}
.xq-glyph {
    position: absolute;
    left: 50%; top: 50%;
    width: 72%; height: 72%;
    transform: translate(-50%, -50%);
    fill: currentColor;
    pointer-events: none;
}
/* THE ARMY IS A COLOUR ON THE POINT AND THE GLYPH INHERITS IT through
   currentColor, which is one set of fourteen shapes filled two ways rather than
   twenty-eight. Both tokens are material colours and neither flips in dark mode. */
.xq-red   { color: var(--card-red); }
.xq-black { color: var(--card-ink); }

/* what a player may pick up, and what they may land on */
.xq-playable .xq-disc { box-shadow: 0 0 0 2px var(--brass), 0 1px 0 var(--walnut-deep); }
.xq-pt.can-land .xq-mark {
    left: 50%; top: 50%;
    width: 34%; height: 34%;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: var(--brass);
    opacity: 0.55;
}
.xq-pt[aria-pressed="true"] .xq-disc { box-shadow: 0 0 0 3px var(--brass), 0 1px 0 var(--walnut-deep); }
.xq-last .xq-disc  { outline: 2px dashed var(--brass); outline-offset: 1px; }
/* THE GENERAL IN CHECK, and it is a ring rather than a colour: a red ring on the
   red army would be invisible, and the whole point is that it is noticed. */
.xq-checked .xq-disc { box-shadow: 0 0 0 3px var(--hole), 0 1px 0 var(--walnut-deep); }
/* a hint is EXCLUSIVE and clears on a move: add-only leaves a ring on every point
   ever suggested, which is the fifth of Oware's five client traps */
.xq-pt.hinted .xq-disc { outline: 2px solid var(--hole); outline-offset: 2px; }

/* the file legends, which turn with the board because a legend labels the column
   it sits under */
.xq-legend { display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); }
.xq-legend i { text-align: center; font-style: normal; font-size: 10px; color: var(--brass); }

/* the material strip: what is MISSING, because that is what a player wants to see */
.xq-taken { display: flex; flex-wrap: wrap; gap: 3px; min-height: 18px; width: 100%; max-width: 356px; }
.xq-gone { position: relative; width: 16px; height: 16px; opacity: 0.75; }
.xq-gone svg { width: 100%; height: 100%; fill: currentColor; }
.xq-gone.xq-r { color: var(--card-red); }
.xq-gone.xq-b { color: var(--card-ink); }
.xq-gone i { position: absolute; right: -2px; bottom: -4px; font-style: normal; font-size: 8px; color: var(--brass); }

.xq-rule { font-size: 12px; color: var(--brass); margin: 2px 0; }

/* ---- xiangqi, second pass ---------------------------------------------------
   THE BOARD WAS PUSHED OFF THE RIGHT EDGE BY THE MOVE LIST, which is worth
   writing down because nothing about it looked like a width bug. `.move-list` is
   the shared class every game uses, and this game puts FORTY-FOUR legal moves in
   it at the opening ("C2.5 C8.5 H2+3 ..."), on one unwrapped line. A flex column
   is as wide as its widest child, so the section became about 500 wide, the
   board was then centred inside THAT, and its right-hand files fell off a 375
   screen. The board itself was the correct 356 the whole time.

   So: the section is capped, and the move list wraps. */
.xq { width: 100%; max-width: 356px; margin-inline: auto; }
.xq .move-list {
    display: flex;
    flex-wrap: wrap;
    gap: 2px 6px;
    width: 100%;
    max-width: 356px;
    max-height: 4.4em;
    overflow-y: auto;
}
.xq .controls { width: 100%; }

/* A REAL BOARD HAS A DOUBLE BORDER, and the outer one is what makes it read as a
   board rather than as a grid. Drawn with an inset shadow rather than a second
   element, so no markup changes. */
.xq-board {
    box-shadow: inset 0 0 0 1px var(--walnut-deep), inset 0 0 0 4px var(--walnut),
                inset 0 0 0 5px var(--walnut-deep);
    border-radius: 3px;
}

/* THE STARRED POINTS: the five soldier points and the two cannon points on each
   side carry little corner ticks on every real board. They are not decoration,
   they are where the soldiers and cannons START, and a player reads the opening
   from them. Drawn as four short brackets around the point, clipped by the board
   edge for the two outer files, which is exactly how a printed board does it. */
.xq-star {
    background-image:
        linear-gradient(var(--walnut-deep), var(--walnut-deep)),
        linear-gradient(var(--walnut-deep), var(--walnut-deep)),
        linear-gradient(var(--walnut-deep), var(--walnut-deep)),
        linear-gradient(var(--walnut-deep), var(--walnut-deep)),
        linear-gradient(var(--walnut-deep), var(--walnut-deep)),
        linear-gradient(var(--walnut-deep), var(--walnut-deep)),
        linear-gradient(var(--walnut-deep), var(--walnut-deep)),
        linear-gradient(var(--walnut-deep), var(--walnut-deep));
    background-size: 7px 1px, 1px 7px, 7px 1px, 1px 7px,
                     7px 1px, 1px 7px, 7px 1px, 1px 7px;
    background-position:
        calc(50% - 11px) calc(50% - 10px), calc(50% - 10px) calc(50% - 11px),
        calc(50% + 11px) calc(50% - 10px), calc(50% + 10px) calc(50% - 11px),
        calc(50% - 11px) calc(50% + 10px), calc(50% - 10px) calc(50% + 11px),
        calc(50% + 11px) calc(50% + 10px), calc(50% + 10px) calc(50% + 11px);
    background-repeat: no-repeat;
}
/* the outer files show only their inward half, as a printed board does */
.xq-star.xq-first-col { background-position:
        -99px -99px, -99px -99px,
        calc(50% + 11px) calc(50% - 10px), calc(50% + 10px) calc(50% - 11px),
        -99px -99px, -99px -99px,
        calc(50% + 11px) calc(50% + 10px), calc(50% + 10px) calc(50% + 11px); }
.xq-star.xq-last-col { background-position:
        calc(50% - 11px) calc(50% - 10px), calc(50% - 10px) calc(50% - 11px),
        -99px -99px, -99px -99px,
        calc(50% - 11px) calc(50% + 10px), calc(50% - 10px) calc(50% + 11px),
        -99px -99px, -99px -99px; }

/* the pieces: a little more presence, and a ring inside the rim as a real one has */
.xq-pc .xq-disc {
    background:
        radial-gradient(circle at 38% 32%, rgba(255,255,255,0.55), transparent 55%),
        var(--card);
    border: 1.5px solid currentColor;
    box-shadow: inset 0 0 0 2px var(--card), inset 0 0 0 3px currentColor,
                0 1px 2px rgba(0,0,0,0.35);
}
.xq-glyph { width: 62%; height: 62%; }
.xq-river { font-size: 10px; letter-spacing: 0.5em; opacity: 0.75; }
.xq-legend i { font-size: 9px; }

/* ---- xiangqi, third pass: the lines were painted OVER the pieces -------------
   ::before and ::after are painted in tree order around a button's children, so
   ::after, which draws the rank line, lands ON TOP of the disc and the glyph. The
   board read as a grid drawn over the pieces rather than under them, and a rule
   I had added to lift the starred points made it worse by lifting the lines too.

   The board is BELOW, the pieces are ABOVE, said once. */
.xq-pt::before, .xq-pt::after { z-index: 0; }
.xq-disc  { z-index: 2; }
.xq-glyph { z-index: 3; }
.xq-mark  { z-index: 4; }

/* a lit destination is a place you may tap, so it says so and looks tappable */
.xq-pt.can-land { cursor: pointer; }
.xq-pt.can-land:not(.xq-pc) .xq-mark { z-index: 1; }
/* landing ON a piece is a capture: ring the piece rather than dotting it */
.xq-pt.can-land.xq-pc .xq-disc {
    box-shadow: inset 0 0 0 2px var(--card), inset 0 0 0 3px currentColor,
                0 0 0 3px var(--hole), 0 1px 2px rgba(0,0,0,0.35);
}

/* ---- xiangqi: a hint must be unmistakable ------------------------------------
   The hint ringed its two points with `outline: 2px solid var(--hole)` and the
   LAST MOVE ringed its two with `outline: 2px dashed var(--brass)`. Solid against
   dashed, on a board that already has gold rings on every playable piece: the
   owner had a hint on screen, could not tell which of two identical cannons it
   meant, and asked. Two things that mean different things must not look alike.

   So the hint pulses and is the only thing on the board that moves. */
.xq-pt.hinted .xq-disc {
    outline: 3px solid var(--hole);
    outline-offset: 2px;
    animation: xq-hint 1.1s ease-in-out infinite;
}
/* the destination of a hint has no piece on it, so the ring needs a body to sit
   on: give the empty disc one, or the outline draws around nothing visible */
.xq-pt.hinted:not(.xq-pc) .xq-disc {
    background: color-mix(in srgb, var(--hole) 22%, transparent);
    border: 2px dashed var(--hole);
}
@keyframes xq-hint {
    0%, 100% { outline-color: var(--hole); }
    50%      { outline-color: var(--brass); }
}
/* A PERSON WHO HAS ASKED FOR HELP IS NOT READING NOTATION. The last move stays a
   quiet dashed ring; the hint is the loud one. */
.xq-last .xq-disc { outline: 2px dashed var(--brass); outline-offset: 1px; }
@media (prefers-reduced-motion: reduce) {
    .xq-pt.hinted .xq-disc { animation: none; }
}

/* ------------------------------------------------------------------------------
   Around the World (plan_aroundtheworld/08). Every rule is namespaced under .atw,
   which is the board class, so nothing here can reach another game's page.

   APPENDED AND NEVER REWRITTEN: this file is edited by every session on the site
   and a reflow of the rules above would be unreviewable.

   THE COURT'S OWN COLOURS ARE --atw-* TOKENS DEFINED HERE, because an undefined
   CSS variable is a SILENT INHERIT: the rule applies, the value comes from
   somewhere else, and nothing errors. So "does the token exist" and "is the dark
   theme covered" are one question, and the dark half follows minigolf's three
   selectors rather than a bare media query, so an explicit light choice still
   wins. The shared tokens used below (--ink, --muted, --line, --accent, --brass)
   are all defined twice in this file, light and dark, and are safe. */
/* THE COURT'S PALETTE, and the client READS THESE OFF THE ELEMENT rather than
   carrying colours of its own, so the hoop follows the page's theme. The names are
   the prototype's render.js's, because that renderer is carried across unchanged and
   a renamed token would be a silent inherit rather than an error. The light values
   are the prototype's court.html and the dark values its dark block: both were drawn
   and looked at before they were written down. */
.atw {
    --atw-floor: #b5763c; --atw-line: rgba(255,255,255,.55);
    --atw-you: var(--accent); --atw-them: var(--brass);
    --atw-wallTop: #dfe3e8; --atw-wallBot: #c7ced6; --atw-steel: #8d959c;
    --atw-glass: rgba(210,226,236,.55);
    --atw-sheenA: rgba(255,255,255,.16); --atw-sheenB: rgba(255,255,255,.02);
    --atw-boardEdge: #ffffff; --atw-pad: #2f3a44;
    --atw-rim: #ef7038; --atw-rimDark: #b8431a; --atw-net: #f2f2ee;
    --atw-ball: #d2803a; --atw-ballHi: #f0a860; --atw-ballLine: #5c3312;
    --atw-ring: var(--atw-rim);
}
:root:not([data-theme="light"]) .atw {
    --atw-floor: #6d4622;
    --atw-wallTop: #1d242b; --atw-wallBot: #141a20; --atw-steel: #6a737b;
    --atw-glass: rgba(120,160,185,.20);
    --atw-sheenA: rgba(255,255,255,.10); --atw-sheenB: rgba(255,255,255,.01);
    --atw-boardEdge: #e8eef2; --atw-pad: #223039;
    --atw-rim: #ff7a3d; --atw-rimDark: #a83c14; --atw-net: #dfe3e6;
    --atw-ball: #d88a44; --atw-ballHi: #f3ac63; --atw-ballLine: #3a2210;
}
@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .atw {
        --atw-floor: #b5763c;
        --atw-wallTop: #dfe3e8; --atw-wallBot: #c7ced6; --atw-steel: #8d959c;
        --atw-glass: rgba(210,226,236,.55);
        --atw-sheenA: rgba(255,255,255,.16); --atw-sheenB: rgba(255,255,255,.02);
        --atw-boardEdge: #ffffff; --atw-pad: #2f3a44;
        --atw-rim: #ef7038; --atw-rimDark: #b8431a; --atw-net: #f2f2ee;
        --atw-ball: #d2803a; --atw-ballHi: #f0a860; --atw-ballLine: #5c3312;
    }
}
:root[data-theme="dark"] .atw {
    --atw-floor: #6d4622;
    --atw-wallTop: #1d242b; --atw-wallBot: #141a20; --atw-steel: #6a737b;
    --atw-glass: rgba(120,160,185,.20);
    --atw-sheenA: rgba(255,255,255,.10); --atw-sheenB: rgba(255,255,255,.01);
    --atw-boardEdge: #e8eef2; --atw-pad: #223039;
    --atw-rim: #ff7a3d; --atw-rimDark: #a83c14; --atw-net: #dfe3e6;
    --atw-ball: #d88a44; --atw-ballHi: #f3ac63; --atw-ballLine: #3a2210;
}

.atw { display: grid; gap: .6rem; justify-items: center; }

/* THE SERVER-DRAWN COURT. Page.pm emits the inner content of the SVG and every
   piece of it carries a class, because the CSP forbids an inline style and a
   `fill` attribute in Perl would be a colour no theme could override. A class
   here that the markup lacks, or a class in the markup that is missing here,
   fails t/aroundtheworld-07-page.t: Reversi's did not agree and every game looked
   empty until the script ran. */
.atw .atw-floor { fill: var(--atw-floor); }
.atw .atw-baseline { stroke: var(--atw-line); stroke-width: 50; }
.atw .atw-arc { fill: none; stroke: var(--atw-line); stroke-width: 50; }
.atw .atw-board { stroke: var(--ink); stroke-width: 60; stroke-linecap: round; }

/* THE RING IN TWO PIECES, so the ball can pass between them. The front piece is
   drawn last and is the brighter of the two, which is what makes the opening read
   as an opening rather than as a flat disc. */
.atw .atw-ring-back { fill: none; stroke: var(--atw-ring); stroke-width: 45; opacity: .55; }
.atw .atw-ring-front { fill: none; stroke: var(--atw-ring); stroke-width: 55; }

.atw .atw-spot { fill: none; stroke: var(--atw-line); stroke-width: 30; }
.atw .atw-done { stroke: var(--atw-you); }
.atw .atw-mark { stroke: none; }
.atw .atw-mark-you { fill: var(--atw-you); }
.atw .atw-mark-them { fill: var(--atw-them); opacity: .8; }

/* THE STACK holds the static SVG and the canvas in the same box, so swapping one
   for the other cannot move the page. */
.atw .atw-stack { position: relative; width: min(100%, 720px); aspect-ratio: 9400 / 7200;
                  border-radius: 10px; overflow: hidden; background: var(--atw-floor); touch-action: none; }
.atw .atw-court { display: block; width: 100%; height: 100%; }
.atw .atw-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* WITH NO SCRIPT the SVG is the game and the canvas is not there; with script the
   client adds `scripted` and the two swap. The canvas keeps its own `hidden` until
   the client clears it, so a stylesheet-only load shows nothing empty. */
.atw .needs-script { display: none; }
.atw.scripted .needs-script { display: revert; }
.atw.scripted .atw-canvas[hidden] { display: none; }

/* THE SHOOTER'S VIEW IS NOT THE PLAN VIEW'S SHAPE. The stack is sized by the static
   SVG's extent (9400 by 7200 mm of court); the canvas looks at a hoop. Leaving the
   stack's ratio in place STRETCHED THE HOOP HORIZONTALLY, which is a large part of
   why the first live page looked nothing like the prototype. Once the client is in,
   the stack takes the canvas's shape, and the floor colour goes with it because the
   canvas paints its own wall. */
.atw.scripted .atw-stack { aspect-ratio: 16 / 10; background: var(--atw-wallBot); }
.atw.scripted .atw-court { visibility: hidden; }
.atw.scripted .atw-strip { opacity: .85; }

/* THE LAP STRIP: seven cells, in words, readable with no picture at all. */
.atw .atw-strip { display: flex; flex-wrap: wrap; gap: .3rem; list-style: none; margin: 0; padding: 0;
                  justify-content: center; }
.atw .atw-cell { border: 1px solid var(--line); border-radius: 999px; padding: .12rem .55rem;
                 font-size: .82rem; color: var(--muted); }
.atw .atw-cell-done { border-color: var(--atw-you); color: var(--ink); }
.atw .atw-cell-you { outline: 2px solid var(--atw-you); outline-offset: 1px; color: var(--ink); }
.atw .atw-cell-them { border-style: dashed; border-color: var(--atw-them); }
.atw .atw-cell-word { white-space: nowrap; }
.atw .atw-where { margin: 0; color: var(--muted); font-size: .9rem; text-align: center; }

/* THERE IS NO AIM BOX. You aim AT THE RING, on the canvas, in the rim's own
   perspective, so the thing you drag is the thing the ball flies through. The
   ellipse and the crosshair are drawn by the client because they are in a
   projection the stylesheet cannot express; what belongs here is the cursor, the
   focus ring and the touch behaviour.

   A separate square box below the court was the first attempt and it was wrong in
   the way the owner named: the aimer belongs in the hoop, not beneath the game. */
.atw .atw-canvas { cursor: crosshair; touch-action: none; }
.atw .atw-canvas:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.atw .atw-aimread { margin: 0; text-align: center; font-size: .8rem; }

/* THE BAR. The sweet band is placed from the engine's own SWEET and DEAD, handed
   over as data attributes, so this file has no copy of them. */
.atw .atw-barwrap { width: min(100%, 420px); display: grid; gap: .2rem; }
.atw .atw-bar { position: relative; height: 26px; border: 1px solid var(--line); border-radius: 6px;
                background: var(--atw-glass); overflow: hidden; cursor: pointer; }
.atw .atw-bar:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.atw .atw-sweet { position: absolute; top: 0; bottom: 0; left: 47%; width: 6%; background: var(--atw-you); opacity: .5; }
.atw .atw-marker { position: absolute; top: -2px; bottom: -2px; width: 3px; background: var(--ink); left: 0; }
.atw .atw-power { position: absolute; right: .35rem; top: 50%; transform: translateY(-50%);
                  font-size: .78rem; color: var(--ink); }
.atw .atw-barlabels { display: flex; justify-content: space-between; font-size: .72rem; color: var(--muted); }

.atw .atw-controls { display: flex; gap: .4rem; flex-wrap: wrap; justify-content: center; }
.atw .atw-help, .atw .atw-noscript { margin: 0; text-align: center; font-size: .85rem; }

/* THE SHEET: where each seat stands, and every shot as a sentence. */
.atw .atw-sheet { width: min(100%, 560px); display: grid; gap: .4rem; }
.atw .atw-tally { display: grid; gap: .2rem; }
.atw .atw-row { display: flex; justify-content: space-between; align-items: center; gap: .5rem; }
.atw .atw-who { display: flex; align-items: center; gap: .35rem; }
.atw .atw-at { font-variant-numeric: tabular-nums; }
.atw .atw-list { margin: 0; padding-left: 1.2rem; font-size: .88rem; color: var(--muted); }

/* A NARROW PHONE: the court keeps its shape and nothing scrolls sideways. */
@media (max-width: 420px) {
    .atw .atw-stack { width: 100%; }
    .atw .atw-cell { font-size: .76rem; padding: .1rem .45rem; }
}

/* REDUCED MOTION: the client already schedules no frame, and the marker stops
   moving here so a paused sweep is not a flicker. */
@media (prefers-reduced-motion: reduce) {
    .atw .atw-marker { transition: none; }
}

/* ---- Briscola (plan_briscola/06) -------------------------------------------
 *
 * THE PREFIX IS `bri-` AND NOT `bs-`, which is what this plan first specified.
 * `.bs-*` and the bare `.bs` section class belong to WARSHIP, which is live:
 * .bs-grid, .bs-cell, .bs-fleet, .bs-ship and a dozen more, plus
 * `<section class="bs">` in warship.tmpl. Two games sharing a prefix is the
 * same class of mistake as Lightlines nearly shipping as `ll-` beside Ludo's
 * `lu-`, and it is worse here because the collision is with a game already on
 * the roster. Checked before a line was written; `bri-` had no hits at all.
 *
 * NOTHING HERE RESTATES `.card`. This file already carries two warnings about
 * a game block that redefined it and damaged two other games, so this block
 * adds layout classes and `.card` MODIFIERS only.
 */
.bri { display: grid; gap: .9rem; justify-items: center; width: min(100%, 34rem); }

/* The stock, the trump line and their pile, across the top. */
.bri-top { display: grid; grid-template-columns: auto 1fr auto; align-items: center;
           gap: .8rem; width: 100%; }

/* THE TURN-UP LIES CROSSWISE UNDER THE STOCK, which is how the card sits on a
 * real table and is the one piece of this layout that is not decoration: the
 * trump is public all deal and it is the last card anybody draws. */
.bri-stock { position: relative; display: grid; justify-items: start;
             min-width: 5.4rem; min-height: 4.6rem; }
.bri-stock .bri-turnup { position: absolute; left: 1.1rem; top: .5rem;
                         transform: rotate(90deg); transform-origin: center; z-index: 0; }
.bri-stock .bri-stack  { position: absolute; left: 0; top: 0; z-index: 1; }
.bri-count { position: absolute; left: 0; bottom: -1.1rem; font-size: .8rem; }

.bri-trump { display: flex; align-items: center; gap: .35rem; justify-content: center;
             margin: 0; }
.bri-trump-glyph { font-size: 1.3rem; line-height: 1; }

.bri-theirs { position: relative; display: grid; justify-items: end;
              min-width: 3.4rem; min-height: 4.6rem; }
.bri-theirs .bri-count { left: auto; right: 0; }

/* THE TRICK IS A SINGLE SLOT AND NOT A ROW: two seats, so the second card
 * resolves it at once and at most one card is ever waiting. It keeps its height
 * when empty so the hand does not jump up and down between turns. */
.bri-trick { display: grid; place-items: center; min-height: 5.2rem; width: 100%; }
.bri-empty { margin: 0; font-size: .85rem; }

/* Three cards, the smallest hand on the roster. `min()` and not `max-content`:
 * a grid takes an item's max-content as its minimum and a phone then scrolls
 * sideways. */
.bri-hand { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center;
            width: min(100%, 22rem); }
.bri-hand .card { cursor: pointer; }
.bri-hand .card[disabled] { cursor: default; opacity: .55; }

.bri-score { display: flex; gap: 1.4rem; justify-content: center; font-size: .9rem; }
.bri-points b { font-variant-numeric: tabular-nums; }

/* A NARROW PHONE: nothing scrolls sideways and the three cards stay on one row. */
@media (max-width: 420px) {
    .bri { gap: .7rem; }
    .bri-hand { gap: .35rem; }
    .bri-top { gap: .5rem; }
}

/* THE REVEAL: both cards of a finished trick, side by side for a beat before
 * the table clears (plan_briscola/06, the owner's report that the opponent's
 * card was never seen). The winner is ringed, so which card took it is carried
 * by more than position. */
.bri-trick { gap: .5rem; grid-auto-flow: column; }
.bri-answer { margin-left: .2rem; }
.bri-took { outline: 2px solid var(--accent, #8a6d3b); outline-offset: 2px; border-radius: .4rem; }

/* ---- cascade (plan_cascade/08) ---------------------------------------------
   Cascade. Every class starts `csc-`. A SEVEN BY SEVEN GRID OF BUTTONS, and
   seven is a measurement rather than a taste: plan_cascade/01 mark 4 found
   eight columns clearing the 44 px tap floor at 375x667 (44.7) and failing
   360x640 (42.9), 667x375 (43.8) and 640x360 (41.9). Seven clears all five
   sizes at 47.9 to 56.6.

   THE HEIGHT BUDGET IS A VARIABLE, `--csc-vh`, whose value is 100vh. Headless
   Chrome refuses a window under about 500x520, so while the arithmetic reads
   100vh directly no probe can measure this board at a phone's height at all.
   Named, the probe sets it to the phone's real height and measures THE SHIPPED
   FORMULA rather than a copy of it (the copied-verifier trap,
   index_tests_that_lie). Nothing else in this block may read 100vh, and
   t/cascade-08-page.t asserts that.

   THE LANDSCAPE RULE COMES AFTER THE PHONE RULE and must stay there. Written
   before it the phone rule's width wins, the portrait reads correctly and the
   landscape does not - which is Match's finding at its own mark 8, and the
   reason the tally sits BESIDE the board on a short screen: a landscape phone
   is 360 to 375 px TALL, and the portrait height budget leaves the board
   185 px.

   AND `min-width: 0`, because article.game is a grid and this section is a grid
   item, whose automatic minimum is its CONTENT'S minimum and not zero: without
   it a seven-wide tally pushes every block on the page sideways on a narrow
   phone, the shared status line included.

   NOT A SCRATCH CARD and not a slot machine (docs/product.md, plan_cascade/00):
   no gradient that reads as foil, no gold, no shine, no 3D transform. A tile is
   a flat tinted face with a flat shape on it, and the SHAPE carries the whole
   difference - the art is tested on its silhouette for exactly that reason, so
   a player who cannot tell the tints apart is playing the same game.

   NO INLINE STYLES IN MARKUP: the CSP has no style-src, and since 1 Oct it has
   none on any page at all (the ad network's own policy went with it). Setting
   `el.style.transform` from script is CSSOM and is NOT refused, which is what
   makes the slide and the fall legal; cascade.js says so where it does it. */
.csc { display: grid; gap: .5rem; justify-items: center; min-width: 0; --csc-vh: 100vh; }

.csc .csc-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
                gap: .5rem .9rem; width: min(100%, 460px); min-height: 2.4rem;
                font-variant-numeric: tabular-nums; }
.csc .csc-phase { text-transform: uppercase; letter-spacing: .06em; font-size: .8rem; color: var(--muted); }
.csc .csc-count { font-weight: 700; font-size: 1.3rem; min-width: 3.2ch; text-align: center; }
.csc .csc-chain { font-weight: 700; font-size: .95rem; color: var(--accent); }
.csc .csc-scores { display: inline-flex; gap: .6rem; }
.csc .csc-seat { font-weight: 600; }
.csc .csc-seat.you .csc-score { text-decoration: underline; }
.csc .csc-note { font-size: .85rem; color: var(--muted); }
.csc .csc-ready { min-width: 8rem; }

/* The grid: square tiles, a PERCENTAGE MARGIN on the cell and never a
   percentage row-gap - a percentage gap in a column of aspect-ratio cells
   resolves against an unknown height and the cells overflow the board, while a
   margin resolves against the width. And `width: min(100%, Npx, ...)`, never
   `max-content`: a board at max-content makes the .game grid column that wide
   and a phone scrolls sideways. */
.csc .csc-grid { position: relative; display: grid; grid-template-columns: repeat(7, 1fr);
                 width: min(100%, 460px, calc(var(--csc-vh) - 190px)); margin: 0 auto;
                 touch-action: none; -webkit-touch-callout: none;
                 user-select: none; -webkit-user-select: none; }
/* 2% AND NOT 3%: the margin is taken from the track twice, so at 360 px the
   tap target measured exactly 44.0 px - the floor itself, with nothing to
   spare. At 2% it is 45.0. */
.csc .csc-tile { position: relative; display: block; width: 96%; aspect-ratio: 1; margin: 2%; padding: 0;
                 border: 2px solid var(--csc-line); border-radius: 18%;
                 background: var(--csc-face, var(--csc-k0)); cursor: pointer; }
.csc .csc-tile[data-k="0"] { --csc-face: var(--csc-k0); }
.csc .csc-tile[data-k="1"] { --csc-face: var(--csc-k1); }
.csc .csc-tile[data-k="2"] { --csc-face: var(--csc-k2); }
.csc .csc-tile[data-k="3"] { --csc-face: var(--csc-k3); }
.csc .csc-tile[data-k="4"] { --csc-face: var(--csc-k4); }
.csc .csc-tile[data-k="5"] { --csc-face: var(--csc-k5); }
.csc .csc-tile:disabled { cursor: default; opacity: 1; }
.csc .csc-tile:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; z-index: 3; }
/* A `<use>` CLONE IS UNREACHABLE BY CLASS, so every per-tile state is styled on
   the button and never on anything inside the clone. */
.csc .csc-tile.csc-picked { outline: 3px solid var(--accent); outline-offset: 2px; z-index: 3; }
.csc .csc-tile.csc-hinted { outline: 3px dashed var(--brass); outline-offset: 2px; z-index: 3; }
.csc .csc-tile[data-mark="streak_h"], .csc .csc-tile[data-mark="streak_v"],
.csc .csc-tile[data-mark="burst"], .csc .csc-tile[data-mark="prism"] {
    border-color: var(--csc-marked); box-shadow: inset 0 0 0 2px var(--csc-marked); }
.csc .csc-face { display: block; width: 86%; height: 86%; margin: 7% auto 0; pointer-events: none; }
/* A DARK FACE AND ITS SHAPE SHARE A HUE and differ only in lightness, so the
   symbol is lifted rather than recoloured: the sheet is one set of shapes for
   both themes (plan_cascade/02). */
:root:not([data-theme="light"]) .csc .csc-face { filter: brightness(1.9) saturate(.8); }
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .csc .csc-face { filter: none; } }
:root[data-theme="dark"] .csc .csc-face { filter: brightness(1.9) saturate(.8); }

/* the two beats of a cascade step: what goes, and what falls. Both are driven
   from script with a duration, because the ramp lives in cascade.js. */
.csc .csc-tile.csc-going { animation: csc-going 170ms ease-in forwards; }
.csc .csc-tile.csc-fired { animation: csc-fired 170ms ease-out forwards; }
.csc .csc-tile.csc-falling { z-index: 2; }
.csc .csc-tile.csc-sliding { z-index: 4; }
@keyframes csc-going { from { transform: scale(1); opacity: 1; } to { transform: scale(.25); opacity: 0; } }
@keyframes csc-fired { 0% { transform: scale(1); } 40% { transform: scale(1.22); } 100% { transform: scale(.3); opacity: 0; } }

/* the points where they were won. Absolutely placed inside the grid, which is
   why the grid is `position: relative`. */
.csc .csc-pop { position: absolute; transform: translate(-50%, -50%); pointer-events: none;
                font: 700 1.05rem/1 var(--body); color: var(--accent);
                text-shadow: 0 1px 2px var(--shadow); animation: csc-pop 900ms ease-out forwards; }
@keyframes csc-pop { 0% { opacity: 0; transform: translate(-50%, -30%) scale(.8); }
                     20% { opacity: 1; transform: translate(-50%, -60%) scale(1); }
                     100% { opacity: 0; transform: translate(-50%, -150%) scale(1); } }

.csc .csc-boardno { font-size: .85rem; }
.csc .csc-boards { margin: 0; padding-left: 1.2rem; font-size: .85rem; color: var(--muted); }
.csc .csc-tally { border-collapse: collapse; width: min(100%, 460px); font-variant-numeric: tabular-nums; }
.csc .csc-tally th, .csc .csc-tally td { text-align: left; padding: .2rem .4rem .2rem 0;
                                         border-bottom: 1px solid var(--line); font-size: .9rem; }
.csc .csc-tally th { color: var(--muted); font-weight: 600; }

/* a phone: the board takes the width it has under a shorter bar */
@media (max-width: 520px) {
  .csc .csc-grid { width: min(100%, calc(var(--csc-vh) - 170px)); }
}
/* A SHORT LANDSCAPE: the bar and the tally BESIDE the board, not above it.
   THIS RULE MUST FOLLOW THE ONE ABOVE: written before it, the phone width wins
   and the board reads 22.4 px tiles (plan_cascade/01 mark 4). */
@media (max-height: 520px) and (orientation: landscape) {
  .csc { grid-auto-flow: column; justify-items: start; align-items: start; gap: .6rem; }
  .csc .csc-bar { flex-direction: column; align-items: flex-start; width: 150px; min-height: 0; }
  .csc .csc-grid { width: min(100%, calc(var(--csc-vh) - 24px)); margin: 0; }
  /* AND THE TALLY GOES WITH THEM. Kept at 190px it steals that width from the
     board: measured 37 px tiles on a 667x375 phone, under the 44 px floor. The
     bar beside the board already carries every seat's score, which is what a
     racer needs mid-board; the tally is the detail and it comes back the moment
     the phone is upright. */
  .csc .csc-boardno, .csc .csc-boards, .csc .csc-tally { display: none; }
}

/* THE TOKENS, and EVERY ONE OF THEM MUST EXIST: an undefined var() voids the
   whole declaration silently, which is how every one of Ninegrid's cell borders
   vanished. Page ink on a card surface is --card-ink and never --ink, which is
   near-white in the dark theme. The six face tints are generated into
   P2PGames::Game::Cascade::Tiles (TILES_VERSION 8) and copied here by
   plan_cascade/08; t/cascade-08-page.t compares the two lists so they cannot
   drift. The site's three-way override: the dark set by default under a dark
   scheme, the light set under a light one, and the explicit choice last. */
.csc { --csc-k0: #dbe2f2; --csc-k1: #d2eff1; --csc-k2: #d9eede; --csc-k3: #f7dcd9;
       --csc-k4: #e8ddf3; --csc-k5: #fae3cf;
       --csc-line: #c4bdae; --csc-marked: #8a6d3b; }
:root:not([data-theme="light"]) .csc {
       --csc-k0: #1b2338; --csc-k1: #10333a; --csc-k2: #17301d; --csc-k3: #351916;
       --csc-k4: #281a34; --csc-k5: #38230f; --csc-line: #4a4538; --csc-marked: #d8b26a; }
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .csc {
       --csc-k0: #dbe2f2; --csc-k1: #d2eff1; --csc-k2: #d9eede; --csc-k3: #f7dcd9;
       --csc-k4: #e8ddf3; --csc-k5: #fae3cf; --csc-line: #c4bdae; --csc-marked: #8a6d3b; } }
:root[data-theme="dark"] .csc {
       --csc-k0: #1b2338; --csc-k1: #10333a; --csc-k2: #17301d; --csc-k3: #351916;
       --csc-k4: #281a34; --csc-k5: #38230f; --csc-line: #4a4538; --csc-marked: #d8b26a; }

/* Nothing here moves for a reader who asked for less motion: cascade.js skips
   the whole animation to the settled board, and these are the belt to that
   brace. */
@media (prefers-reduced-motion: reduce) {
  .csc .csc-tile.csc-going, .csc .csc-tile.csc-fired, .csc .csc-pop { animation: none !important; }
  .csc .csc-tile { transition: none !important; }
}

/* ---- floppy (plan_floppy/06) -----------------------------------------------
 *
 * Two canvases and a table. NO COLOUR IS NAMED HERE: the bird inks, the sky
 * and the pipes are painted by the renderer, which reads the theme itself,
 * so this block is layout, the tap behaviour and the reduced-motion rule,
 * and every colour it does set comes from a token.
 *
 * THE PREFIX IS THE WHOLE WORD. Seven sessions share this file and a two or
 * three letter prefix is how two games collide; `.floppy-` was claimed by
 * message on 2 Oct 2026 and the Cascade session confirmed site.css was free.
 */
.floppy { display: flex; flex-direction: column; gap: .5rem; align-items: center; }

/* THE TAP TARGET IS THE PLAY AREA. `touch-action: none` or the page pans
   under a thumb that is trying to fly, and a flap is then a scroll
   (reference_pointercancel_must_cancel is the other half: the renderer's
   controls release on pointercancel, because a phone raises one on a scroll,
   a call or a system gesture). */
.floppy-wrap {
  display: flex; flex-direction: column; gap: .35rem;
  touch-action: none; -webkit-user-select: none; user-select: none;
  cursor: pointer; border-radius: 6px; outline-offset: 3px;
}
.floppy-wrap:focus-visible { outline: 2px solid var(--accent); }

/* side by side when the window is short and wide: phase 01 measured the mini
   view three pixels BELOW THE FOLD at 812x375 (its bottom was 378 of 375),
   and the client adds this class from fit() rather than guessing in a media
   query, because what matters is the height left under the status line. */
.floppy-wrap.floppy-side { flex-direction: row; align-items: flex-start; }

.floppy-canvas { display: block; border-radius: 6px; background: var(--surface-2); }
.floppy-view-main { box-shadow: 0 1px 3px var(--shadow); }
.floppy-view-mini { box-shadow: 0 1px 2px var(--shadow); opacity: 1; }

.floppy-count { margin: 0; }
.floppy-finish { margin: 0; font-weight: 600; }
.floppy-hint { margin-left: .5rem; }

.floppy-seats { border-collapse: collapse; width: 100%; max-width: 30rem; }
.floppy-seats th, .floppy-seats td { padding: .25rem .5rem; text-align: left; border-bottom: 1px solid var(--line); }
.floppy-seats td + td, .floppy-seats th + th { text-align: right; }
.floppy-seat.you { background: var(--surface); }
.floppy-name { font-weight: 600; }
.floppy-noscript { margin: .5rem 0 0; color: var(--muted); max-width: 34rem; }

/* REDUCED MOTION. The flight itself cannot be removed: it IS the game, and a
   bird that does not move is not a gentler version of this, it is nothing.
   What is removed is every motion that is not the flight, which here is the
   idle parallax drift behind an empty board and the shadow transitions. 01's
   mark 12 has the positive control that makes this testable. */
@media (prefers-reduced-motion: reduce) {
  .floppy-canvas { transition: none !important; }
  .floppy-view-main, .floppy-view-mini { box-shadow: none; }
}

/* ---- Carrom (plan_carrom/06). The server draws the board as SVG for the reading
   with scripting off; carrom.js replaces it with a stack of three canvases sized
   by the client: the board paints once, the men clear per frame, the top layer
   carries the striker, the band and the guide. The board is SQUARE and is never
   turned for portrait; p2 sees it rotated through 180 degrees by the client's
   affine, never by CSS. Every control is 44 px tall. ---- */
.carrom { margin: 0 0 .8rem; }
.carrom .crm-svg { display: block; width: min(100%, 560px); height: auto; margin: 0 auto .6rem; }
.carrom .crm-svg .frame { fill: var(--walnut); }
.carrom .crm-svg .pocket { fill: var(--hole); }
.carrom .crm-svg .mark { stroke: rgba(92, 48, 24, .6); stroke-width: 2; fill: none; }
.carrom .crm-svg .mark.circle { fill: rgba(183, 49, 42, .18); }
.carrom .crm-svg .mark.centre { fill: rgba(183, 49, 42, .22); }
.carrom .crm-stack { display: none; position: relative; width: min(100%, 560px); aspect-ratio: 1; margin: 0 auto; touch-action: none;
                     border-radius: 10px; overflow: hidden; background: var(--walnut); box-shadow: 0 10px 28px var(--shadow);
                     user-select: none; -webkit-user-select: none; }
.carrom .crm-stack:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.carrom .layer { position: absolute; inset: 0; display: block; }
.carrom .layer-top { cursor: grab; }
.carrom.scripted .crm-stack { display: block; }
.carrom.scripted .crm-svg, .carrom.scripted .crm-men { display: none; }
.carrom .crm-summary { margin: .4rem 0; text-align: center; }
.carrom .needs-script { display: none; }
.carrom.scripted .needs-script { display: inline-flex; }
.carrom.scripted .needs-script[hidden] { display: none; }
.carrom .crm-controls { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .6rem .9rem; margin-top: .4rem; }
.carrom .crm-controls .button { min-height: 44px; }
.carrom .guide { align-items: center; gap: 6px; font-size: .9rem; min-height: 44px; }
@media (max-height: 700px) and (orientation: landscape) {
    .carrom .crm-stack, .carrom .crm-svg { width: min(100%, 560px, calc(100vh - 7rem)); }
}
@media (prefers-reduced-motion: reduce) { .carrom .layer { transition: none !important; } }

/* ---- tilerace (plan_sliding_puzzle/07) ---- */

/* THE FACE AND THE INK ARE ONE PAIR AND BOTH FLIP WITH THE THEME. The first
 * version of this block took the face from a hardcoded cream and the ink from
 * `--ink`, which the site redefines in dark mode: light ink on a light tile,
 * and the numbers vanished. The owner saw it before any test did, because
 * nothing here renders a page in both themes and looks.
 *
 * So the tile's colours are its own variables, declared for both themes in the
 * three lines this stylesheet uses everywhere else: the no-preference default,
 * the explicit light override, and the explicit dark one. Never read `--ink`
 * on a surface this block paints itself. */
:root:not([data-theme="light"]) .tr {
    --tr-board: #2b2622; --tr-face: #4a4038; --tr-face-home: #5b4e43;
    --tr-ink: #f2ece0; --tr-edge: #322c27; --tr-lit: #a8792a; --tr-lit-edge: #7d5a1f;
    --tr-refused: #7a3b34; --tr-hole: rgba(0, 0, 0, .38);
}
@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .tr {
        --tr-board: #c9bca7; --tr-face: #e9dfcf; --tr-face-home: #f3ece0;
        --tr-ink: #1d1a16; --tr-edge: #b9ab93; --tr-lit: #ffd166; --tr-lit-edge: #c9a13b;
        --tr-refused: #f6b1aa; --tr-hole: rgba(0, 0, 0, .18);
    }
}
:root[data-theme="dark"] .tr {
    --tr-board: #2b2622; --tr-face: #4a4038; --tr-face-home: #5b4e43;
    --tr-ink: #f2ece0; --tr-edge: #322c27; --tr-lit: #a8792a; --tr-lit-edge: #7d5a1f;
    --tr-refused: #7a3b34; --tr-hole: rgba(0, 0, 0, .38);
}

/* THE BOARD DOES NOT TURN. The chrome around it does: under `ar` the page is
 * right to left and these sixteen squares are not, because a mirrored four by
 * four is a different puzzle under the same indices. The template pins
 * dir="ltr" and this block never uses a logical property that would undo it. */
.tr-board {
    position: relative;
    width: min(100%, 24rem);
    aspect-ratio: 1;
    margin: 0 auto;
    background: var(--tr-board, #c9bca7);
    border-radius: .5rem;
    touch-action: manipulation;
    /* so the tile's font can be sized against the BOARD and not the viewport:
     * the board is capped at 24rem, and a tile on a wide screen should not grow
     * with the window. */
    container-type: inline-size;
}

/* A TILE IS AN ELEMENT THAT MOVES. Its square is two custom properties the
 * client sets, and the transform is here, so a redraw that puts a tile
 * somewhere else is a real movement and the browser animates it. The client
 * says only how long. */
.tr-board .tr-tile,
.tr-board .tr-hole {
    position: absolute;
    width: 25%;
    height: 25%;
    margin: 0;
    padding: 2%;
    box-sizing: border-box;
    transform: translate(calc(var(--c, 0) * 100%), calc(var(--r, 0) * 100%));
}

.tr-board .tr-tile {
    display: flex;
    align-items: center;
    justify-content: center;
    font: inherit;
    font-weight: 700;
    font-size: clamp(1rem, 7cqw, 1.9rem);
    font-variant-numeric: tabular-nums;
    border: 0;
    background: none;
    color: var(--tr-ink, #1d1a16);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

/* The face is an inner box drawn by the padding above, so the gap between
 * tiles costs no extra element and the hit area stays the whole cell. */
.tr-board .tr-tile::before {
    content: '';
    position: absolute;
    inset: 4%;
    border-radius: .35rem;
    background: var(--tr-face, #e9dfcf);
    box-shadow: 0 2px 0 var(--tr-edge, #b9ab93);
    z-index: -1;
}

.tr-board .tr-tile.home::before { background: var(--tr-face-home, #efe6d6); }
.tr-board .tr-tile.lit::before   { background: var(--tr-lit, #ffd166); box-shadow: 0 2px 0 var(--tr-lit-edge, #c9a13b); }
.tr-board .tr-tile.refused::before { background: var(--tr-refused, #f6b1aa); }
.tr-board .tr-tile[disabled] { cursor: default; opacity: .75; }

.tr-board .tr-hole::before {
    content: '';
    position: absolute;
    inset: 4%;
    border-radius: .35rem;
    box-shadow: inset 0 2px 6px var(--tr-hole, rgba(0, 0, 0, .18));
}

/* THE SLIDE. The duration is the client's, set per run, so a run of three is
 * one motion and not three. */
.tr-board .tr-tile.moving { transition: transform 120ms cubic-bezier(.2, .7, .2, 1); }

/* REDUCED MOTION MEANS NONE, not a shorter one: the client schedules no timer
 * either, so this is the belt to that brace. */
@media (prefers-reduced-motion: reduce) {
    .tr-board .tr-tile, .tr-board .tr-hole { transition: none !important; }
}

.tr-racers { list-style: none; margin: .75rem 0 0; padding: 0; display: flex; gap: 1rem; flex-wrap: wrap; }
.tr-racer { display: flex; gap: .35rem; align-items: baseline; font-variant-numeric: tabular-nums; }
.tr-racer .tr-name { font-weight: 600; }
/* `--muted` AND `--accent`, WHICH ARE TOKENS. These two lines read
 * `var(--quiet, #5a5247)` and `var(--ok, #2d6a4f)`, and NEITHER token exists on
 * this site: the names are `--muted` and there is no `--ok` at all. Both had a
 * hardcoded fallback, so nothing broke and nothing warned - the declaration was
 * valid and the colour simply never followed the theme. In the dark theme that
 * left dark brown counters and a dark green name on a #26211c surface, which is
 * the SAME bug the owner reported in the tiles, surviving in the row beneath
 * them because the fallback hid it from the grep that would have found an
 * undefined property.
 *
 * Both of these flip: --muted is #6d655a light and #a79d8f dark, --accent is
 * #7a4b22 light and #d3aa55 dark. `--peg-green` was the obvious choice for
 * "done" and is not used, because it is a fixed seat colour that does not flip
 * and is 2.5:1 on the dark surface. */
.tr-racer .tr-taps, .tr-racer .tr-moves { color: var(--muted); }
.tr-racer[data-status="done"] .tr-name { color: var(--accent); }
.tr-racer[data-status="out"] .tr-name,
.tr-racer[data-status="dq"] .tr-name { opacity: .6; }

/* THE COUNTDOWN, over the board rather than beside it: a player looking at the
 * tiles must not have to look somewhere else to learn why they cannot move. */
.tr .tr-countdown {
    position: absolute;
    inset: 0;
    z-index: 2;
    margin: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--tr-board, #c9bca7) 70%, transparent);
    border-radius: .5rem;
    font-size: 4rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    color: var(--tr-ink, #1d1a16);
    text-shadow: 0 1px 0 var(--tr-face, #e9dfcf);
    pointer-events: none;
}

/* ---- Racing Demon (plan_racing_demon/07) --------------------------------
 *
 * APPENDED AT THE END, never woven in: the file is shared with every other
 * game and ten sessions edit it.
 *
 * EVERY TOKEN THIS BLOCK USES IS DEFINED HERE with a fallback. An undefined
 * CSS variable is a SILENT INHERIT and not a fallback
 * (reference_undefined_css_variable_is_a_silent_inherit), so a colour that
 * depends on a token another block happens to define is a colour that
 * disappears the day that block moves.
 *
 * minmax(0, 1fr) EVERYWHERE, never a bare 1fr: a 1fr track's automatic minimum
 * is min-content, which in the prototype made one unshrinkable row of chips
 * push the page 157 px sideways at a 360 px viewport. The same family as the
 * max-content trap this site already knows, with a different spelling. */
.rd {
  --rd-line: var(--line, #c9c0b4);
  --rd-red: #b32020;
  --rd-felt: #2e5d4b;
  --rd-can: #f0b429;
  --rd-tap: 44px;
  --rd-cardw: 58px;
}
.rd-h { font-size: 12px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--quiet, #6a635a); margin: 14px 0 4px; font-weight: 700; }
.rd-h .quiet { text-transform: none; letter-spacing: 0; font-weight: 400; }

.rd-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
.rd-phase { font-weight: 600; }
.rd-note { padding: 4px 8px; border-radius: 6px; background: #fff6d8;
  border: 1px solid #e8d79a; font-weight: 600; }

.rd-centre { display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr));
  gap: 6px; background: var(--rd-felt); padding: 8px; border-radius: 8px; }
.rd-slot { border: 1px dashed rgba(255,255,255,.35); border-radius: 6px;
  background: rgba(255,255,255,.10); padding: 4px; text-align: center;
  min-height: var(--rd-tap); color: #fff; }
.rd-slot .rd-cap { display: block; font-size: 10px; color: #cfe3d8; margin-top: 3px; }
.rd-slot.closed { opacity: .45; border-style: solid; }
.rd-slot.can { border-color: var(--rd-can); border-style: solid; background: rgba(240,180,41,.22); }
/* THE CARD INSIDE A SLOT MUST NOT SWALLOW THE DROP. The slot is the button and
 * the card is decoration inside it; without this the pointer lands on the
 * card and `dropAt` finds no target, which is one of the four gestures 01
 * found dead by hand. */
.rd-slot .card { pointer-events: none; }

/* THE CARDS ARE THE SITE'S `.card` (site.css:483) AND NOT THIS GAME'S OWN.
 * There was a `.rd-card` here until 4 Oct 2026: a rounded box with the rank
 * and the suit as one string in the middle. It worked, and it was the only
 * card on the site that did not look like a card, which the owner put plainly
 * as "we need our proper card styles". `.card` is the shared component every
 * other card game here uses, with the corner index, the centre pip, the
 * mirrored second index, the 5/7 aspect ratio, the hover lift, the deal
 * animation and the patterned back, and taking it also deleted this game's
 * private `red`, its own `back` and its own highlight.
 *
 * WHAT IS LEFT HERE IS ONLY WHAT THIS GAME ADDS: a drag needs
 * `touch-action: none` or the browser scrolls the page instead of moving the
 * card, and no other card game on this site drags, so that rule belongs to
 * this block and not to the shared one. */
.rd .card { touch-action: none; user-select: none; }

/* The zones size off the shared card rather than off a number of their own,
 * so a change to `--w` moves the holders with it. */
.rd .rd-holder .card,
.rd .rd-pile .card { --w: var(--rd-cardw); }
.rd .rd-slot .card { --w: 52px; }

.rd-zones { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px; max-width: 320px; }
.rd-zone { text-align: center; }
.rd-lab { font-size: 11px; color: var(--quiet, #6a635a); min-height: 15px; }
.rd-holder { min-height: calc(var(--rd-cardw) * 7 / 5); display: flex;
  justify-content: center; align-items: flex-start; }

.rd-work { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px; max-width: 420px; }
/* A WORK PILE OVERLAPS, which is what a patience column IS: the cards fan down
 * so every rank stays readable and the lowest one, the only one that can be
 * played, sits proud at the bottom. The first version stacked them with a gap
 * and a fixed 132px, so a pile of one card was a card in a tall empty box and
 * a pile of six ran off the end of it. The negative margin is the fan and the
 * `:first-child` reset stops it pulling the top card up out of the pile. */
.rd-pile { position: relative; min-height: calc(var(--rd-cardw) * 7 / 5);
  border: 1px dashed var(--rd-line); border-radius: 6px; padding: 3px;
  display: flex; flex-direction: column; align-items: center; }
.rd-pile .card { margin-top: calc(var(--rd-cardw) * -7 / 5 + 22px); }
.rd-pile .card:first-child { margin-top: 0; }
.rd-pile.can { border: 2px solid var(--rd-can); background: rgba(240,180,41,.14); }

.rd-rivals { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 6px; }
/* A RIVAL PANEL IS NOT A PLAYING CARD, and treating it as one made its text
 * unreadable. It used `--rd-card`, which is the site's `--card`: the colour of
 * the face of a card, deliberately cream in BOTH themes because a playing card
 * is light whatever the page is. The panel set that as its background and set
 * no `color`, so the text kept inheriting `--ink`, which in dark mode is very
 * nearly white. Cream on cream. The owner read it off the screen on 4 Oct 2026
 * as "the text colour of '13 to clear' is not readable on the background".
 *
 * `--surface-2` and `--ink` are the site's own panel pair and they flip
 * TOGETHER with the theme, which is the property the card colour never had.
 * The rule is: take a background and its colour from the same pair, or one of
 * them will follow the theme without the other. */
.rd-rival { border: 1px solid var(--rd-line); border-radius: 6px; padding: 6px;
  background: var(--surface-2); color: var(--ink); font-size: 12px; }
.rd-rival b { color: var(--ink); }
.rd-left { color: var(--muted); }
.rd-prog { width: 100%; height: 6px; }
/* the rivals' exposed cards, as the site's small inline `.pip-card` rather
 * than as twenty full cards of furniture at four seats */
.rd-chips { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 4px; }
.rd-chips .pip-card { font-size: 11px; }

.rd-tally { border-collapse: collapse; margin: 8px 0; font-size: 12px; width: 100%; max-width: 420px; }
.rd-tally td, .rd-tally th { border: 1px solid var(--rd-line); padding: 3px 6px; text-align: right; }
.rd-tally th:first-child, .rd-tally td:first-child { text-align: left; }

@media (prefers-reduced-motion: reduce) {
  .rd .card { transition: none !important; animation: none !important; }
}
/* the always-present slot an ace opens a new pile in: a hook the template and
 * the client both name, so it has a rule of its own rather than borrowing
 * .rd-slot's and being invisible the day that changes */
.rd-slot.rd-new { border-style: dotted; }
/* THE SHARED .button MEASURES 41.2 px TALL and the tap floor is 44. That is a
 * site-wide number Landlord's phase 08 already wanted raised and it is still
 * open; raising it here would change every game's buttons from this game's
 * commit, so this rule raises its OWN two, which is the part this block is
 * entitled to decide, and leaves the shared rule to whoever takes that
 * decision. var(--rd-tap), not a second 44, and it inherits from .rd above.
 * Load-bearing and measured both ways on the rendered lobby page at a verified
 * 360 px viewport, 4 October 2026: #rd-ready is 41.2 px without this line and
 * 44 with it. */
.rd-bar .button { min-height: var(--rd-tap); }

/* ---- Solitaire (plan_solitaire/07) -----------------------------------------
 *
 * EVERY TOKEN USED HERE IS DEFINED HERE. An undefined custom property is a
 * SILENT INHERIT and an undefined token voids the whole declaration it
 * appears in, so a typo is a rule that quietly does nothing rather than an
 * error (reference_an_undefined_css_variable_is_a_silent_inherit,
 * reference_a_css_token_that_does_not_exist_voids_the_whole_declaration).
 *
 * minmax(0, 1fr) EVERYWHERE AND NEVER A BARE 1fr. A `1fr` track's automatic
 * minimum is min-content, and seven overlapping card piles have a wide one:
 * it scrolled Racing Demon's prototype sideways by 157 px at a 360 px
 * viewport. The page must scroll DOWN and never across.
 */
.sol {
  --sol-gap: 4px;
  --sol-radius: 4px;
  --sol-can: #f0b429;
  --sol-lift: #7b4bd8;
  --sol-felt: #2e5d4b;
  --sol-slot: rgba(255, 255, 255, 0.10);
  --sol-slot-line: rgba(255, 255, 255, 0.35);
  --sol-ghost: #cfe3d8;
  /* ITS OWN LINE AND TRACK TOKENS, not the page's. The house convention is
   * per-game tokens (Racing Demon defines --rd-line rather than reaching
   * for a shared one), and the first cut of this block used `var(--sol-track)`,
   * WHICH DOES NOT EXIST. An undefined token voids the whole declaration,
   * so the bar simply had no background and nothing said so. */
  --sol-line: #c9c0b4;
  --sol-track: #e3ded5;
  background: var(--sol-felt);
  padding: var(--sol-gap);
  border-radius: 8px;
}

.sol-top,
.sol-tableau {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--sol-gap);
}
.sol-top { margin-bottom: 8px; }

.sol-found,
.sol-stock,
.sol-waste,
.sol-pile {
  position: relative;
  min-height: 44px;
  border: 1px dashed var(--sol-slot-line);
  border-radius: var(--sol-radius);
  background: var(--sol-slot);
}
.sol-found,
.sol-stock { aspect-ratio: 5 / 7; }
.sol-waste { grid-column: span 2; }
.sol-pile  { min-height: 120px; }

/* A CARD IS AS WIDE AS ITS SLOT, AND THE GLOBAL RULE ALREADY KNOWS HOW TO DO
 * THAT. `.card` is `width: var(--w); font-size: calc(var(--w) * .21)` with
 * `--w: 66px`, so setting the TOKEN sizes the card and its glyph together,
 * which is the whole reason the token exists.
 *
 * The first cut set `left: 0; right: 0` instead and left `--w` alone. AN
 * ABSOLUTELY POSITIONED BOX WHOSE WIDTH IS NOT `auto` IGNORES `right`: every
 * pile and foundation card stayed 68 px inside a 39 px track, and the page
 * scrolled sideways by 18 px at 320, 12 at 360 and 10 at 375. Nothing warned,
 * because `right` on a sized box is not an error, it is simply dropped. Only
 * the waste looked right, and only because its own rule gave it a width.
 *
 * cqw AND NOT %, because the card must size against the SLOT it sits in and a
 * percentage on an absolutely positioned box already does that for width while
 * doing nothing useful for `font-size`, which resolves against the parent's
 * font. The @supports guard is the house pattern from `.dab-board`. */
.sol-found,
.sol-stock,
.sol-waste,
.sol-pile { container-type: inline-size; }

.sol .card {
  position: absolute;
  left: 0;
  right: auto;
  --w: 100%;
  font-size: 11px;
  aspect-ratio: 5 / 7;
  border-radius: var(--sol-radius);
}
.sol-waste .card { --w: calc((100% - 2 * var(--sol-gap)) / 2.2); }
@supports (width: 1cqw) {
  .sol .card       { --w: 100cqw; font-size: calc(var(--w) * .21); }
  .sol-waste .card { --w: calc((100cqw - 2 * var(--sol-gap)) / 2.2); }
}
.sol-found .card,
.sol-stock .card { top: 0; }
/* THE PIP FALLBACK (plan_cards/05) picks the layout or one pip from --w in px, and
   these cards are sized by their pile, so the pile says which side of 61px it
   is; the waste card is 1/2.2 of its pile. Higher specificity than the cards
   block's `.cards-pips-layout .card`, which is later in the file. */
@container (max-width: 60px)  { .cards-pips-layout .sol .card { --big: 0; } }
@container (min-width: 61px)  { .cards-pips-layout .sol .card { --big: 1; } }
@container (max-width: 142px) { .cards-pips-layout .sol .sol-waste .card { --big: 0; } }
@container (min-width: 143px) { .cards-pips-layout .sol .sol-waste .card { --big: 1; } }
.sol .card.lift   { outline: 3px solid var(--sol-lift); outline-offset: 1px; z-index: 50; }
.sol .card.flight { opacity: 0.45; }
.sol .sol-under   { opacity: 0.85; pointer-events: none; }
.sol .can         { border: 2px solid var(--sol-can); background: rgba(240, 180, 41, 0.22); }

.sol-ghost { position: absolute; left: 0; right: 0; top: 36%; text-align: center;
             font-size: 18px; color: var(--sol-ghost); }
.sol-ghost.red { color: #e39aa0; }
.sol-count { position: absolute; left: 0; right: 0; bottom: 2px; text-align: center;
             font-size: 11px; color: #fff; pointer-events: none; }

.sol-h { font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; margin: 12px 0 4px; }
.sol-h .hint { text-transform: none; letter-spacing: 0; font-weight: 400; }
.sol-racers { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 6px; }
.sol-racer  { border: 1px solid var(--sol-line); border-radius: 6px; padding: 5px 7px; font-size: 12px; }
.sol-racer.me  { border-color: var(--sol-lift); }
.sol-racer.out { opacity: 0.6; }
.sol-bar { display: block; height: 6px; background: var(--sol-track); border-radius: 3px;
           margin-top: 4px; overflow: hidden; }
.sol-bar i { display: block; height: 100%; background: var(--sol-lift); }
.sol-teach { margin-top: 10px; }

/* ---- Solitaire offsets, because THE CSP FORBIDS A STYLE ATTRIBUTE -----------
 *
 * This site's policy is `default-src 'self'` with no style-src, so every
 * `style="top:Npx"` the template used to write was dropped on the floor and
 * all nineteen cards of a pile stacked at top 0. A style ATTRIBUTE cannot be
 * rescued the way a <style> element can: a nonce does not apply to it, and
 * nor does a hash without 'unsafe-hashes'. A nonced <style> block is out too,
 * because root/templates/how_to_play.tmpl records that a nonce collides with
 * the site's file cache, where a page stored under one nonce is served under
 * another.
 *
 * So the offsets are a CLOSED SET and they live here. A card's position is
 * two numbers: how many face-down cards are beneath the fan (the pile's own
 * `sol-b`), and how far down the fan the card sits (`sol-u`). They compose
 * through a custom property, which is why this is 7 + 19 + 19 rules and not
 * one rule for every (down, up) pair.
 *
 * The steps match the constants in P2PGames::Game::Solitaire: DOWN_STEP 6,
 * UP_STEP 22, WASTE_STEP 16, CARD_H 58. t/solitaire-07-page.t asserts the
 * two agree, because a stylesheet that drifts from them is a board that
 * looks right in the test and wrong on the page. */

/* a face-down card: a thin strip each */
.sol-d0 { top: 0px; }
.sol-d1 { top: 6px; }
.sol-d2 { top: 12px; }
.sol-d3 { top: 18px; }
.sol-d4 { top: 24px; }
.sol-d5 { top: 30px; }
.sol-d6 { top: 36px; }

/* the pile says how deep its face-down stack is, and how long its fan is */
.sol-b0 { --sol-b: 0px; }
.sol-b1 { --sol-b: 6px; }
.sol-b2 { --sol-b: 12px; }
.sol-b3 { --sol-b: 18px; }
.sol-b4 { --sol-b: 24px; }
.sol-b5 { --sol-b: 30px; }
.sol-b6 { --sol-b: 36px; }

/* a face-up card's place in the fan; the pile's `sol-t` repeats the last one
   so the column can be as tall as its contents without an inline height */
.sol-u0 { --sol-u: 0; }
.sol-u1 { --sol-u: 1; }
.sol-u2 { --sol-u: 2; }
.sol-u3 { --sol-u: 3; }
.sol-u4 { --sol-u: 4; }
.sol-u5 { --sol-u: 5; }
.sol-u6 { --sol-u: 6; }
.sol-u7 { --sol-u: 7; }
.sol-u8 { --sol-u: 8; }
.sol-u9 { --sol-u: 9; }
.sol-u10 { --sol-u: 10; }
.sol-u11 { --sol-u: 11; }
.sol-u12 { --sol-u: 12; }
.sol-u13 { --sol-u: 13; }
.sol-u14 { --sol-u: 14; }
.sol-u15 { --sol-u: 15; }
.sol-u16 { --sol-u: 16; }
.sol-u17 { --sol-u: 17; }
.sol-u18 { --sol-u: 18; }

.sol-t0 { --sol-t: 0; }
.sol-t1 { --sol-t: 1; }
.sol-t2 { --sol-t: 2; }
.sol-t3 { --sol-t: 3; }
.sol-t4 { --sol-t: 4; }
.sol-t5 { --sol-t: 5; }
.sol-t6 { --sol-t: 6; }
.sol-t7 { --sol-t: 7; }
.sol-t8 { --sol-t: 8; }
.sol-t9 { --sol-t: 9; }
.sol-t10 { --sol-t: 10; }
.sol-t11 { --sol-t: 11; }
.sol-t12 { --sol-t: 12; }
.sol-t13 { --sol-t: 13; }
.sol-t14 { --sol-t: 14; }
.sol-t15 { --sol-t: 15; }
.sol-t16 { --sol-t: 16; }
.sol-t17 { --sol-t: 17; }
.sol-t18 { --sol-t: 18; }

.sol .card.sol-upc { top: calc(var(--sol-b, 0px) + var(--sol-u, 0) * 22px); }
/* THE COLUMN IS AS TALL AS ITS LAST CARD, AND THE CARD IS FLUID. The first
   cut added a flat 58px for the card, which is CARD_H, a fixed guess from a
   360px phone. A card is `aspect-ratio: 5/7` of a track that grows with the
   viewport, so on a desktop it is nearer 94px and the bottom of every pile
   overflowed its own column by the difference: the heading below the board
   rendered ON TOP of the cards.

   The pseudo-element is the fix and it needs no magic number and no
   container query. It is a block of the pile's own width with the card's own
   aspect ratio, so it is exactly as tall as a card at any width, pushed down
   to where the last card starts. The pile's height is then the last card's
   top plus a card, measured rather than estimated. */
.sol-pile { min-height: 44px; }
.sol-pile::after {
  content: "";
  display: block;
  width: 100%;
  aspect-ratio: 5 / 7;
  margin-top: calc(var(--sol-b, 0px) + var(--sol-t, 0) * 22px);
  pointer-events: none;
}

/* the waste fans sideways */
.sol-waste .card.sol-w0 { left: 0px; }
.sol-waste .card.sol-w1 { left: 16px; }
.sol-waste .card.sol-w2 { left: 32px; }
.sol-waste .card.sol-w3 { left: 48px; }

/* THE RACE BAR IN STEPS OF FIVE, and the client makes it exact. A width is a
   style attribute and the policy drops it; 101 classes for 101 widths is
   absurd. `bar.style.width` from the client is CSSOM and NOT subject to the
   policy, so the server paints the nearest 5% and the script refines it. */
.sol-bar0 { width: 0%; }
.sol-bar5 { width: 5%; }
.sol-bar10 { width: 10%; }
.sol-bar15 { width: 15%; }
.sol-bar20 { width: 20%; }
.sol-bar25 { width: 25%; }
.sol-bar30 { width: 30%; }
.sol-bar35 { width: 35%; }
.sol-bar40 { width: 40%; }
.sol-bar45 { width: 45%; }
.sol-bar50 { width: 50%; }
.sol-bar55 { width: 55%; }
.sol-bar60 { width: 60%; }
.sol-bar65 { width: 65%; }
.sol-bar70 { width: 70%; }
.sol-bar75 { width: 75%; }
.sol-bar80 { width: 80%; }
.sol-bar85 { width: 85%; }
.sol-bar90 { width: 90%; }
.sol-bar95 { width: 95%; }
.sol-bar100 { width: 100%; }


/* ---- cards: the settings (plan_cards/05) ---------------------------------------------
   Six axes a player chooses on the account page and wears as body classes (D3: the
   CSP allows no style attribute): theme-<colour> above, and here cards-back-<pattern>,
   cards-face-<colourway>, cards-pips-layout, cards-courts-figure|crest and
   cards-ace-plain|ornate. A default emits no class and this block changes nothing.

   The face's parts carry their suit as a class (s-S s-H s-D s-C, games/_card.tmpl
   and cards.js), which is how a four-colour deck tells a diamond from a heart when
   the element around them says only `red`. The SVG faces are the symbol sheet
   games/_card_faces.tmpl, filled with currentColor: a <use> clone cannot be reached by
   a class selector, so colour is the only switch, and it is set on the part.

   EVERY TOKEN THIS BLOCK USES IS DEFINED IN THIS FILE: --card --card-ink --card-red
   --card-edge --back --back-line --shadow --w on :root and .card above; --card-blue
   --card-green --big here. An undefined custom property is a silent inherit. */
.card { --big: 1; --card-blue: #2f4c8a; --card-green: #1f7a4d; }

/* the colourways: the face tokens, redefined on the card */
.cards-face-four .card .s-D { color: var(--card-blue); }
.cards-face-four .card .s-C { color: var(--card-green); }
.cards-face-parchment .card { --card: #f0e3c4; --card-ink: #3b2a17; --card-red: #9e2f26; --card-edge: rgba(60, 40, 10, .3); }
.cards-face-contrast .card  { --card: #ffffff; --card-ink: #000000; --card-red: #d40000; --card-blue: #0033cc; --card-green: #007a33; }
.cards-face-midnight .card  { --card: #2a2623; --card-ink: #f3eadb; --card-red: #ff8a7a; --card-blue: #8fb4ff; --card-green: #7fd4a0; --card-edge: rgba(255, 245, 225, .28); }
/* Midnight is the one face that is not ivory in both themes (site.css:1-4), so its
   edge is the one card token that varies: a dark card on the dark ground needs a
   brighter edge to be a card at all. minigolf's triple, so an explicit light choice
   still wins. */
:root:not([data-theme="light"]) .cards-face-midnight .card { --card-edge: rgba(255, 245, 225, .42); }
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .cards-face-midnight .card { --card-edge: rgba(255, 245, 225, .28); } }
:root[data-theme="dark"] .cards-face-midnight .card { --card-edge: rgba(255, 245, 225, .42); }

/* the backs: the pattern draws with --back and --back-line, the colour above sets --back */
.cards-back-plaid .card.back, .cards-back-plaid .pile { background: repeating-linear-gradient(0deg, transparent 0 6px, var(--back-line) 6px 8px), repeating-linear-gradient(90deg, transparent 0 6px, var(--back-line) 6px 8px), var(--back); }
.cards-back-dots .card.back,  .cards-back-dots .pile  { background: radial-gradient(circle, var(--back-line) 1.3px, transparent 1.7px) 0 0 / 7px 7px, var(--back); }
.cards-back-rings .card.back, .cards-back-rings .pile { background: repeating-radial-gradient(circle at 50% 50%, var(--back) 0 3px, var(--back-line) 3px 4px); }
.cards-back-rays .card.back,  .cards-back-rays .pile  { background: repeating-conic-gradient(from 0deg at 50% 50%, var(--back) 0 9deg, var(--back-line) 9deg 12deg); }
.cards-back-plain .card.back { background: var(--back); box-shadow: inset 0 0 0 3px var(--card), inset 0 0 0 4px var(--back-line), inset 0 0 0 7px var(--back), inset 0 0 0 8px var(--back-line), 0 1px 2px var(--shadow); }
.cards-back-plain .pile      { background: var(--back); box-shadow: inset 0 0 0 3px var(--card), inset 0 0 0 4px var(--back-line), inset 0 0 0 7px var(--back), inset 0 0 0 8px var(--back-line); }

/* the faces: a court, a crest or an ornate ace fills the card; the plain ace is a larger pip */
.card svg.face { position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none; }
.card .pip.big { font-size: 2.7em; }

/* the pip field: one svg a pip, placed by its position in a field of n */
.card .field { position: absolute; inset: 0; pointer-events: none; }
.card .field .p { position: absolute; display: block; transform: translate(-50%, -50%); }
.card .field .p.flip { transform: translate(-50%, -50%) rotate(180deg); }
/* GENERATED by bin/cards-css.pl from P2PGames::Cards::PIPS; do not edit by hand */
.card .field .p:nth-child(1):nth-last-child(2) { left: 50.000%; top: 20.000%; width: calc(var(--w) * 0.27); height: calc(var(--w) * 0.27); }
.card .field .p:nth-child(2):nth-last-child(1) { left: 50.000%; top: 80.000%; width: calc(var(--w) * 0.27); height: calc(var(--w) * 0.27); }
.card .field .p:nth-child(1):nth-last-child(3) { left: 50.000%; top: 20.000%; width: calc(var(--w) * 0.27); height: calc(var(--w) * 0.27); }
.card .field .p:nth-child(2):nth-last-child(2) { left: 50.000%; top: 50.000%; width: calc(var(--w) * 0.27); height: calc(var(--w) * 0.27); }
.card .field .p:nth-child(3):nth-last-child(1) { left: 50.000%; top: 80.000%; width: calc(var(--w) * 0.27); height: calc(var(--w) * 0.27); }
.card .field .p:nth-child(1):nth-last-child(4) { left: 33.000%; top: 20.000%; width: calc(var(--w) * 0.23); height: calc(var(--w) * 0.23); }
.card .field .p:nth-child(2):nth-last-child(3) { left: 67.000%; top: 20.000%; width: calc(var(--w) * 0.23); height: calc(var(--w) * 0.23); }
.card .field .p:nth-child(3):nth-last-child(2) { left: 33.000%; top: 80.000%; width: calc(var(--w) * 0.23); height: calc(var(--w) * 0.23); }
.card .field .p:nth-child(4):nth-last-child(1) { left: 67.000%; top: 80.000%; width: calc(var(--w) * 0.23); height: calc(var(--w) * 0.23); }
.card .field .p:nth-child(1):nth-last-child(5) { left: 33.000%; top: 20.000%; width: calc(var(--w) * 0.23); height: calc(var(--w) * 0.23); }
.card .field .p:nth-child(2):nth-last-child(4) { left: 67.000%; top: 20.000%; width: calc(var(--w) * 0.23); height: calc(var(--w) * 0.23); }
.card .field .p:nth-child(3):nth-last-child(3) { left: 50.000%; top: 50.000%; width: calc(var(--w) * 0.23); height: calc(var(--w) * 0.23); }
.card .field .p:nth-child(4):nth-last-child(2) { left: 33.000%; top: 80.000%; width: calc(var(--w) * 0.23); height: calc(var(--w) * 0.23); }
.card .field .p:nth-child(5):nth-last-child(1) { left: 67.000%; top: 80.000%; width: calc(var(--w) * 0.23); height: calc(var(--w) * 0.23); }
.card .field .p:nth-child(1):nth-last-child(6) { left: 33.000%; top: 20.000%; width: calc(var(--w) * 0.22); height: calc(var(--w) * 0.22); }
.card .field .p:nth-child(2):nth-last-child(5) { left: 67.000%; top: 20.000%; width: calc(var(--w) * 0.22); height: calc(var(--w) * 0.22); }
.card .field .p:nth-child(3):nth-last-child(4) { left: 33.000%; top: 50.000%; width: calc(var(--w) * 0.22); height: calc(var(--w) * 0.22); }
.card .field .p:nth-child(4):nth-last-child(3) { left: 67.000%; top: 50.000%; width: calc(var(--w) * 0.22); height: calc(var(--w) * 0.22); }
.card .field .p:nth-child(5):nth-last-child(2) { left: 33.000%; top: 80.000%; width: calc(var(--w) * 0.22); height: calc(var(--w) * 0.22); }
.card .field .p:nth-child(6):nth-last-child(1) { left: 67.000%; top: 80.000%; width: calc(var(--w) * 0.22); height: calc(var(--w) * 0.22); }
.card .field .p:nth-child(1):nth-last-child(7) { left: 33.000%; top: 20.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(2):nth-last-child(6) { left: 67.000%; top: 20.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(3):nth-last-child(5) { left: 50.000%; top: 35.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(4):nth-last-child(4) { left: 33.000%; top: 50.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(5):nth-last-child(3) { left: 67.000%; top: 50.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(6):nth-last-child(2) { left: 33.000%; top: 80.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(7):nth-last-child(1) { left: 67.000%; top: 80.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(1):nth-last-child(8) { left: 33.000%; top: 20.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(2):nth-last-child(7) { left: 67.000%; top: 20.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(3):nth-last-child(6) { left: 50.000%; top: 35.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(4):nth-last-child(5) { left: 33.000%; top: 50.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(5):nth-last-child(4) { left: 67.000%; top: 50.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(6):nth-last-child(3) { left: 50.000%; top: 65.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(7):nth-last-child(2) { left: 33.000%; top: 80.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(8):nth-last-child(1) { left: 67.000%; top: 80.000%; width: calc(var(--w) * 0.21); height: calc(var(--w) * 0.21); }
.card .field .p:nth-child(1):nth-last-child(9) { left: 33.000%; top: 20.000%; width: calc(var(--w) * 0.185); height: calc(var(--w) * 0.185); }
.card .field .p:nth-child(2):nth-last-child(8) { left: 67.000%; top: 20.000%; width: calc(var(--w) * 0.185); height: calc(var(--w) * 0.185); }
.card .field .p:nth-child(3):nth-last-child(7) { left: 33.000%; top: 40.000%; width: calc(var(--w) * 0.185); height: calc(var(--w) * 0.185); }
.card .field .p:nth-child(4):nth-last-child(6) { left: 67.000%; top: 40.000%; width: calc(var(--w) * 0.185); height: calc(var(--w) * 0.185); }
.card .field .p:nth-child(5):nth-last-child(5) { left: 50.000%; top: 50.000%; width: calc(var(--w) * 0.185); height: calc(var(--w) * 0.185); }
.card .field .p:nth-child(6):nth-last-child(4) { left: 33.000%; top: 60.000%; width: calc(var(--w) * 0.185); height: calc(var(--w) * 0.185); }
.card .field .p:nth-child(7):nth-last-child(3) { left: 67.000%; top: 60.000%; width: calc(var(--w) * 0.185); height: calc(var(--w) * 0.185); }
.card .field .p:nth-child(8):nth-last-child(2) { left: 33.000%; top: 80.000%; width: calc(var(--w) * 0.185); height: calc(var(--w) * 0.185); }
.card .field .p:nth-child(9):nth-last-child(1) { left: 67.000%; top: 80.000%; width: calc(var(--w) * 0.185); height: calc(var(--w) * 0.185); }
.card .field .p:nth-child(1):nth-last-child(10) { left: 33.000%; top: 20.000%; width: calc(var(--w) * 0.18); height: calc(var(--w) * 0.18); }
.card .field .p:nth-child(2):nth-last-child(9) { left: 67.000%; top: 20.000%; width: calc(var(--w) * 0.18); height: calc(var(--w) * 0.18); }
.card .field .p:nth-child(3):nth-last-child(8) { left: 50.000%; top: 30.000%; width: calc(var(--w) * 0.18); height: calc(var(--w) * 0.18); }
.card .field .p:nth-child(4):nth-last-child(7) { left: 33.000%; top: 40.000%; width: calc(var(--w) * 0.18); height: calc(var(--w) * 0.18); }
.card .field .p:nth-child(5):nth-last-child(6) { left: 67.000%; top: 40.000%; width: calc(var(--w) * 0.18); height: calc(var(--w) * 0.18); }
.card .field .p:nth-child(6):nth-last-child(5) { left: 33.000%; top: 60.000%; width: calc(var(--w) * 0.18); height: calc(var(--w) * 0.18); }
.card .field .p:nth-child(7):nth-last-child(4) { left: 67.000%; top: 60.000%; width: calc(var(--w) * 0.18); height: calc(var(--w) * 0.18); }
.card .field .p:nth-child(8):nth-last-child(3) { left: 50.000%; top: 70.000%; width: calc(var(--w) * 0.18); height: calc(var(--w) * 0.18); }
.card .field .p:nth-child(9):nth-last-child(2) { left: 33.000%; top: 80.000%; width: calc(var(--w) * 0.18); height: calc(var(--w) * 0.18); }
.card .field .p:nth-child(10):nth-last-child(1) { left: 67.000%; top: 80.000%; width: calc(var(--w) * 0.18); height: calc(var(--w) * 0.18); }
/* END GENERATED */

/* THE FALLBACK (plan_cards/01, measured): the field AND the centre pip are both in
   the markup, and --big, from --w alone, picks which shows: 61 px and above the
   layout, 59 and below one pip. No class, no media query, no second template branch. */
.cards-pips-layout .card { --big: clamp(0, calc((var(--w) - 59px) / 2px), 1); }
.card .field { opacity: var(--big); }
.card .field + .pip { opacity: calc(1 - var(--big)); }

/* the index makes room for art in the middle: .21w today, .16w beside a figure, a
   crest or an ornate ace, and beside a pip field that is actually showing */
.card:has(> .field) .idx { font-size: calc(var(--w) * (.21 - .05 * var(--big))); }
.card:has(> .face) .idx  { font-size: calc(var(--w) * .16); }

/* the account page's card settings: the preview is the game table's walnut, the
   axes a grid, and Save stands clear of the last select (the owner, 4 Oct 2026) */
.card-preview { display: flex; flex-wrap: wrap; gap: 6px; align-items: flex-start; padding: 10px; margin: .4rem 0 .8rem; border-radius: 10px; background: var(--walnut); }
.card-preview .card { cursor: default; }
.card-axes { display: grid; gap: .6rem; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); }
form.card-style .card-axis { display: flex; flex-direction: column; gap: .3rem; font-size: .85rem; font-weight: 600; color: var(--muted); }
form.card-style button { margin-top: 1rem; }

/* ---- carrom, second pass (the owner, 5 Oct 2026): the aim buttons and the hold-to-charge Shoot ----
   APPENDED, never folded into the block above: this file is edited by every session on the site.
   The four aim buttons are a group of 44 px targets; Shoot fills from the left while it is held,
   the width a custom property the client sets through the CSSOM (no style attribute: the CSP), and
   the percent is written into the button's own word. Hidden by the attribute when it is not the
   shooter's stroke, as the Ready buttons elsewhere are. */
.carrom .crm-aim { display: inline-flex; gap: 6px; }
.carrom .crm-aim .button { min-width: 56px; min-height: 44px; padding: 0 .5rem; font-variant-numeric: tabular-nums; }
.carrom .crm-aim[hidden], .carrom .crm-power[hidden] { display: none; }
.carrom .crm-shoot { position: relative; overflow: hidden; min-width: 7.5rem; font-variant-numeric: tabular-nums; }
.carrom .crm-shoot .crm-charge { position: absolute; left: 0; top: 0; bottom: 0; width: calc(var(--crm-charge, 0) * 100%);
                                 background: rgba(255, 255, 255, .28); pointer-events: none; }
.carrom .crm-shoot .crm-shoot-word { position: relative; }
.carrom .crm-shoot.charging { box-shadow: 0 0 0 3px var(--brass); }
.carrom .crm-power { font-variant-numeric: tabular-nums; min-width: 6.5ch; }
@media (prefers-reduced-motion: reduce) { .carrom .crm-shoot .crm-charge { transition: none !important; } }

/* ---- Beehive (plan_beehive/08) ----------------------------------------------
 *
 * THE COMB IS SPANS AND THE PICTURE IS A CANVAS OVER THEM. The spans are the
 * accessible hive and are never rebuilt, only re-classed; the canvas is
 * aria-hidden and carries everything that moves. So the spans need a real
 * layout (a screen reader and the keyboard walk them) and need to be invisible
 * under the canvas, which is `visibility: hidden` on the cells and NOT
 * `display: none`: a display-none element has no box, so the canvas could not
 * be sized from the comb's own geometry and a keyboard focus ring would have
 * nothing to sit on.
 *
 * EVERY CUSTOM PROPERTY USED HERE IS DEFINED HERE. An undefined `var()` is not
 * an error, it is an inherit, and it looks like a theme bug three screens away
 * (reference_undefined_css_variable_is_a_silent_inherit).
 *
 * NO STYLE ATTRIBUTE ANYWHERE. This site's CSP is `default-src 'self'` with no
 * style-src, and a style ATTRIBUTE cannot be rescued by a nonce or a hash the
 * way a <style> element can (reference_the_p2p_csp_forbids_a_style_attribute).
 * The row inset is a CLASS, `bh-inset`, and the cell size is one custom
 * property the client sets on the comb through `el.style.setProperty`, which is
 * script touching a property and not markup carrying an attribute.
 *
 * minmax(0, 1fr) AND NEVER A BARE 1fr. A 1fr track's automatic minimum is
 * min-content, and a comb eight cells wide has a wide one; that is what
 * scrolled Racing Demon's prototype sideways by 157 px at 360
 * (reference_a_positioned_box_with_a_width_ignores_right is the sibling trap).
 */
.bh {
  --bh-cell: 34px;          /* the client overrides this on .bh-comb */
  --bh-gap: 1px;
  --bh-wax: #f0c35a;
  --bh-wax-rim: #b57a1e;
  --bh-ground: #241a0c;
  --bh-ink: #2b1d0e;
  --bh-guide: rgba(255, 255, 255, 0.55);
  --bh-death: #c0392b;
  --bh-owed: #e9b94c;
  /* DEFINED HERE RATHER THAN BORROWED. The first draft wrote
   * `var(--focus, #4ea1ff)` for the focus ring, and `--focus` is defined
   * NOWHERE in this stylesheet: the fallback was the only thing painting it,
   * which is the undefined-variable trap caught by its own safety net rather
   * than by a reader (reference_undefined_css_variable_is_a_silent_inherit).
   * A block that defines what it uses cannot be broken by a theme it does not
   * know about. */
  --bh-focus: #4ea1ff;
  /* THE FIELD IS PORTRAIT AND THE BOX MUST SAY SO. 2040 by 3152 in the
   * engine's units, which is the launcher line and the whole flight area
   * above it, not the five opening rows. The client overrides this from the
   * engine's own constants in `resize`; the value here is the fallback for
   * the moment before the script runs, and for a reader who has none. */
  --bh-aspect: 2040 / 3152;
  display: block;
}
/* A PORTRAIT COMB IS BOUNDED BY HEIGHT, NEVER BY WIDTH. At 3fr of a desktop
 * row the main column is around 1150px, and 1150px of a 0.647 field is 1777px
 * tall: a comb taller than three screens. So the height is what is capped and
 * the width follows from the ratio, which is also why the column below is 2fr
 * and not 3fr. */
.bh-game {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: 0.75rem;
  align-items: start;
}
.bh-main { min-width: 0; }
.bh-side { min-width: 0; }

/* the comb and the canvas share one box, so the canvas can be sized from the
 * comb's own measured geometry and nothing has to agree about a number */
.bh-combwrap {
  position: relative;
  aspect-ratio: var(--bh-aspect);
  /* HEIGHT BINDS FIRST, then the ratio gives the width, then `max-width`
   * clamps it on a narrow screen and the ratio gives the height back. Without
   * the cap a desktop column paints a comb three screens tall; without
   * `max-width` a phone scrolls sideways (minmax(0, 1fr) above is the sibling
   * half of that). `margin-inline: auto` because the box is now narrower than
   * its column and an off-centre hive looks like a bug. */
  height: min(72vh, 680px);
  width: auto;
  max-width: 100%;
  margin-inline: auto;
}
.bh-comb {
  display: flex;
  flex-direction: column;
  gap: var(--bh-gap);
  background: var(--bh-ground);
  border-radius: 6px;
  padding: calc(var(--bh-cell) / 8);
  overflow: hidden;
}
/* THE SPANS FILL THE SAME BOX AS THE CANVAS. They are the readable hive under
 * the picture, so they share its geometry rather than sizing themselves from
 * their own row count. Scoped to the wrapped comb: the rival's is in the
 * sidebar with no wrapper and sizes itself from its rows. */
.bh-combwrap .bh-comb { height: 100%; }
.bh-comb:focus-visible { outline: 3px solid var(--bh-focus); outline-offset: 2px; }
.bh-row { display: flex; gap: var(--bh-gap); }
/* THE INSET IS A CLASS. Half a cell, which is what makes it a hex packing. */
.bh-inset { margin-inline-start: calc(var(--bh-cell) / 2); }
.bh-cell {
  width: var(--bh-cell);
  height: var(--bh-cell);
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--bh-wax);
  box-shadow: inset 0 0 0 1px var(--bh-wax-rim);
}
.bh-empty { background: transparent; box-shadow: none; }

/* THE SIX KINDS, AS SIX OUTLINES AND NOT SIX COLOURS. The template and the
 * client both write `bh-k0` to `bh-k5` on a cell, on the bubble in hand and on
 * the one after it, and NOTHING DEFINED THEM: every occupied cell painted as
 * the same wax disc, so the rival's comb carried no kind at all and the two
 * launcher bubbles were blank discs on the page.
 *
 * They are told apart by SHAPE, which is 00's D9 and the same rule the canvas
 * obeys: a kind is a silhouette and never a hue, so the game is playable with
 * no colour vision and these stay legible at the 12px the sidebar uses. The
 * canvas covers these the moment it is up; they are what a reader with no
 * script gets, what the sidebar shows, and what the launcher slots show. The
 * accessible name is on the span and says the kind's NAME, never a colour. */
.bh-k0 { border-radius: 50%; }                                  /* worker, a disc */
.bh-k1 { border-radius: 28%; }                                  /* drone, round-shouldered */
.bh-k2 { border-radius: 50% / 32%; }                            /* queen, long */
.bh-k3 { border-radius: 62% 20% 62% 20%; }                      /* larva, curled */
.bh-k4 { border-radius: 50%; clip-path: polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%); }  /* pollen, faceted */
.bh-k5 { border-radius: 50% 50% 50% 8%; }                       /* nectar, a drop */

/* UNDER THE CANVAS THE CELLS ARE HIDDEN AND STILL THERE. `visibility`, not
 * `display`: the boxes have to keep their geometry. The client adds
 * `bh-painted` to the comb once the canvas is up, so a page whose script never
 * ran shows the spans and is readable. */
.bh-painted .bh-cell { visibility: hidden; }
.bh-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  touch-action: none;
}
/* THE CLASS IS ON THE WRAPPER AS WELL AS THE COMB, and that is what makes
 * this rule reach anything: the canvas is the comb's SIBLING, so with the
 * class only on the comb this selector never matched and the canvas sat at
 * `pointer-events: none` for the life of the page. The board looked finished
 * and could only be played from the keyboard and the buttons. */
.bh-painted .bh-canvas { pointer-events: auto; cursor: crosshair; }

/* THE COLUMN IS CENTRED ON THE COMB. The board is portrait and narrower than
 * the track it sits in, so the launcher row and the controls line up under its
 * middle rather than against the far left of a wide column. `.controls` is the
 * shared class and is only reached HERE, under `.bh`, so no other game moves. */
.bh-launcher {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  justify-content: center;
  margin-top: 0.5rem;
  flex-wrap: wrap;
}
.bh .controls { justify-content: center; }
.bh-slot { display: inline-flex; align-items: center; gap: 0.35rem; }
.bh-label { font-size: 0.8rem; opacity: 0.75; }
.bh-bub {
  width: var(--bh-cell);
  height: var(--bh-cell);
  border-radius: 50%;
  background: var(--bh-wax);
  box-shadow: inset 0 0 0 1px var(--bh-wax-rim);
  display: inline-block;
  position: relative;
}
/* THE SWATCH'S PICTURE, blitted from the same atlas as the board so a player
 * can see what they are about to fire. The wax disc behind it stays as the
 * no-script and not-yet-loaded fallback, which is why this sits on top rather
 * than replacing it. */
.bh-bub-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
.bh-owed b { color: var(--bh-owed); font-variant-numeric: tabular-nums; }
/* BEING BURIED IS THE THING A PLAYER MUST NOTICE, so a change in what is owed
 * flashes once. A change nobody sees is a change they will think was a bug. */
.bh-owed-change { animation: bh-flash 480ms ease-out 1; }
@keyframes bh-flash {
  0%   { background: var(--bh-death); }
  100% { background: transparent; }
}

.bh-sidehead { font-size: 0.9rem; margin: 0 0 0.35rem; }
.bh-side .bh-comb { --bh-cell: 12px; }
/* THE RIVAL'S BOX. Same field ratio as this seat's, bounded by the column it
 * sits in rather than by the viewport: it is a thing to glance at, not to play
 * on. Its canvas is the `.bh-canvas` rule above, which fills the wrapper. */
.bh-sidewrap {
  height: auto;
  width: 100%;
  max-width: 220px;
  margin-inline: 0;
}
.bh-count { font-variant-numeric: tabular-nums; margin-top: 0.4rem; }
.bh-credit { font-size: 0.75rem; margin-top: 0.6rem; }

.bh-keys { display: inline-flex; gap: 0.4rem; }
.bh-key {
  min-width: 3rem;
  min-height: 2.75rem;     /* a thumb, not a mouse */
  font-size: 1.1rem;
  border-radius: 6px;
  /* THE AIM KEYS ARE HELD DOWN, so the browser must not wait to see whether a
   * tap was a double tap and must not pan the page under the thumb. Without
   * this a hold reads as a scroll on a phone and the repeat never starts. */
  touch-action: manipulation;
  user-select: none;
}
.bh-fire { min-width: 4.5rem; }
.bh-noscript { margin-top: 0.5rem; }

/* THE PHONE GATE: at 360 px the comb, the launcher and the rival all fit with
 * no horizontal scroll, which means the rival's comb goes under rather than
 * beside. 01 measured the comb at eight and a half cells wide, so the cell is
 * whatever a third of the viewport divided by nine comes to and the client
 * sets it; this only has to stop the grid from insisting on two columns. */
@media (max-width: 640px) {
  .bh-game { grid-template-columns: minmax(0, 1fr); }
  .bh-side { display: flex; gap: 0.75rem; align-items: flex-start; }
  .bh-side .bh-comb { --bh-cell: 9px; }
}

/* REDUCED MOTION DROPS THE FLIGHT, AND THE GAME STAYS PLAYABLE. The client
 * reads the same query and places the bubble instead of animating it; this is
 * the half that stops the owed counter flashing. A branch that exists and is
 * never taken is not a branch, so t/beehive-08-page.t drives both. */
@media (prefers-reduced-motion: reduce) {
  .bh-owed-change { animation: none; }
}

/* DARK AND LIGHT ARE BOTH PAINTED. The wax reads on either; the ground and the
 * ink are what change. */
@media (prefers-color-scheme: light) {
  .bh { --bh-ground: #e8dcc4; --bh-ink: #3a2a14; --bh-guide: rgba(0, 0, 0, 0.45); }
}

/* ---- merrills ---- */
/* Nine Men's Morris (plan_game_merrills/11). A walnut board, brass lines, and
   a man where lines meet. Every colour here is a MATERIAL token or a literal:
   --walnut, --hole and --brass do not flip with the theme, and neither do the
   two men, so nothing on the board changes meaning in dark mode. A fixed
   ground needs a foreground that is equally fixed.

   The buttons are placed by two classes each on a seven by seven grid, never
   by a style attribute, which the CSP forbids. `minmax(0, 1fr)` and not a bare
   `1fr`, whose min-content floor would let the page scroll sideways.

   THE MARKS ARE SHAPES. The last move is a solid ring, the point it left a
   dotted one, a man taken a dashed one, a point that may be pressed a small
   brass ring, the man picked up is lifted with a double ring, and a man that
   may be taken a dashed ring in another colour. Colour is on top of each and
   never alone, so they survive greyscale. */
.merrills {
	--mm-white: #f3ead8;
	--mm-white-edge: #cfc2a6;
	--mm-black: #1f1712;
	--mm-black-edge: #0d0907;
	--mm-ring: #fbf7ee;
	--mm-take: #ff9a7a;
	--mm-go: #9fe0a0;
}
.merrills-board {
	position: relative; display: grid;
	grid-template-columns: repeat(7, minmax(0, 1fr));
	grid-template-rows: repeat(7, minmax(0, 1fr));
	width: min(100%, 30rem); aspect-ratio: 1; margin: 0 auto; padding: .3rem;
	box-sizing: border-box;
	background: var(--walnut); border-radius: 14px;
	box-shadow: inset 0 2px 8px rgba(0, 0, 0, .35), 0 3px 10px var(--shadow);
}
.merrills-lines { grid-area: 1 / 1 / 8 / 8; width: 100%; height: 100%; pointer-events: none; }
.merrills-lines path { fill: none; stroke: var(--brass); stroke-width: .07; stroke-linecap: round; stroke-linejoin: round; }
.merrills .point {
	position: relative; display: grid; place-items: center;
	min-width: 0; min-height: 0; margin: 0; padding: 0;
	border: 0; border-radius: 50%; background: none; color: inherit;
	appearance: none; cursor: default;
}
.merrills .point:not(:disabled) { cursor: pointer; }
/* A POINT THAT CANNOT BE PRESSED IS STILL A MAN ON THE BOARD. The site dims
   every disabled button, which is right for a control and wrong here: most of
   the board is disabled most of the time, and a man you may not move is not
   half a man. Found by looking at the page; no test saw it. */
.merrills .point:disabled { opacity: 1; }
.merrills .merrills-r0 { grid-row: 1; } .merrills .merrills-r1 { grid-row: 2; }
.merrills .merrills-r2 { grid-row: 3; } .merrills .merrills-r3 { grid-row: 4; }
.merrills .merrills-r4 { grid-row: 5; } .merrills .merrills-r5 { grid-row: 6; }
.merrills .merrills-r6 { grid-row: 7; }
.merrills .merrills-c0 { grid-column: 1; } .merrills .merrills-c1 { grid-column: 2; }
.merrills .merrills-c2 { grid-column: 3; } .merrills .merrills-c3 { grid-column: 4; }
.merrills .merrills-c4 { grid-column: 5; } .merrills .merrills-c5 { grid-column: 6; }
.merrills .merrills-c6 { grid-column: 7; }
.merrills .point .man {
	display: block; width: 24%; aspect-ratio: 1; border-radius: 50%;
	background: var(--hole); box-shadow: 0 0 0 2px var(--brass);
	text-decoration: none; transition: transform .12s ease;
}
.merrills .point.white .man, .merrills .point.black .man { width: 76%; }
.merrills .point.white .man {
	background: radial-gradient(circle at 35% 30%, #fffaf0, var(--mm-white) 55%, var(--mm-white-edge));
	box-shadow: 0 2px 4px rgba(0, 0, 0, .5);
}
.merrills .point.black .man {
	background: radial-gradient(circle at 35% 30%, #5b4b3f, var(--mm-black) 55%, var(--mm-black-edge));
	/* An ivory rim, because near-black on walnut is two dark things. */
	box-shadow: 0 0 0 2px rgba(243, 234, 216, .8), 0 2px 4px rgba(0, 0, 0, .5);
}
.merrills .point.in-mill .man { outline: 2px solid var(--brass); outline-offset: 2px; }
.merrills .point::after {
	content: ""; position: absolute; inset: 6%; border-radius: 50%;
	border: 0 solid transparent; pointer-events: none;
}
.merrills .point.last-move::after { border: 2px solid var(--mm-ring); }
.merrills .point.last-left::after { border: 2px dotted var(--mm-ring); }
.merrills .point.last-taken::after { border: 2px dashed var(--mm-take); }
.merrills .point.option.empty .man { width: 34%; box-shadow: 0 0 0 3px var(--brass), 0 0 0 5px var(--hole); }
.merrills .point.option:not(.empty)::after { border: 2px solid var(--mm-go); }
.merrills .point.takeable::after { inset: 2%; border: 3px dashed var(--mm-take); }
.merrills .point.chosen .man { transform: translateY(-9%) scale(1.06); outline: 4px double var(--mm-ring); outline-offset: 1px; }
.merrills .point.landed .man { outline: 4px double var(--mm-go); outline-offset: 1px; }
.merrills .point.hinted::after { inset: 0; border: 3px dashed var(--brass); }
.merrills .point:focus-visible { outline: 3px solid var(--mm-ring); outline-offset: -3px; }
.merrills-step { min-height: 1.5em; margin: .6rem 0 0; text-align: center; font-weight: 600; }
.merrills-hands { margin: .25rem 0 0; text-align: center; }
@media (prefers-reduced-motion: reduce) { .merrills .point .man { transition: none; } }

/* ---- brandubh ---- */
/* Brandubh (plan_game_brandubh/11). A walnut frame, a board of two woods, and
   three pieces that are three SHAPES: the attacker a dark disc with a pale
   rim, the defender a pale disc with a ring inside it, and the king a pale
   disc with a ring and a cross. The king is not a paler defender, and the
   three survive greyscale.

   Every colour is a MATERIAL token (--walnut, --hole, --brass) or a literal
   declared on .bdb: none flips with the theme, so nothing on the board changes
   meaning in dark mode.

   THE SPECIAL SQUARES ARE A PATTERN, NOT ONLY A TINT. The throne is hatched
   one way and the corners crossed both ways, whether or not a piece stands on
   them. The throne keeps its hatching when the king has left: it is still a
   square nobody else may stop on.

   THE MARKS ARE SHAPES TOO, each a different frame on the square's ::after:
   the last move a solid frame (dotted on the square it left), a piece just
   captured a dashed one, a square a piece can land on a brass dot, a piece
   that move would take a thick dashed frame, the piece picked up lifted with
   a double ring. Colour is on top of each and never alone.

   `minmax(0, 1fr)` and not a bare `1fr`, whose min-content floor would let
   the page scroll sideways at a phone's width. The coordinates sit INSIDE the
   edge squares, so they cost the board no width. No style attribute anywhere:
   the CSP forbids one. */
.bdb {
	--bdb-light: #d9b98c;
	--bdb-dark: #b58f5f;
	--bdb-ink: #2a1a0e;
	--bdb-pale: #f6eedc;
	--bdb-pale-edge: #c9b994;
	--bdb-coal: #1f1712;
	--bdb-coal-edge: #0b0806;
	--bdb-ring: #fffdf5;
	--bdb-take: #c2261c;
	--bdb-go: #1f7a3a;
}
.bdb-board {
	display: grid;
	grid-template-columns: repeat(7, minmax(0, 1fr));
	grid-template-rows: repeat(7, minmax(0, 1fr));
	width: min(100%, 30rem); aspect-ratio: 1; margin: 0 auto; padding: .3rem;
	box-sizing: border-box; gap: 0;
	background: var(--walnut); border-radius: 10px;
	box-shadow: inset 0 2px 8px rgba(0, 0, 0, .35), 0 3px 10px var(--shadow);
	touch-action: manipulation;
}
.bdb .bdb-cell {
	position: relative; display: grid; place-items: center;
	min-width: 0; min-height: 0; margin: 0; padding: 0;
	border: 0; border-radius: 0; color: var(--bdb-ink); font: inherit;
	appearance: none; cursor: default; background-color: var(--bdb-dark);
}
.bdb .bdb-cell.bdb-light { background-color: var(--bdb-light); }
.bdb .bdb-cell:not(:disabled) { cursor: pointer; }
/* A SQUARE THAT CANNOT BE PRESSED IS STILL A SQUARE WITH A PIECE ON IT. The
   site dims every disabled button, which is right for a control and wrong
   for most of a board most of the time. */
.bdb .bdb-cell:disabled { opacity: 1; }
.bdb .bdb-cell.bdb-throne {
	background-image: repeating-linear-gradient(45deg, rgba(42, 26, 14, .5) 0 2px, transparent 2px 7px);
	box-shadow: inset 0 0 0 2px rgba(42, 26, 14, .7);
}
.bdb .bdb-cell.bdb-corner {
	background-image:
		repeating-linear-gradient(45deg, rgba(42, 26, 14, .5) 0 2px, transparent 2px 7px),
		repeating-linear-gradient(-45deg, rgba(42, 26, 14, .5) 0 2px, transparent 2px 7px);
	box-shadow: inset 0 0 0 2px var(--brass);
}
.bdb .bdb-coord {
	position: absolute; font-style: normal; font-size: .62rem; line-height: 1;
	color: var(--bdb-ink); opacity: .75; pointer-events: none;
}
.bdb .bdb-coord-rank { top: 2px; left: 3px; }
.bdb .bdb-coord-file { bottom: 2px; right: 3px; }
.bdb .bdb-piece {
	position: relative; display: none; width: 72%; aspect-ratio: 1; border-radius: 50%;
	text-decoration: none; transition: transform .12s ease;
}
.bdb .bdb-attacker .bdb-piece, .bdb .bdb-defender .bdb-piece, .bdb .bdb-king .bdb-piece { display: block; }
.bdb .bdb-attacker .bdb-piece, .bdb-drag.bdb-attacker {
	background: radial-gradient(circle at 35% 30%, #5b4b3f, var(--bdb-coal) 55%, var(--bdb-coal-edge));
	box-shadow: 0 0 0 2px rgba(246, 238, 220, .85), 0 2px 4px rgba(0, 0, 0, .5);
}
.bdb .bdb-defender .bdb-piece, .bdb .bdb-king .bdb-piece, .bdb-drag.bdb-defender, .bdb-drag.bdb-king {
	background: radial-gradient(circle at 35% 30%, #ffffff, var(--bdb-pale) 55%, var(--bdb-pale-edge));
	box-shadow: inset 0 0 0 3px var(--bdb-pale), inset 0 0 0 5px var(--bdb-coal), 0 2px 4px rgba(0, 0, 0, .5);
}
.bdb .bdb-king .bdb-piece { width: 82%; }
/* The king's cross: two bars, drawn on the piece and not typed as a letter. */
.bdb .bdb-king .bdb-piece::before, .bdb .bdb-king .bdb-piece::after,
.bdb-drag.bdb-king::before, .bdb-drag.bdb-king::after {
	content: ""; position: absolute; left: 50%; top: 50%; background: var(--bdb-coal); border-radius: 1px;
}
.bdb .bdb-king .bdb-piece::before, .bdb-drag.bdb-king::before { width: 46%; height: 12%; transform: translate(-50%, -50%); }
.bdb .bdb-king .bdb-piece::after, .bdb-drag.bdb-king::after { width: 12%; height: 46%; transform: translate(-50%, -50%); }
.bdb .bdb-cell::after {
	content: ""; position: absolute; inset: 3px; border: 0 solid transparent; pointer-events: none;
}
.bdb .bdb-cell.bdb-last::after { border: 2px solid var(--bdb-ring); }
.bdb .bdb-cell.bdb-last.bdb-left::after { border: 2px dotted var(--bdb-ring); }
.bdb .bdb-cell.bdb-taken::after { border: 2px dashed var(--bdb-take); }
.bdb .bdb-cell.bdb-option:not(:disabled) .bdb-piece { outline: 2px solid var(--bdb-go); outline-offset: 1px; }
.bdb .bdb-cell.bdb-can-land::after {
	inset: 34%; border: 0; border-radius: 50%; background: var(--brass);
	box-shadow: 0 0 0 2px var(--hole);
}
.bdb .bdb-cell.bdb-would-take::after { inset: 1px; border: 4px dashed var(--bdb-take); border-radius: 0; background: none; box-shadow: none; }
.bdb .bdb-cell.bdb-selected .bdb-piece { transform: translateY(-8%) scale(1.06); outline: 4px double var(--bdb-ring); outline-offset: 1px; }
.bdb .bdb-cell.bdb-dragging .bdb-piece { opacity: .25; }
.bdb .bdb-cell.bdb-hinted::after { inset: 0; border: 3px dashed var(--brass); border-radius: 0; background: none; box-shadow: none; }
.bdb .bdb-cell:focus-visible { outline: 3px solid var(--bdb-ring); outline-offset: -3px; z-index: 1; }
.bdb-drag {
	position: fixed; z-index: 50; border-radius: 50%; pointer-events: none;
	transform: scale(1.12);
}
.bdb-repeat { min-height: 1.5em; margin: .6rem 0 0; text-align: center; font-weight: 600; }
.bdb-left { margin: .25rem 0 0; text-align: center; }
.bdb-draw { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; justify-content: center; min-height: 1.5em; margin: .5rem 0 0; }
.bdb-moves {
	display: flex; flex-wrap: wrap; gap: .15rem .7rem; margin: .6rem 0 0; padding: 0 0 0 1.8rem;
	max-height: 6.5em; overflow-y: auto; font-variant-numeric: tabular-nums;
}
.bdb-moves li { margin: 0 1.2rem 0 0; }
@media (prefers-reduced-motion: reduce) { .bdb .bdb-piece { transition: none; } }

/* ---- Scopa (plan_scopa/06) --------------------------------------------------
   Everything under .sco-, and NOTHING HERE RESTATES .card. The shared card kit
   (plan_cards) owns the face, the size and the back; this block owns only the
   board that holds them.

   NO STYLE ATTRIBUTE ANYWHERE, in this file or the template: the site's CSP
   forbids one. And no undefined custom property is read, because an undefined
   CSS variable is a silent inherit rather than an error (index_web_template).

   THE TABLE WRAPS AND NEVER SCROLLS SIDEWAYS. It holds four cards at rest and
   can reach twelve in a bad hand, so it is a flex row that wraps: the page
   scrolls down and never across, which is plan_scopa/09's mark 13.

   EVERY TAP TARGET IS AT OR ABOVE 44px, and a TABLE CARD IS A TAP TARGET here
   because the chooser taps them. That is what makes the small card sizes
   dangerous in this game in a way they are not in Briscola, where only the hand
   is tappable, so .sco-card carries its own min-height and min-width rather
   than inheriting whatever .card is today. */
.sco { display: flex; flex-direction: column; gap: 0.75rem; }

.sco-piles { display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; }
.sco-pile { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.sco-count { white-space: nowrap; }
.sco-sette { font-weight: 600; white-space: nowrap; }

.sco-table {
	display: flex; flex-wrap: wrap; gap: 0.4rem;
	min-height: 5.5rem; align-content: flex-start;
}
.sco-card, .sco-held { min-width: 44px; min-height: 44px; }

/* THE PICKED CLASS IS THE SELECTION, and it is Big Two's. It must read as
   chosen without relying on colour alone, so it carries an outline as well:
   a four-colour deck plus a colour-only cue is two things fighting. */
.sco-card.picked, .sco-held.picked { outline: 3px solid currentColor; outline-offset: 2px; }

.sco-empty { margin: 0; align-self: center; }

.sco-hand { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.sco-chooser { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.sco-sum { font-variant-numeric: tabular-nums; white-space: nowrap; }
.sco-why { flex: 1 1 12rem; }

.sco-score { display: flex; gap: 1rem; flex-wrap: wrap; align-items: baseline; }
.sco-points { white-space: nowrap; }
.sco-target, .sco-hand-no { white-space: nowrap; }

.sco-lines { margin: 0; padding: 0 0 0 1.1rem; }
.sco-lines li { margin: 0; }

/* THE SWEEP'S BEAT. The client adds .sco-swept for one beat before the table
   clears, so the opponent's answer is seen rather than inferred from the counts
   moving. Reduced motion gets the hold without the transition: the BEAT is
   information and the animation is not. */
.sco-table.sco-swept { transition: opacity 180ms ease-out; opacity: 0.45; }
@media (prefers-reduced-motion: reduce) { .sco-table.sco-swept { transition: none; } }

/* ---- royalur ---- */
/* The Royal Game of Ur (plan_game_royalur/11). A walnut frame, twenty squares
   of pale shell, and two kinds of piece that are two SHAPES: light a pale disc
   with a dark ring inside it, dark a coal disc with a pale rim and a pale dot
   at its centre. Neither is the other recoloured, and the two survive
   greyscale.

   THE BOARD STANDS ON END ON A NARROW SCREEN AND LIES DOWN ON A WIDE ONE. It
   is eight squares long and three wide, and lying down at a phone's width a
   square is under the 44px a finger needs. So below the breakpoint the three
   rows are three COLUMNS, eight squares tall; above it, three rows. One grid,
   two templates, every square placed by its own name: the markup does not
   change and a screen reader hears one board.

   THE VIEWER'S OWN ROW IS NEAREST THE VIEWER: on the left standing, at the
   bottom lying down. data-side="dark" swaps the two outer rows. A spectator
   sees light's.

   THE FOUR SQUARES THE BOARD DOES NOT HAVE HOLD THE HANDS AND THE HOMES, the
   pieces waiting beside the square they enter on and the pieces home beside
   the last one they leave.

   A ROSETTE IS EIGHT PETALS ROUND THE EDGE OF ITS SQUARE, not a mark in the
   middle, so it shows round a piece standing on it.

   THE MARKS ARE SHAPES, each a different frame on the square's ::after: the
   last move a solid frame (dotted on the square it left), the square a piece
   will land on a brass ring, a piece that landing would capture a thick
   dashed frame, the squares on the way a small dot. Colour is on top of each
   and never alone.

   Every colour is a MATERIAL token (--walnut, --hole, --brass) or a literal
   declared on .rur: none flips with the theme. `minmax(0, 1fr)` and not a
   bare `1fr`, whose min-content floor would let the page scroll sideways. No
   style attribute anywhere: the CSP forbids one. */
.rur {
	--rur-shell: #e9dcc0;
	--rur-shared: #d8c49a;
	--rur-ink: #2a1a0e;
	--rur-petal: #7d2f24;
	--rur-pale: #f6eedc;
	--rur-pale-edge: #c9b994;
	--rur-coal: #1f1712;
	--rur-coal-edge: #0b0806;
	--rur-ring: #fffdf5;
	--rur-take: #c2261c;
	--rur-go: #1f7a3a;
}
.rur-board {
	display: grid; gap: 3px;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	grid-template-rows: repeat(8, minmax(0, 1fr));
	grid-template-areas:
		"a1 a2 a3" "b1 b2 b3" "c1 c2 c3" "d1 d2 d3"
		"handl e2 handd" "homel f2 homed"
		"g1 g2 g3" "h1 h2 h3";
	width: min(100%, 21rem); aspect-ratio: 3 / 4.6; margin: 0 auto; padding: .35rem;
	box-sizing: border-box;
	background: var(--walnut); border-radius: 10px;
	box-shadow: inset 0 2px 8px rgba(0, 0, 0, .35), 0 3px 10px var(--shadow);
	touch-action: manipulation;
}
.rur-board[data-side="dark"] {
	grid-template-areas:
		"a3 a2 a1" "b3 b2 b1" "c3 c2 c1" "d3 d2 d1"
		"handd e2 handl" "homed f2 homel"
		"g3 g2 g1" "h3 h2 h1";
}
@media (min-width: 44rem) {
	.rur-board {
		grid-template-columns: repeat(8, minmax(0, 1fr));
		grid-template-rows: repeat(3, minmax(0, 1fr));
		grid-template-areas:
			"a3 b3 c3 d3 handd homed g3 h3"
			"a2 b2 c2 d2 e2 f2 g2 h2"
			"a1 b1 c1 d1 handl homel g1 h1";
		width: min(100%, 44rem); aspect-ratio: 8 / 3;
	}
	.rur-board[data-side="dark"] {
		grid-template-areas:
			"a1 b1 c1 d1 handl homel g1 h1"
			"a2 b2 c2 d2 e2 f2 g2 h2"
			"a3 b3 c3 d3 handd homed g3 h3";
	}
}
.rur .rur-at-a1 { grid-area: a1; } .rur .rur-at-b1 { grid-area: b1; } .rur .rur-at-c1 { grid-area: c1; } .rur .rur-at-d1 { grid-area: d1; }
.rur .rur-at-g1 { grid-area: g1; } .rur .rur-at-h1 { grid-area: h1; }
.rur .rur-at-a2 { grid-area: a2; } .rur .rur-at-b2 { grid-area: b2; } .rur .rur-at-c2 { grid-area: c2; } .rur .rur-at-d2 { grid-area: d2; }
.rur .rur-at-e2 { grid-area: e2; } .rur .rur-at-f2 { grid-area: f2; } .rur .rur-at-g2 { grid-area: g2; } .rur .rur-at-h2 { grid-area: h2; }
.rur .rur-at-a3 { grid-area: a3; } .rur .rur-at-b3 { grid-area: b3; } .rur .rur-at-c3 { grid-area: c3; } .rur .rur-at-d3 { grid-area: d3; }
.rur .rur-at-g3 { grid-area: g3; } .rur .rur-at-h3 { grid-area: h3; }
.rur .rur-hand-light { grid-area: handl; } .rur .rur-home-light { grid-area: homel; }
.rur .rur-hand-dark { grid-area: handd; } .rur .rur-home-dark { grid-area: homed; }

.rur .rur-cell {
	position: relative; display: grid; place-items: center;
	min-width: 0; min-height: 0; margin: 0; padding: 0;
	border: 0; border-radius: 4px; color: var(--rur-ink); font: inherit;
	appearance: none; cursor: default; background-color: var(--rur-shell);
}
.rur .rur-cell.rur-shared { background-color: var(--rur-shared); }
.rur .rur-cell:not(:disabled), .rur .rur-hand:not(:disabled) { cursor: pointer; }
/* A SQUARE THAT CANNOT BE PRESSED IS STILL A SQUARE WITH A PIECE ON IT. The
   site dims every disabled button, which is right for a control and wrong
   for most of a board most of the time. */
.rur .rur-cell:disabled, .rur .rur-hand:disabled { opacity: 1; }
.rur .rur-rose {
	position: absolute; left: 50%; top: 50%; height: 96%; aspect-ratio: 1; max-width: 96%;
	transform: translate(-50%, -50%); fill: var(--rur-petal); pointer-events: none;
}
.rur .rur-piece {
	position: relative; display: none; height: 58%; aspect-ratio: 1; max-width: 58%; border-radius: 50%;
	text-decoration: none; transition: transform .12s ease;
}
.rur .rur-light > .rur-piece, .rur .rur-dark > .rur-piece,
.rur .rur-hand > .rur-piece, .rur .rur-home > .rur-piece { display: block; }
.rur .rur-light > .rur-piece, .rur .rur-hand-light > .rur-piece, .rur .rur-home-light > .rur-piece {
	background: radial-gradient(circle at 35% 30%, #ffffff, var(--rur-pale) 55%, var(--rur-pale-edge));
	box-shadow: inset 0 0 0 4px var(--rur-pale), inset 0 0 0 7px var(--rur-coal), 0 2px 4px rgba(0, 0, 0, .5);
}
.rur .rur-dark > .rur-piece, .rur .rur-hand-dark > .rur-piece, .rur .rur-home-dark > .rur-piece {
	background:
		radial-gradient(circle, var(--rur-pale) 0 18%, transparent 20%),
		radial-gradient(circle at 35% 30%, #5b4b3f, var(--rur-coal) 55%, var(--rur-coal-edge));
	box-shadow: 0 0 0 2px rgba(246, 238, 220, .9), 0 2px 4px rgba(0, 0, 0, .5);
}
.rur .rur-cell::after {
	content: ""; position: absolute; inset: 3px; border: 0 solid transparent; border-radius: 3px; pointer-events: none;
}
.rur .rur-cell.rur-last::after { border: 2px solid var(--rur-ink); }
.rur .rur-cell.rur-last.rur-left::after { border: 2px dotted var(--rur-ink); }
.rur .rur-cell.rur-option:not(:disabled) > .rur-piece,
.rur .rur-hand.rur-option:not(:disabled) > .rur-piece { outline: 3px solid var(--rur-go); outline-offset: 2px; }
.rur .rur-cell.rur-path::after {
	inset: 44%; border: 0; border-radius: 50%; background: var(--rur-ink); opacity: .55;
}
.rur .rur-cell.rur-can-land::after, .rur .rur-home.rur-can-land::after {
	inset: 12%; border: 4px solid var(--brass); border-radius: 50%; background: none;
	box-shadow: 0 0 0 2px var(--hole);
}
.rur .rur-cell.rur-would-take::after {
	inset: 1px; border: 4px dashed var(--rur-take); border-radius: 0; background: none; box-shadow: none;
}
.rur .rur-cell.rur-hinted::after, .rur .rur-hand.rur-hinted::after {
	content: ""; position: absolute; inset: 0; border: 3px dashed var(--brass); border-radius: 0; background: none; box-shadow: none; pointer-events: none;
}
.rur .rur-cell.rur-refused > .rur-piece, .rur .rur-hand.rur-refused > .rur-piece { outline: 3px dashed var(--rur-take); outline-offset: 2px; }
.rur .rur-cell.rur-dragging > .rur-piece { opacity: .35; }
.rur .rur-cell:focus-visible, .rur .rur-hand:focus-visible { outline: 3px solid var(--rur-ring); outline-offset: -3px; z-index: 1; }

/* The hands and the homes, in the four gaps. A hand is a button and a home is
   not; both show one piece, how many, and a word. */
.rur .rur-hand, .rur .rur-home {
	position: relative; display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: center;
	gap: 0 .3rem; min-width: 0; min-height: 0; margin: 0; padding: 0 .15rem;
	border: 0; border-radius: 4px; background: none; color: var(--rur-pale); font: inherit; line-height: 1.1;
	appearance: none; cursor: default; overflow: hidden;
}
.rur .rur-hand > .rur-piece, .rur .rur-home > .rur-piece { height: 40%; max-width: 40%; }
.rur .rur-home > .rur-piece { opacity: .8; }
.rur .rur-count { font-size: 1.05rem; font-variant-numeric: tabular-nums; }
.rur .rur-word { flex-basis: 100%; font-size: .68rem; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rur .rur-home { position: relative; }
.rur .rur-home::after { content: ""; position: absolute; pointer-events: none; }

/* The dice: four triangles, the marked ones with a dot at the tip. */
.rur-roll { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; justify-content: center; min-height: 2rem; margin: .7rem 0 0; font-weight: 600; }
.rur-dice { display: inline-flex; gap: .3rem; }
.rur-die {
	position: relative; display: inline-block; width: 1.5rem; height: 1.35rem;
	background: var(--rur-pale-edge);
	clip-path: polygon(50% 4%, 97% 96%, 3% 96%);
}
.rur-die::after {
	content: ""; position: absolute; left: 50%; top: 30%; width: 26%; aspect-ratio: 1; border-radius: 50%;
	transform: translate(-50%, 0); background: none;
}
.rur-die.rur-marked { background: var(--rur-pale); }
.rur-die.rur-marked::after { background: var(--rur-coal); }
.rur-dice.rur-rolling .rur-die { animation: rur-tumble .3s ease-out; }
@keyframes rur-tumble { from { transform: rotate(-140deg) scale(.7); opacity: .4; } to { transform: none; opacity: 1; } }
.rur-why { min-height: 1.5em; margin: .25rem 0 0; text-align: center; }
.rur-moves {
	display: flex; flex-wrap: wrap; gap: .15rem .7rem; margin: .6rem 0 0; padding: 0 0 0 1.8rem;
	max-height: 6.5em; overflow-y: auto; font-variant-numeric: tabular-nums;
}
.rur-moves li { margin: 0 1.2rem 0 0; }
@media (prefers-reduced-motion: reduce) {
	.rur .rur-piece { transition: none; }
	.rur-dice.rur-rolling .rur-die { animation: none; }
}

/* ---- skyward (plan_skyward/07) ---------------------------------------------
 *
 * Up: two canvases and a table. NO COLOUR IS NAMED HERE: the seat inks, the
 * sky, the platforms and the floor are painted by the renderer, which reads
 * the theme itself, so this block is layout, the tap behaviour and the
 * reduced-motion rule, and every colour it does set comes from a token.
 *
 * THE PREFIX IS THE WHOLE WORD. Seven sessions share this file and a two or
 * three letter prefix is how two games collide; `.skyward-` was claimed by
 * message on 11 Oct 2026 and the six other sessions confirmed site.css was
 * free of it.
 */
.skyward { display: flex; flex-direction: column; gap: .5rem; align-items: center; }

/* THE STATUS LINE'S HEIGHT IS RESERVED, at three lines: the longest sentence
   wraps to two at 375 px, and a sentence that rewraps moves the play area
   under the thumb mid-climb (01 measured 4.5 px under a two-line reserve). */
.skyward-status { min-height: 4.4em; }

/* THE TAP TARGET IS THE PLAY AREA. `touch-action: none` or the page pans
   under a thumb that is trying to steer, and a tap is then a scroll. The
   controls release on pointercancel, because a phone raises one on a scroll,
   a call or a system gesture. */
.skyward-wrap {
  display: flex; flex-direction: column; gap: .35rem;
  touch-action: none; -webkit-user-select: none; user-select: none;
  cursor: pointer; border-radius: 6px; outline-offset: 3px;
}
.skyward-wrap:focus-visible { outline: 2px solid var(--accent); }

/* side by side when the window is short and wide: the second view is then a
   tall strip beside the main one, as the prototype has it; the client adds
   this class from fit() rather than guessing in a media query, because what
   matters is the height left under the status line. */
.skyward-wrap.skyward-side { flex-direction: row; align-items: flex-start; }

.skyward-canvas { display: block; border-radius: 6px; background: var(--surface-2); }
.skyward-view-main { box-shadow: 0 1px 3px var(--shadow); }
.skyward-view-mini { box-shadow: 0 1px 2px var(--shadow); }

.skyward-count { margin: 0; font-variant-numeric: tabular-nums; }
.skyward-finish { margin: 0; font-weight: 600; }
.skyward-hint { margin-left: .5rem; }

.skyward-seats { border-collapse: collapse; width: 100%; max-width: 30rem; }
.skyward-seats th, .skyward-seats td { padding: .25rem .5rem; text-align: left; border-bottom: 1px solid var(--line); }
.skyward-seats td + td, .skyward-seats th + th { text-align: right; }
.skyward-seat.you { background: var(--surface-2); }   /* not --surface: t/merrills-04 grades every block after Merrills' as its board and forbids the tokens that flip with the theme */
.skyward-name { font-weight: 600; }
.skyward-noscript { margin: .5rem 0 0; color: var(--muted); max-width: 34rem; }

/* REDUCED MOTION. The climb itself cannot be removed: it IS the game. What
   is removed is every motion that is not the climb: the camera's easing (the
   client tells the renderer to snap, and counts the frames it would have
   eased on as the positive control) and the shadow transitions here. */
@media (prefers-reduced-motion: reduce) {
  .skyward-canvas { transition: none !important; }
  .skyward-view-main, .skyward-view-mini { box-shadow: none; }
}

/* ---- fanorona ---- */
/* Fanorona (plan_game_fanorona/11). A board of lines: forty-five points where
   they cross, and a stone sits ON a crossing and never in a cell.

   ONE BOARD, DRAWN ONCE, TURNED FOUR WAYS. The board is always laid out lying
   down, nine files across and five ranks up, with a1 bottom left. On a wide
   screen it is shown so; on a narrow one the whole board is turned a quarter,
   so five points go across and nine down and a point is wide enough for a
   thumb. Either way it is turned again for the seat with the black stones, so
   that a viewer's own stones are nearest them: at the bottom lying down, on
   the left standing. The turning is a transform on the board inside a frame
   that has the turned shape, so nothing is drawn twice and a tap lands where
   the eye says.

   NO STYLE ATTRIBUTE PLACES ANYTHING: the site's CSP forbids one. A point is
   placed by two classes, its file and its rank, each of which sets one custom
   property, and both properties have a value on every point before a class
   says otherwise, so neither is ever undefined.

   A STONE IS A SHAPE AND AN OUTLINE, not only a fill: white is a pale disc
   with a dark edge, black a dark disc with a pale ring inside it, so the
   board reads in greyscale. A mark is a ring or a frame round the point, and
   the two lines of a step that takes two ways are framed two different ways,
   solid and dashed, as well as in two colours. */
.fnr {
	--fnr-wood: #dcbd86;
	--fnr-wood-edge: #8a5a2b;
	--fnr-line: #4a2f18;
	--fnr-pale: #f8f1df;
	--fnr-pale-edge: #5c4a2e;
	--fnr-coal: #1f1712;
	--fnr-coal-ring: #b9ad97;
	--fnr-go: #17703a;
	--fnr-take: #c2261c;
	--fnr-behind: #5b2a86;
	--fnr-trail: #1d4f91;
	--fnr-hint: #cfa64f;
}
.fnr-frame {
	position: relative; width: min(100%, 21rem); aspect-ratio: 5 / 9; margin: 0 auto;
	touch-action: manipulation;
}
.fnr-board {
	position: absolute; top: 0; left: 0; width: 180%; aspect-ratio: 9 / 5; box-sizing: border-box;
	transform-origin: 0 0; transform: rotate(90deg) translateY(-100%);
	background: var(--fnr-wood); border: 2px solid var(--fnr-wood-edge); border-radius: 10px;
	box-shadow: 0 0 10px var(--shadow);
}
.fnr-frame[data-side="black"] .fnr-board { transform: rotate(-90deg) translateX(-100%); }
@media (min-width: 44rem) {
	.fnr-frame { width: min(100%, 44rem); aspect-ratio: 9 / 5; }
	.fnr-board { width: 100%; transform-origin: 50% 50%; transform: none; }
	.fnr-frame[data-side="black"] .fnr-board { transform: rotate(180deg); }
}
.fnr-lines { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: block; pointer-events: none; }
.fnr-lines line { stroke: var(--fnr-line); stroke-width: .045; stroke-linecap: round; }

.fnr-point {
	--fnr-f: 0;
	--fnr-r: 0;
	position: absolute;
	left: calc((var(--fnr-f) + .5) * 100% / 9);
	top: calc((4.5 - var(--fnr-r)) * 100% / 5);
	width: calc(100% / 9 * .86); aspect-ratio: 1 / 1;
	transform: translate(-50%, -50%);
	margin: 0; padding: 0; border: 0; border-radius: 50%;
	background: transparent; color: inherit; font: inherit;
	display: grid; place-items: center;
	cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.fnr-point:disabled { cursor: default; opacity: 1; }
.fnr-point[aria-disabled="true"] { cursor: default; }
.fnr-f0 { --fnr-f: 0; } .fnr-f1 { --fnr-f: 1; } .fnr-f2 { --fnr-f: 2; }
.fnr-f3 { --fnr-f: 3; } .fnr-f4 { --fnr-f: 4; } .fnr-f5 { --fnr-f: 5; }
.fnr-f6 { --fnr-f: 6; } .fnr-f7 { --fnr-f: 7; } .fnr-f8 { --fnr-f: 8; }
.fnr-r0 { --fnr-r: 0; } .fnr-r1 { --fnr-r: 1; } .fnr-r2 { --fnr-r: 2; }
.fnr-r3 { --fnr-r: 3; } .fnr-r4 { --fnr-r: 4; }

/* An empty point is a dot where the lines cross, and a larger one where the
   diagonals cross as well. */
.fnr-stone {
	display: block; box-sizing: border-box; width: 16%; aspect-ratio: 1 / 1; border-radius: 50%;
	background: var(--fnr-line); border: 0 solid transparent;
}
.fnr-strong .fnr-stone { width: 24%; }
.fnr-white .fnr-stone {
	width: 80%;
	background: radial-gradient(circle at 35% 30%, #fffdf5, var(--fnr-pale) 60%);
	border: 2px solid var(--fnr-pale-edge);
	box-shadow: 0 1px 3px rgba(0, 0, 0, .45);
}
.fnr-black .fnr-stone {
	width: 80%;
	background: radial-gradient(circle at 35% 30%, #4a3d33, var(--fnr-coal) 60%);
	border: 2px solid #000;
	box-shadow: inset 0 0 0 3px var(--fnr-coal), inset 0 0 0 5px var(--fnr-coal-ring), 0 1px 3px rgba(0, 0, 0, .45);
}

/* ---- the marks, each a ring or a frame round the point ---- */
.fnr-option { box-shadow: 0 0 0 3px var(--fnr-go); }
.fnr-chosen, .fnr-chain { box-shadow: 0 0 0 4px var(--fnr-go), 0 0 0 7px var(--fnr-pale); }
.fnr-target .fnr-stone {
	width: 46%; background: transparent; border: 3px dashed var(--fnr-go);
}
.fnr-target:hover .fnr-stone, .fnr-target:focus-visible .fnr-stone { background: var(--fnr-go); }
.fnr-last-to { box-shadow: 0 0 0 3px var(--fnr-trail); }
.fnr-last-from .fnr-stone { width: 30%; background: transparent; border: 3px solid var(--fnr-trail); }
/* Where a stone was taken: a cross, which no stone and no empty point is. */
.fnr-taken .fnr-stone {
	width: 44%; border-radius: 0; border: 0;
	background:
		linear-gradient(45deg, transparent 42%, var(--fnr-take) 42%, var(--fnr-take) 58%, transparent 58%),
		linear-gradient(-45deg, transparent 42%, var(--fnr-take) 42%, var(--fnr-take) 58%, transparent 58%);
}
.fnr-would-take { box-shadow: 0 0 0 4px var(--fnr-take); }
.fnr-line-ahead { box-shadow: 0 0 0 4px var(--fnr-take); border-radius: 22%; }
.fnr-line-behind { outline: 4px dashed var(--fnr-behind); outline-offset: 1px; border-radius: 22%; }
.fnr-hinted { box-shadow: 0 0 0 4px var(--fnr-hint), 0 0 0 6px var(--fnr-coal); }
.fnr-point:focus-visible { outline: 3px solid var(--fnr-coal); outline-offset: 3px; }
.fnr-line-behind:focus-visible { outline: 4px dashed var(--fnr-behind); }

.fnr-step { min-height: 1.5em; margin: .6rem 0 0; text-align: center; font-weight: 600; }
.fnr-quiet { margin: .25rem 0 0; text-align: center; }
.fnr-quiet:empty, .fnr-step:empty { margin: 0; }
.fnr-draw { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; justify-content: center; margin: .5rem 0 0; }
.fnr-draw:empty { display: none; }
.fnr-acts { display: inline-flex; gap: .5rem; }
.fnr-acts:empty { display: none; }
.fnr-moves {
	display: flex; flex-wrap: wrap; gap: .15rem .9rem; margin: .75rem 0 0; padding: 0 0 0 0;
	list-style: none; counter-reset: fnr-move; font-variant-numeric: tabular-nums;
}
.fnr-moves li { counter-increment: fnr-move; }
.fnr-moves li::before { content: counter(fnr-move) ". "; color: var(--muted); }
.fnr-moves .fnr-by-p1, .fnr-moves .fnr-by-p2 { white-space: nowrap; }

@media (prefers-reduced-motion: no-preference) {
	.fnr-point { transition: box-shadow .12s ease-out; }
	.fnr-stone { transition: width .12s ease-out; }
}

/* ---- shogi (plan_game_shogi/12) ---------------------------------------------
   Nine by nine squares and two hands. Every colour is the block's own token,
   none of them one the dark block redefines: a board is wood in either theme.
   A piece is turned round when it belongs to the side the viewer is not, so
   your own pieces always point up the board, as the real ones point at the
   opponent; the tint is added on top of the turn and never instead of it. */
.shogi {
    --sg-wood: #e6c587;
    --sg-wood-deep: #8a6a34;
    --sg-line: #5a4422;
    --sg-face: #f7e9c4;
    --sg-edge: #6b5530;
    --sg-ink: #1d1d1d;
    --sg-red: #b0262c;
    --sg-tint: #dbe3ef;
    --sg-lit: rgba(46, 125, 50, 0.45);
    --sg-last: rgba(255, 193, 7, 0.45);
    --sg-check: rgba(198, 40, 40, 0.5);
    display: flex; flex-direction: column; align-items: center; gap: 6px;
}
.sg-frame {
    width: 100%; max-width: 440px; margin: 0; padding: 0;
    display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: auto auto; gap: 2px;
}
.sg-files { grid-column: 1; grid-row: 1; display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); text-align: center; font-size: 12px; opacity: 0.7; }
.sg-files i, .sg-ranks i { font-style: normal; }
.sg-ranks { grid-column: 2; grid-row: 2; display: grid; grid-template-rows: repeat(9, minmax(0, 1fr)); align-items: center; font-size: 12px; opacity: 0.7; padding-left: 3px; }
.sg-board {
    grid-column: 1; grid-row: 2;
    display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); grid-template-rows: repeat(9, minmax(0, 1fr));
    aspect-ratio: 1; background: var(--sg-wood); border: 2px solid var(--sg-wood-deep);
}
.sg-sq {
    position: relative; margin: 0; padding: 1px; min-width: 0; min-height: 0;
    border: 0; border-right: 1px solid var(--sg-line); border-bottom: 1px solid var(--sg-line);
    background: transparent; -webkit-appearance: none; appearance: none;
    display: flex; align-items: center; justify-content: center; color: inherit;
}
.sg-sq:nth-child(9n) { border-right: 0; }
.sg-sq:nth-child(n+73) { border-bottom: 0; }
.sg-sq:disabled { opacity: 1; cursor: default; }
.sg-playable, .sg-land { cursor: pointer; }
.sg-glyph { display: block; width: 92%; height: 92%; color: var(--sg-ink); }
.sg-pro .sg-glyph { color: var(--sg-red); }
.sg-body { fill: var(--sg-face); stroke: var(--sg-edge); stroke-width: 5; }
.sg-face-k, .sg-face-i { fill: currentColor; }
.sg-ray { fill: none; stroke: currentColor; stroke-width: 9; }
.sg-ring { fill: none; stroke: currentColor; stroke-width: 6; }
.shogi[data-colour="black"] .sg-white .sg-glyph,
.shogi[data-colour="white"] .sg-black .sg-glyph { transform: rotate(180deg); }
.shogi[data-colour="black"] .sg-white .sg-body,
.shogi[data-colour="white"] .sg-black .sg-body { fill: var(--sg-tint); }
.shogi .sg-face-i { display: none; }
.shogi[data-face="icons"] .sg-face-i { display: inline; }
.shogi[data-face="icons"] .sg-face-k { display: none; }
.sg-sq[aria-pressed="true"], .sg-held[aria-pressed="true"] { background: var(--sg-lit); }
.sg-last { background: var(--sg-last); }
.sg-checked { background: var(--sg-check); }
.sg-hinted { outline: 3px solid var(--sg-lit); outline-offset: -3px; }
.sg-land::after { content: ""; position: absolute; width: 30%; height: 30%; border-radius: 50%; background: var(--sg-lit); pointer-events: none; }
.sg-land.sg-pc::after { width: auto; height: auto; top: 6%; right: 6%; bottom: 6%; left: 6%; background: transparent; border: 3px solid var(--sg-lit); border-radius: 4px; }
.sg-dot-bl::before, .sg-dot-tl::before, .sg-dot-tr::before, .sg-dot-br::before {
    content: ""; position: absolute; width: 7px; height: 7px; border-radius: 50%; background: var(--sg-line); z-index: 1; pointer-events: none;
}
.sg-dot-bl::before { left: -4px; bottom: -4px; }
.sg-dot-tl::before { left: -4px; top: -4px; }
.sg-dot-tr::before { right: -4px; top: -4px; }
.sg-dot-br::before { right: -4px; bottom: -4px; }
.sg-hand { width: 100%; max-width: 440px; min-height: 48px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.sg-held {
    position: relative; width: 44px; height: 44px; margin: 0; padding: 2px;
    border: 1px solid transparent; border-radius: 4px; background: var(--sg-wood);
    -webkit-appearance: none; appearance: none; color: inherit;
}
.sg-held .sg-glyph { width: 100%; height: 100%; }
.sg-held:disabled { opacity: 1; cursor: default; }
.sg-held.sg-playable { border-color: var(--sg-wood-deep); }
.sg-n { position: absolute; right: -2px; bottom: -4px; font-style: normal; font-size: 12px; font-weight: 700; line-height: 16px; min-width: 16px; text-align: center; border-radius: 8px; background: var(--sg-face); color: var(--sg-ink); }
.sg-sq.sg-playable, .sg-held.sg-playable { touch-action: none; }
.sg-dragging .sg-glyph { opacity: 0.35; }
.sg-drag { position: fixed; z-index: 20; pointer-events: none; }
.sg-drag .sg-glyph { width: 100%; height: 100%; }
.sg-promote { display: flex; gap: 8px; justify-content: center; }
.sg-promote[hidden] { display: none; }

/* ---- sternhalma ---- */
/* Sternhalma (plan_game_sternhalma/11). A star of 121 holes, a piece in a
   hole, and lines joining each hole to its neighbours.

   ONE BOARD, TURNED A SIXTH AT A TIME. The star is drawn with the top point
   up, in a square frame; the frame's turn class turns the board so the
   viewer's own start is at the bottom, and turns each piece back so its mark
   stays upright. A six-pointed star turned a sixth is the same shape, so the
   square fits every turn, and a tap lands where the eye says.

   NO STYLE ATTRIBUTE PLACES ANYTHING: the site's CSP forbids one. A hole is
   placed by two classes, its column (half a hole wide) and its row, each of
   which sets one custom property, and both have a value on every hole before a
   class says otherwise, so neither is ever undefined.

   A PIECE IS A COLOUR AND A SHAPE, so six seats read without colour: red a dot,
   yellow a triangle, green a square, blue a diamond, purple a cross, orange a
   ring. Every colour is this block's own token, none one the dark block
   redefines: a board is a board in either theme. */
.sternhalma {
	--sternhalma-wood: #e8d3a6;
	--sternhalma-wood-edge: #8a6a34;
	--sternhalma-line: #b19461;
	--sternhalma-hole: #5c4a2e;
	--sternhalma-red: #d0312d;
	--sternhalma-yellow: #f2c418;
	--sternhalma-green: #2e9e44;
	--sternhalma-blue: #2563c9;
	--sternhalma-purple: #8a3fb8;
	--sternhalma-orange: #ee7b1c;
	--sternhalma-dark-mark: #1b1408;
	--sternhalma-light-mark: #fffaf0;
	--sternhalma-red-zone: #f1c6c3;
	--sternhalma-yellow-zone: #f6e7a6;
	--sternhalma-green-zone: #c6e6c9;
	--sternhalma-blue-zone: #c8d7f1;
	--sternhalma-purple-zone: #dfcaed;
	--sternhalma-orange-zone: #f6d3b2;
	--sternhalma-go: #157a3a;
	--sternhalma-path: #0f5bb8;
	--sternhalma-last: #7a4a12;
	--sternhalma-hint: #c99a12;
	--sternhalma-ring: #1b1408;
}
.sternhalma-frame {
	position: relative; width: min(100%, 34rem); aspect-ratio: 1 / 1; margin: 0 auto;
	touch-action: manipulation;
}
.sternhalma-board {
	position: absolute; top: 0; left: 0; width: 100%; height: 100%; box-sizing: border-box;
	background: var(--sternhalma-wood); border: 2px solid var(--sternhalma-wood-edge); border-radius: 50%;
	transform-origin: 50% 50%;
}
.sternhalma-turn-1 .sternhalma-board { transform: rotate(60deg); }
.sternhalma-turn-2 .sternhalma-board { transform: rotate(120deg); }
.sternhalma-turn-3 .sternhalma-board { transform: rotate(180deg); }
.sternhalma-turn-4 .sternhalma-board { transform: rotate(240deg); }
.sternhalma-turn-5 .sternhalma-board { transform: rotate(300deg); }
.sternhalma-turn-1 .sternhalma-board .sternhalma-piece { transform: rotate(-60deg); }
.sternhalma-turn-2 .sternhalma-board .sternhalma-piece { transform: rotate(-120deg); }
.sternhalma-turn-3 .sternhalma-board .sternhalma-piece { transform: rotate(-180deg); }
.sternhalma-turn-4 .sternhalma-board .sternhalma-piece { transform: rotate(-240deg); }
.sternhalma-turn-5 .sternhalma-board .sternhalma-piece { transform: rotate(-300deg); }
.sternhalma-lines { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: block; pointer-events: none; }
.sternhalma-lines line { stroke: var(--sternhalma-line); stroke-width: .16; stroke-linecap: round; }

/* A HOLE IS PLACED ON A GRID OF 29 BY 29 UNITS round the centre: a column is
   one unit, a row the height of a triangle on two. */
.sternhalma-hole {
	--sternhalma-x: 12;
	--sternhalma-y: 8;
	position: absolute;
	left: calc(50% + (var(--sternhalma-x) - 12) * 100% / 29);
	top: calc(50% + (var(--sternhalma-y) - 8) * 1.7320508 * 100% / 29);
	width: calc(100% * 1.9 / 29); aspect-ratio: 1 / 1;
	transform: translate(-50%, -50%);
	margin: 0; padding: 0; border: 0; border-radius: 50%;
	background: var(--sternhalma-wood); color: inherit; font: inherit;
	display: grid; place-items: center;
	cursor: pointer; -webkit-tap-highlight-color: transparent;
}
/* A HOLE THAT CANNOT BE PRESSED LETS THE TAP THROUGH to the board, which
   gives it to the nearest candidate: a disabled button swallows a click whole,
   and a thumb on an empty hole beside the one it meant would do nothing. */
.sternhalma-hole:disabled { cursor: default; opacity: 1; pointer-events: none; }
.sternhalma-hole[aria-disabled="true"] { cursor: default; }
.sternhalma-x0 { --sternhalma-x: 0; } .sternhalma-x1 { --sternhalma-x: 1; } .sternhalma-x2 { --sternhalma-x: 2; }
.sternhalma-x3 { --sternhalma-x: 3; } .sternhalma-x4 { --sternhalma-x: 4; } .sternhalma-x5 { --sternhalma-x: 5; }
.sternhalma-x6 { --sternhalma-x: 6; } .sternhalma-x7 { --sternhalma-x: 7; } .sternhalma-x8 { --sternhalma-x: 8; }
.sternhalma-x9 { --sternhalma-x: 9; } .sternhalma-x10 { --sternhalma-x: 10; } .sternhalma-x11 { --sternhalma-x: 11; }
.sternhalma-x12 { --sternhalma-x: 12; } .sternhalma-x13 { --sternhalma-x: 13; } .sternhalma-x14 { --sternhalma-x: 14; }
.sternhalma-x15 { --sternhalma-x: 15; } .sternhalma-x16 { --sternhalma-x: 16; } .sternhalma-x17 { --sternhalma-x: 17; }
.sternhalma-x18 { --sternhalma-x: 18; } .sternhalma-x19 { --sternhalma-x: 19; } .sternhalma-x20 { --sternhalma-x: 20; }
.sternhalma-x21 { --sternhalma-x: 21; } .sternhalma-x22 { --sternhalma-x: 22; } .sternhalma-x23 { --sternhalma-x: 23; }
.sternhalma-x24 { --sternhalma-x: 24; }
.sternhalma-y0 { --sternhalma-y: 0; } .sternhalma-y1 { --sternhalma-y: 1; } .sternhalma-y2 { --sternhalma-y: 2; }
.sternhalma-y3 { --sternhalma-y: 3; } .sternhalma-y4 { --sternhalma-y: 4; } .sternhalma-y5 { --sternhalma-y: 5; }
.sternhalma-y6 { --sternhalma-y: 6; } .sternhalma-y7 { --sternhalma-y: 7; } .sternhalma-y8 { --sternhalma-y: 8; }
.sternhalma-y9 { --sternhalma-y: 9; } .sternhalma-y10 { --sternhalma-y: 10; } .sternhalma-y11 { --sternhalma-y: 11; }
.sternhalma-y12 { --sternhalma-y: 12; } .sternhalma-y13 { --sternhalma-y: 13; } .sternhalma-y14 { --sternhalma-y: 14; }
.sternhalma-y15 { --sternhalma-y: 15; } .sternhalma-y16 { --sternhalma-y: 16; }

/* A point in play is tinted the colour of the seat going there. */
.sternhalma-zone-red { background: var(--sternhalma-red-zone); }
.sternhalma-zone-yellow { background: var(--sternhalma-yellow-zone); }
.sternhalma-zone-green { background: var(--sternhalma-green-zone); }
.sternhalma-zone-blue { background: var(--sternhalma-blue-zone); }
.sternhalma-zone-purple { background: var(--sternhalma-purple-zone); }
.sternhalma-zone-orange { background: var(--sternhalma-orange-zone); }

/* An empty hole is a dark dot; a piece fills it. */
.sternhalma-piece {
	display: grid; place-items: center; box-sizing: border-box; width: 30%; aspect-ratio: 1 / 1; border-radius: 50%;
	background: var(--sternhalma-hole); border: 0 solid transparent;
}
.sternhalma-mark { display: none; width: 46%; aspect-ratio: 1 / 1; }
.sternhalma-has-red .sternhalma-piece, .sternhalma-has-yellow .sternhalma-piece, .sternhalma-has-green .sternhalma-piece,
.sternhalma-has-blue .sternhalma-piece, .sternhalma-has-purple .sternhalma-piece, .sternhalma-has-orange .sternhalma-piece {
	width: 90%; border: 2px solid var(--sternhalma-ring);
	box-shadow: inset 0 -2px 0 rgba(0, 0, 0, .25), 0 1px 2px rgba(0, 0, 0, .4);
}
.sternhalma-has-red .sternhalma-mark, .sternhalma-has-yellow .sternhalma-mark, .sternhalma-has-green .sternhalma-mark,
.sternhalma-has-blue .sternhalma-mark, .sternhalma-has-purple .sternhalma-mark, .sternhalma-has-orange .sternhalma-mark { display: block; }
.sternhalma-has-red .sternhalma-piece { background: var(--sternhalma-red); }
.sternhalma-has-yellow .sternhalma-piece { background: var(--sternhalma-yellow); }
.sternhalma-has-green .sternhalma-piece { background: var(--sternhalma-green); }
.sternhalma-has-blue .sternhalma-piece { background: var(--sternhalma-blue); }
.sternhalma-has-purple .sternhalma-piece { background: var(--sternhalma-purple); }
.sternhalma-has-orange .sternhalma-piece { background: var(--sternhalma-orange); }
.sternhalma-has-red .sternhalma-mark { border-radius: 50%; background: var(--sternhalma-light-mark); width: 36%; }
.sternhalma-has-yellow .sternhalma-mark { clip-path: polygon(50% 6%, 96% 90%, 4% 90%); background: var(--sternhalma-dark-mark); }
.sternhalma-has-green .sternhalma-mark { background: var(--sternhalma-dark-mark); width: 40%; }
.sternhalma-has-blue .sternhalma-mark { clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); background: var(--sternhalma-light-mark); }
.sternhalma-has-purple .sternhalma-mark { clip-path: polygon(38% 0, 62% 0, 62% 38%, 100% 38%, 100% 62%, 62% 62%, 62% 100%, 38% 100%, 38% 62%, 0 62%, 0 38%, 38% 38%); background: var(--sternhalma-light-mark); }
.sternhalma-has-orange .sternhalma-mark { border-radius: 50%; background: transparent; border: 3px solid var(--sternhalma-dark-mark); box-sizing: border-box; }

/* ---- the marks, each a ring round the hole ---- */
.sternhalma-option { box-shadow: 0 0 0 3px var(--sternhalma-go); }
.sternhalma-chosen { box-shadow: 0 0 0 3px var(--sternhalma-go), 0 0 0 6px var(--sternhalma-light-mark); }
.sternhalma-target .sternhalma-piece { width: 46%; background: transparent; border: 3px dashed var(--sternhalma-go); }
.sternhalma-target:hover .sternhalma-piece, .sternhalma-target:focus-visible .sternhalma-piece { background: var(--sternhalma-go); }
.sternhalma-way { box-shadow: 0 0 0 3px var(--sternhalma-path); }
.sternhalma-last-to { box-shadow: 0 0 0 3px var(--sternhalma-last); }
.sternhalma-last-from .sternhalma-piece { width: 40%; background: transparent; border: 3px solid var(--sternhalma-last); }
.sternhalma-last-path .sternhalma-piece { width: 34%; background: var(--sternhalma-last); }
.sternhalma-hinted { box-shadow: 0 0 0 4px var(--sternhalma-hint), 0 0 0 6px var(--sternhalma-ring); }
.sternhalma-hole:focus-visible { outline: 3px solid var(--sternhalma-ring); outline-offset: 2px; }

.sternhalma-step { min-height: 1.5em; margin: .6rem 0 0; text-align: center; font-weight: 600; }
.sternhalma-step:empty { margin: 0; }
.sternhalma-seats {
	display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr)); gap: .3rem .8rem;
	margin: .6rem 0 0; padding: 0; list-style: none;
}
.sternhalma-seat { display: flex; align-items: center; gap: .4rem; min-width: 0; }
.sternhalma-seat .sternhalma-piece { width: 1.4rem; flex: none; }
.sternhalma-seat b, .sternhalma-seat .sternhalma-said { min-width: 0; overflow-wrap: anywhere; }
.sternhalma-seat.sternhalma-turn b { text-decoration: underline; text-underline-offset: .2em; }
.sternhalma-moves {
	display: flex; flex-wrap: wrap; gap: .15rem .9rem; margin: .75rem 0 0; padding: 0;
	list-style: none; counter-reset: sternhalma-move; font-variant-numeric: tabular-nums;
}
.sternhalma-moves li { counter-increment: sternhalma-move; white-space: nowrap; }
.sternhalma-moves li::before { content: counter(sternhalma-move) ". "; }

@media (prefers-reduced-motion: no-preference) {
	.sternhalma-hole { transition: box-shadow .12s ease-out; }
	.sternhalma-piece { transition: width .12s ease-out; }
}

/* ---- senet ---- */
/* Senet (plan_game_senet/11). A walnut frame, thirty houses of sandstone, and
   two kinds of piece that are two SHAPES, as the surviving Egyptian sets are:
   light a spool, pale with a dark rim, and dark a cone, coal with a pale rim.
   Neither is the other recoloured, and the two survive greyscale.

   THE BOARD STANDS ON END ON A NARROW SCREEN AND LIES DOWN ON A WIDE ONE. It
   is ten houses long and three wide, and lying down at a phone's width a
   house is a quarter under the 44px a finger needs. Standing, it is the lying
   grid TRANSPOSED, not turned: 1 to 10 down the first column, 20 to 11 down
   the second, 21 to 30 down the third, so house 1 is top left and 30 bottom
   right both ways. One grid, two templates, every house placed by its own
   name: the markup, which is the track in order, does not change.

   BOTH SIDES SHARE THE TRACK, so nothing turns with the seat.

   A MARKED HOUSE CARRIES ITS SIGN, drawn under the piece. A PROTECTED piece
   sits on a band that joins it to its neighbours; a blockade's band is
   double. Both come from the view and are never counted here.

   THE MARKS ARE SHAPES, each a different frame on the house's ::after: the
   last move a solid frame (dotted on the house it left), the house a piece
   will land on a brass ring, a piece that landing would swap a thick dashed
   frame, the houses on the way a small dot, a ring for a move back, a wave
   for the water. Colour is on top of each and never alone.

   Every colour is a MATERIAL token (--walnut, --brass, --hole) or a literal
   declared on .snt: none flips with the theme. `minmax(0, 1fr)` and not a
   bare `1fr`. No style attribute anywhere: the CSP forbids one. */
.snt {
	--snt-sand: #e8d6ac;
	--snt-marked: #dcc28d;
	--snt-ink: #2a1a0e;
	--snt-ochre: #9a3f22;
	--snt-ivory: #f7f0dd;
	--snt-ivory-edge: #3a2a1c;
	--snt-coal: #241911;
	--snt-coal-edge: #f3e7c8;
	--snt-ring: #fffdf5;
	--snt-swap: #c2261c;
	--snt-go: #1f7a3a;
	--snt-stick-dark: #3b2a1d;
	--snt-stick-light: #f3e7c8;
}
.snt-board {
	display: grid; gap: 3px;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	grid-template-rows: repeat(10, minmax(2.75rem, 1fr)) minmax(2.75rem, .9fr);
	grid-template-areas:
		"h1 h20 h21"
		"h2 h19 h22"
		"h3 h18 h23"
		"h4 h17 h24"
		"h5 h16 h25"
		"h6 h15 h26"
		"h7 h14 h27"
		"h8 h13 h28"
		"h9 h12 h29"
		"h10 h11 h30"
		". hd hl";
	width: min(100%, 21rem); aspect-ratio: 3 / 5.4; margin: 0 auto; padding: .35rem;
	box-sizing: border-box;
	background: var(--walnut); border-radius: 10px;
	box-shadow: inset 0 2px 8px rgba(0, 0, 0, .35), 0 3px 10px var(--shadow);
	touch-action: manipulation;
}
@media (min-width: 36rem) {
	.snt-board {
		grid-template-columns: repeat(10, minmax(0, 1fr));
		grid-template-rows: repeat(3, minmax(0, 1fr)) minmax(0, .8fr);
		grid-template-areas:
			"h1 h2 h3 h4 h5 h6 h7 h8 h9 h10"
			"h20 h19 h18 h17 h16 h15 h14 h13 h12 h11"
			"h21 h22 h23 h24 h25 h26 h27 h28 h29 h30"
			". . . . . . hd hd hl hl";
		width: min(100%, 48rem); aspect-ratio: 10 / 3.9;
	}
}
.snt .snt-h1 { grid-area: h1; } .snt .snt-h2 { grid-area: h2; } .snt .snt-h3 { grid-area: h3; } .snt .snt-h4 { grid-area: h4; } .snt .snt-h5 { grid-area: h5; }
.snt .snt-h6 { grid-area: h6; } .snt .snt-h7 { grid-area: h7; } .snt .snt-h8 { grid-area: h8; } .snt .snt-h9 { grid-area: h9; } .snt .snt-h10 { grid-area: h10; }
.snt .snt-h11 { grid-area: h11; } .snt .snt-h12 { grid-area: h12; } .snt .snt-h13 { grid-area: h13; } .snt .snt-h14 { grid-area: h14; } .snt .snt-h15 { grid-area: h15; }
.snt .snt-h16 { grid-area: h16; } .snt .snt-h17 { grid-area: h17; } .snt .snt-h18 { grid-area: h18; } .snt .snt-h19 { grid-area: h19; } .snt .snt-h20 { grid-area: h20; }
.snt .snt-h21 { grid-area: h21; } .snt .snt-h22 { grid-area: h22; } .snt .snt-h23 { grid-area: h23; } .snt .snt-h24 { grid-area: h24; } .snt .snt-h25 { grid-area: h25; }
.snt .snt-h26 { grid-area: h26; } .snt .snt-h27 { grid-area: h27; } .snt .snt-h28 { grid-area: h28; } .snt .snt-h29 { grid-area: h29; } .snt .snt-h30 { grid-area: h30; }
.snt .snt-home-dark { grid-area: hd; } .snt .snt-home-light { grid-area: hl; }

.snt .snt-house {
	position: relative; display: grid; place-items: center;
	min-width: 0; min-height: 0; margin: 0; padding: 0;
	border: 0; border-radius: 4px; color: var(--snt-ink); font: inherit;
	appearance: none; cursor: default; background-color: var(--snt-sand);
}
.snt .snt-house.snt-marked { background-color: var(--snt-marked); }
/* A SAFE HOUSE, where no piece is swapped: its number is underlined twice. */
.snt .snt-house.snt-safe .snt-n { text-decoration: underline double; text-underline-offset: 2px; opacity: .9; }
.snt .snt-house:not(:disabled) { cursor: pointer; }
/* A HOUSE THAT CANNOT BE PRESSED IS STILL A HOUSE WITH A PIECE ON IT: the
   site dims every disabled button, which is wrong for most of a board. */
.snt .snt-house:disabled { opacity: 1; }
.snt .snt-n {
	position: absolute; left: 4px; top: 2px; font-size: .62rem; line-height: 1;
	font-variant-numeric: tabular-nums; opacity: .7; pointer-events: none;
}
.snt .snt-sign {
	position: absolute; left: 50%; top: 50%; height: 86%; aspect-ratio: 1; max-width: 86%;
	transform: translate(-50%, -50%); pointer-events: none;
	fill: none; stroke: var(--snt-ochre); stroke-width: 7; stroke-linecap: round; opacity: .75;
}
.snt .snt-sign .snt-ankh, .snt .snt-sign .snt-nefer { stroke-width: 8; }
.snt .snt-sign .snt-water { stroke-width: 6; }
.snt .snt-sign .snt-strokes { stroke-width: 9; }

/* THE PIECES ARE TWO SHAPES. The rim is the piece itself, clipped to its
   shape, and the fill an inset copy of the same shape over it: a clipped box
   has no border or shadow of its own, so this is the edge. */
.snt .snt-piece, .snt .snt-ghost {
	position: relative; display: none; height: 64%; aspect-ratio: 1; max-width: 64%;
	text-decoration: none; transition: transform .12s ease;
}
.snt .snt-piece::before, .snt .snt-ghost::before { content: ""; position: absolute; inset: 9%; }
.snt .snt-light > .snt-piece, .snt .snt-dark > .snt-piece, .snt .snt-home > .snt-piece { display: block; }
.snt .snt-light > .snt-piece, .snt .snt-home-light > .snt-piece, .snt .snt-ghost-light > .snt-ghost,
.snt .snt-light > .snt-piece::before, .snt .snt-home-light > .snt-piece::before, .snt .snt-ghost-light > .snt-ghost::before {
	clip-path: polygon(10% 6%, 90% 6%, 90% 20%, 66% 34%, 66% 66%, 90% 80%, 90% 94%, 10% 94%, 10% 80%, 34% 66%, 34% 34%, 10% 20%);
}
.snt .snt-light > .snt-piece, .snt .snt-home-light > .snt-piece, .snt .snt-ghost-light > .snt-ghost { background: var(--snt-ivory-edge); }
.snt .snt-light > .snt-piece::before, .snt .snt-home-light > .snt-piece::before, .snt .snt-ghost-light > .snt-ghost::before {
	background: linear-gradient(90deg, #d9ccae, var(--snt-ivory) 45%, #ffffff 60%, #d9ccae);
}
.snt .snt-dark > .snt-piece, .snt .snt-home-dark > .snt-piece, .snt .snt-ghost-dark > .snt-ghost,
.snt .snt-dark > .snt-piece::before, .snt .snt-home-dark > .snt-piece::before, .snt .snt-ghost-dark > .snt-ghost::before {
	clip-path: polygon(50% 2%, 62% 12%, 92% 96%, 8% 96%, 38% 12%);
}
.snt .snt-dark > .snt-piece, .snt .snt-home-dark > .snt-piece, .snt .snt-ghost-dark > .snt-ghost { background: var(--snt-coal-edge); }
.snt .snt-dark > .snt-piece::before, .snt .snt-home-dark > .snt-piece::before, .snt .snt-ghost-dark > .snt-ghost::before {
	inset: 14% 12% 8%;
	background: linear-gradient(90deg, #120c08, #5b4b3f 45%, var(--snt-coal));
}
/* Where a swapped piece will stand: a ghost of it, on the house the mover leaves. */
.snt .snt-ghost-light > .snt-ghost, .snt .snt-ghost-dark > .snt-ghost { display: block; position: absolute; opacity: .45; }

/* A run of one side: a band along the foot of the house, reaching across the
   gap to the next; a blockade's band is double. */
.snt .snt-band { position: absolute; left: -3px; right: -3px; bottom: 3px; height: 0; pointer-events: none; }
.snt .snt-protected > .snt-band { height: 4px; background: var(--snt-ink); opacity: .7; }
.snt .snt-blockade > .snt-band { height: 9px; background: none; border-top: 3px solid var(--snt-ink); border-bottom: 3px solid var(--snt-ink); box-sizing: border-box; opacity: .8; }

.snt .snt-house::after {
	content: ""; position: absolute; inset: 3px; border: 0 solid transparent; border-radius: 3px; pointer-events: none;
}
.snt .snt-house.snt-last::after { border: 2px solid var(--snt-ink); }
.snt .snt-house.snt-last.snt-left::after { border: 2px dotted var(--snt-ink); }
.snt .snt-house.snt-option:not(:disabled) { box-shadow: inset 0 0 0 3px var(--snt-go); }
.snt .snt-board[data-side="dark"] .snt-house.snt-option:not(:disabled) { box-shadow: inset 0 0 0 3px var(--snt-go), inset 0 0 0 5px var(--snt-coal-edge); }
.snt .snt-house.snt-path::after { inset: 44%; border: 0; border-radius: 50%; background: var(--snt-ink); opacity: .55; }
.snt .snt-house.snt-path-back::after { inset: 38%; border: 2px solid var(--snt-ink); border-radius: 50%; background: none; opacity: .7; }
.snt .snt-house.snt-path-water::after { inset: 40% 22%; border: 0; border-top: 3px solid var(--snt-ink); border-bottom: 3px solid var(--snt-ink); border-radius: 40%; background: none; opacity: .6; }
.snt .snt-house.snt-can-land::after, .snt .snt-home.snt-can-land::after {
	inset: 12%; border: 4px solid var(--brass); border-radius: 50%; background: none; box-shadow: 0 0 0 2px var(--hole);
}
.snt .snt-house.snt-would-swap::after { inset: 1px; border: 4px dashed var(--snt-swap); border-radius: 0; background: none; box-shadow: none; }
.snt .snt-house.snt-hinted::after { inset: 0; border: 3px dashed var(--brass); border-radius: 0; background: none; box-shadow: none; }
.snt .snt-house.snt-refused { outline: 3px dashed var(--snt-swap); outline-offset: -3px; }
.snt .snt-house.snt-dragging > .snt-piece { opacity: .35; }
.snt .snt-house:focus-visible { outline: 3px solid var(--snt-ring); outline-offset: -3px; z-index: 1; }

/* The homes, beyond house 30: one piece, how many, and a word. */
.snt .snt-home {
	position: relative; display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
	gap: 0 .3rem; min-width: 0; min-height: 0; margin: 0; padding: 0 .15rem;
	border-radius: 4px; color: var(--snt-coal-edge); line-height: 1.1; overflow: hidden;
}
.snt .snt-home > .snt-piece { height: 46%; max-width: 46%; opacity: .85; }
.snt .snt-home::after { content: ""; position: absolute; pointer-events: none; }
.snt .snt-count { font-size: 1.05rem; font-variant-numeric: tabular-nums; }
.snt .snt-word { flex-basis: 100%; font-size: .68rem; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The sticks: four flat bars, the light face pale, the dark face dark; and
   beside them the roll as a digit, which is the number a player reads. */
.snt-throw { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; justify-content: center; min-height: 2.6rem; margin: .7rem 0 0; font-weight: 600; }
.snt-sticks { display: inline-flex; gap: .3rem; }
.snt-stick {
	display: inline-block; width: .55rem; height: 2.2rem; border-radius: .3rem;
	background: linear-gradient(90deg, #22170f, var(--snt-stick-dark) 50%, #22170f);
}
.snt-stick.snt-up { background: var(--snt-stick-light); box-shadow: inset 0 0 0 2px var(--snt-stick-dark); }
.snt-digit { min-width: 1.4rem; font-size: 1.6rem; text-align: center; font-variant-numeric: tabular-nums; }
.snt-sticks.snt-falling .snt-stick { animation: snt-fall .32s ease-out; }
@keyframes snt-fall { from { transform: translateY(-.7rem) rotate(-18deg); opacity: .4; } to { transform: none; opacity: 1; } }
.snt-threw { font-weight: 400; }
.snt-why { min-height: 1.5em; margin: .25rem 0 0; text-align: center; }
.snt-moves {
	display: flex; flex-wrap: wrap; gap: .15rem .7rem; margin: .6rem 0 0; padding: 0; list-style-position: inside;
	max-height: 6.5em; overflow-y: auto; font-variant-numeric: tabular-nums;
}
.snt-moves li { margin: 0 1.2rem 0 0; }
.snt-moves .snt-by-p1, .snt-moves .snt-by-p2 { white-space: nowrap; }
@media (prefers-reduced-motion: reduce) {
	.snt .snt-piece { transition: none; }
	.snt-sticks.snt-falling .snt-stick { animation: none; }
}
