.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid red;
}

.validation-message {
    color: red;
}

#blazor-error-ui {
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
    }

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.loading-progress {
    position: relative;
    display: block;
    width: 8rem;
    height: 8rem;
    margin: 20vh auto 1rem auto;
}

    .loading-progress circle {
        fill: none;
        stroke: #e0e0e0;
        stroke-width: 0.6rem;
        transform-origin: 50% 50%;
        transform: rotate(-90deg);
    }

        .loading-progress circle:last-child {
            stroke: #1b6ec2;
            stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
            transition: stroke-dasharray 0.05s ease-in-out;
        }

.loading-progress-text {
    position: absolute;
    text-align: center;
    font-weight: bold;
    inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}

    .loading-progress-text:after {
        content: var(--blazor-load-percentage-text, "Loading");
    }

code {
    color: #c02d76;
}

/* Everything above this line is the verbatim Blazor WebAssembly project template (issue 482). Its
   class names - valid / invalid / modified / validation-message / blazor-error-boundary /
   loading-progress - are written by the FRAMEWORK, never by this repo, so no markup here spells them
   and none may be deleted as unused. CssClassReconciliationTests reads this marker to bound the
   block; moving it down the sheet would excuse whatever it swallows. */
/* @caracri-template-block-end */

.app { max-width: 900px; margin: 0 auto; padding: 1rem; }
.error { color: #ff8a80; }
.message { color: #86e0a3; }

/* Canvas board (drag-and-drop). Wrapper keeps the legacy .board class for E2E, but the
   scene is drawn on <canvas>, so the old grid rules are neutralised here. */
.board.board-stage { display: block; background: none; width: auto; gap: 0; margin: 1rem 0; }
.board-canvas {
    display: block;
    width: 100%;
    max-width: 680px;
    margin-inline: auto;              /* #311 */
    aspect-ratio: 1 / 1;
    border-radius: 0;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
    touch-action: none;
    user-select: none;
}
/* #296: the canvas is square, the BOARD inside it need not be. board.js sets this class from the
   shape it actually drew; a shadow cast from the canvas edge would outline a square the board does
   not fill, which is the same "chrome reports the canvas, not the board" defect as the backdrop. */
.board-canvas.board-nonsquare { box-shadow: none; }
.board-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.75rem; margin-inline: auto; max-width: 680px; }  /* #311 */
/* #302: the online game passes no actions (they live in the side column), so the row was 12px of
   dead margin under every board — margin the sticky wide-layout column cannot afford on a 390px
   screen. Hot-seat boards keep their chips, and the row with them. */
.board-actions:empty { display: none; }

/* Wait: a permanent part of the board (not an action chip). Shows the acting player's occupied
   count vs the opponent's; bold + clickable only when a pass is actually allowed. */
/* #306 (2.4 gate review): the plate art is a PNG stretched `100% 100%` whose drawn rounded-rect
   covers ~66% of the image height, so its transparent margin is always ~17% OF THE BOX HEIGHT -
   the interior a label may occupy is only [.173H, .837H]. min-height alone sizes the box for ONE
   line, so a wrapped localized label (ru "nelzya propustit" in a 260px landscape column) landed
   OUTSIDE the artwork and the gold border ran through its glyphs. Vertical padding is the only
   lever that grows the box WITH the wrap, which is what an inset-of-height plate needs: at 1rem a
   one-line bar is still exactly 52px (min-height wins) and a two-line one grows to ~74px, keeping
   the text clear of the drawn edge. The budget is TWO lines of the largest shipped type; a third
   would need ~1.35rem and a taller bar than the #314 phone chrome can pay for, so the line is held
   by Application\test-plate-label-fit.ps1 instead of by hope - it lays every locale out at every
   reviewed viewport and reddens the moment a string needs more room than the plate has. */
.board-topbar {
    display: flex; align-items: center; justify-content: center; gap: 0.55rem; box-sizing: border-box;
    width: 100%; max-width: 680px; min-height: 52px;
    padding: 1rem 1.15rem; margin-bottom: 0.4rem; margin-inline: auto;   /* #311 */
    background: var(--inline-waitbar) center / 100% 100% no-repeat;
    color: var(--ink-dim); user-select: none; font-variant-numeric: tabular-nums; cursor: default;
    position: relative; overflow: hidden; /* #274: contains the skip-cue fill */
}
/* #77: the numeric run ("First 0 ≤ 0") must NEVER wrap — a dangling digit on line two read as a
   bug (owner report e7512ff3, RU @360px). The word label may wrap as whole words; narrow
   viewports shrink the bar's type instead. */
.board-topbar .wait-counts { font-weight: 700; color: var(--ink); white-space: nowrap; }
.board-topbar .wait-op { padding: 0 0.15rem; }
@media (max-width: 480px) {
    /* #306: padding-INLINE only. The shorthand here used to reset the vertical padding to 0, which
       is exactly the phone width where the labels wrap - the narrow bar's vertical room must come
       from one place, the rule above. */
    .board-topbar { font-size: .85rem; gap: .4rem; padding-inline: .7rem; }
    .board-topbar .wait-label { font-size: .85rem; }
    .board-topbar.can .wait-label { font-size: .9rem; letter-spacing: .8px; }
}
.board-topbar .wait-icon { width: 20px; height: 20px; }
.board-topbar .wait-win { font-weight: 800; font-size: 1.1rem; letter-spacing: 0.5px; }
/* The winner's name glows in that player's PIECE colour, so both take the board palette through the
   one declaration (#402). The text itself is a lighter reading tint, not a board colour. */
.board-topbar.win-blue .wait-win { color: #6bb0ff; text-shadow: 0 0 10px rgba(var(--piece-blue-rgb), .6); }
.board-topbar.win-yellow .wait-win { color: #ffd84a; text-shadow: 0 0 10px rgba(var(--piece-yellow-rgb), .6); }
.board-topbar.can { background-image: var(--inline-waitbar-can); cursor: pointer; color: #fff; }
.board-topbar.can .wait-label { font-weight: 800; color: #fff; letter-spacing: 1.5px; font-size: 1.05rem; }
.board-topbar.can:hover { filter: brightness(1.06); }
.board-topbar:focus-visible { outline: 2px solid #1565c0; outline-offset: 2px; }
/* #274: skip just became available (the opponent's turn ended). A colour wash sweeps outward from
   the centre of the bar, one shot, so the state change is NOTICED rather than merely rendered.
   BoardCanvas gates the class on the same "move animations" setting board.js reads. */
.board-topbar.skip-cue::after {
    content: ""; position: absolute; inset: 0; pointer-events: none;
    background: radial-gradient(circle at center, rgba(255,216,74,.9) 0%, rgba(255,216,74,.5) 45%, rgba(255,216,74,0) 72%);
    animation: skip-cue-fill 1.2s ease-out forwards;
}
@keyframes skip-cue-fill {
    from { transform: scale(.05); opacity: 1; }
    70%  { transform: scale(1.5); opacity: .85; }
    to   { transform: scale(2.4); opacity: 0; }
}

/* Clickable areas replacing native buttons/selects */
.chip {
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0.5rem 1rem; border-radius: 999px; cursor: pointer; user-select: none;
    background: #eceff1; color: #263238; font-weight: 500; line-height: 1;
    border: 1px solid #cfd8dc; transition: background 0.12s, transform 0.06s;
    /* #200: a pill button must never wrap its own label. In a flex row (.board-actions) the default
       flex-shrink squeezed the Undo/Redo chips, and dense CJK ("되돌리기"/"元に戻す") then wrapped to
       3 lines. nowrap keeps each chip one line; the ROW wraps instead (flex-wrap on the container). */
    white-space: nowrap;
}
/* NOTE: no :hover background rules here — legacy light-theme hover colors (#e0e5e8 etc.) used to
   lurk under the dark skin's image swap and surfaced as the infamous "stuck white button" on touch
   the moment the swap was removed. Hover styling lives ONLY in the dark-skin section below. */
.chip:active { transform: translateY(1px); }
.chip:focus-visible { outline: 2px solid #1565c0; outline-offset: 2px; }
.chip.disabled { opacity: 0.4; pointer-events: none; }
.chip-primary { background: #2e7d32; color: #fff; border-color: #2e7d32; }
.chip-wait { background: #fff3e0; border-color: #ffcc80; color: #e65100; }

.tile-row { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.5rem 0 1rem; }
.tile {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 4rem; padding: 0.75rem 1rem; border-radius: 10px; cursor: pointer; user-select: none;
    background: #eceff1; border: 2px solid #cfd8dc; font-weight: 600; color: #37474f;
    transition: background 0.12s, border-color 0.12s;
    /* Issue 497: the BOX is constrained, the string is never shortened. Every option label on
       /newgame is a locale string, and the AI strength names ("Maximum" / "Максимальная" /
       "מרבית") are the first ones whose length is a translator's choice rather than a token.
       Both properties are one-directional - max-width can only shrink a tile that would
       otherwise have run past its row, and overflow-wrap only breaks a word that would
       otherwise have overflowed - so no tile that already fits moves by a pixel. Same remedy
       as .tile.ai-tile got for a long nickname (#257), applied to the tile every group uses. */
    max-width: 100%; overflow-wrap: anywhere; text-align: center;
}
.tile.selected { background: #e8f5e9; border-color: #2e7d32; color: #1b5e20; }
.tile:focus-visible { outline: 2px solid #1565c0; outline-offset: 2px; }
/* #188: a vs-AI player tile NAMES its player. #257 ("Remove subtext"): the engine sublabel is gone
   from the tile — the engine stays readable in-game (.p-card .p-ai). Column layout kept so a long
   nickname can wrap to a second line inside the pill instead of widening it past its row. */
.tile.ai-tile { flex-direction: column; gap: .1rem; line-height: 1.25; }
.tile.ai-tile .ai-name { overflow-wrap: anywhere; text-align: center; }

.setup { max-width: 680px; margin-inline: auto; }                        /* #311 */
.game-page .lede { color: #546e7a; max-width: 680px; margin-inline: auto; }   /* #311 */

.game-page .moves { margin-top: 1rem; color: #607d8b; }
/* Reserve a stable line so the turn text can never change the block height and shift the
   board under the player's finger (#50 — "don't move controls under the finger"). */
.game-page .turn { color: var(--gold); font-weight: 600; min-height: 1.4em; }
.game-page .turn.over { color: #ff8a80; }

/* End-of-game result summary */
.result-summary { margin: 0.25rem 0 0.5rem; }
.result-summary .result-heading { font-weight: 700; font-size: 1.15rem; margin: 0 0 0.15rem;
    /* #302: Game.razor scrolls this heading into view after the viewer's own resign (caracri.reveal);
       without the margin it lands UNDER the pinned .topnav (55px) — same trap and same remedy as the
       #150 filter input (scroll-margin-top: 70px). */
    scroll-margin-top: 70px; }
.result-summary .result-heading.res-won { color: #6ed88a; }
.result-summary .result-heading.res-lost { color: #ff8a8a; }
.result-summary .result-heading.res-draw { color: var(--ink-dim); }
.result-summary .result-rating { margin: 0; color: var(--ink-dim); }
/* Rematch (#90): offer/accept row under the result summary; the note text stays dim. */
.result-summary .rematch-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem; }
.result-summary .rematch-note { color: var(--ink-dim); }
.result-summary .rematch-waiting { animation: ctaPulse 1.6s ease-in-out infinite; }

/* ── Game clock (#3) ──────────────────────────────────────────────────────────── */
/* #311: `auto` in the inline slot, not `0`. `.app` is `max-width: 1040px; margin: 0 auto`, so the
   SHELL is centred - but a block capped narrower than the shell and given `margin: <v> 0 <v>` sits
   hard against the inline-start edge of it, with all of the slack on one side. The clock pair
   centres its CONTENTS (justify-content), which is why the block sitting off to the side read as
   "the menu is off-centre" rather than "the text is". Invisible on a phone, where the shell is
   narrower than the cap and every one of these blocks fills its column; visible from ~490px up.
   Held by Application\test-shell-block-centring.ps1 for the whole family, not for this rule. */
.clocks { display: flex; gap: 0.6rem; justify-content: center; margin: 0.4rem auto 0.6rem; max-width: 460px; }
.clocks .clock {
    flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 0.1rem;
    padding: 0.4rem 0.5rem; border-radius: 12px; border: 1px solid rgba(201,162,75,.18);
    background: rgba(18,25,37,.6); transition: border-color .15s, box-shadow .15s, opacity .15s;
}
/* The two seat identities, in the two PIECE colours (#402). .clock.yellow used to take --gold, which
   is also .clock.running's colour below at the same specificity (0,3,0) — so the yellow seat's 3px top
   edge was #c9a24b whether it was that player's turn or not and the strongest of the three running
   cues was inert on exactly one of the two seats. Board entity -> board palette. */
.clocks .clock.blue { border-top: 3px solid var(--piece-blue); }
.clocks .clock.yellow { border-top: 3px solid var(--piece-yellow); }
.clocks .clock .clock-side { font-size: 0.72rem; color: var(--ink-dim); text-transform: uppercase; letter-spacing: .5px; }
.clocks .clock .clock-time { font-size: 1.5rem; font-weight: 800; font-variant-numeric: tabular-nums; color: var(--ink); }
.clocks .clock.running { border-color: var(--gold); box-shadow: 0 0 0 1px rgba(201,162,75,.35), 0 4px 14px rgba(0,0,0,.35); }
.clocks .clock.running .clock-time { color: #fff; }
.clocks .clock.mine .clock-side { color: var(--gold); }
.clocks.paused .clock { opacity: .55; }

.pause-bar { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; justify-content: center; margin: 0 auto 0.7rem; max-width: 460px; }  /* #311 */
.pause-bar .pause-badge { font-weight: 800; color: var(--gold); text-transform: uppercase; letter-spacing: .5px; }
.pause-bar .pause-msg { color: var(--ink-dim); }
.review-row { align-items: center; }
/* min-width kept small: 6rem forced the counter wide and overflowed the landscape side column
   (higher specificity than the sizing rules further down — keep these two in sync). */
.review-row .review-label { color: var(--ink-dim); font-variant-numeric: tabular-nums; min-width: 34px; text-align: center; }
.review-row .chip.disabled { opacity: .4; pointer-events: none; }
/* #556: the analysed-board toggle is its own row beside the board. It remains available at the
   initial review position even though that position deliberately has no move indicator. */
.analysis-board-visualization { margin: .45rem 0 .65rem; }
.analysis-board-toggle { flex: 1 1 100%; justify-content: center; text-align: center;
    white-space: normal; overflow-wrap: anywhere; }
.analysis-board-toggle.selected { border-image-source: var(--inline-pill-primary); font-weight: 700; }
/* ===== #423 deliverable 6: the post-game analysis panel =====================================
   Built to the standing mobile directive: at a real phone width (the 360x531 report) the BOX
   gives and the STRING never does. Every text element here is `overflow-wrap: anywhere` with no
   width of its own, and the only fixed sizes in the block are the two bars, which are decoration.
   Concretely, the three things that would break it at 360px and what each costs:
     1. .panel's 1.6rem side padding is 51px of a 360px screen. This panel takes it back to 1rem
        (32px), which is the whole difference between "Analysing position 12 of 35" fitting on one
        line and wrapping to three.
     2. The head is a WRAPPING flex row, so the balance chip drops under the title rather than
        squeezing it - a title and a readout on one crushed line is the #393 inversion.
     3. The played/best pair is a two-column grid whose LABEL column is `minmax(0, max-content)`,
        not `max-content`: a long localized label ("Сыграно", "실제 수") then shrinks and wraps
        instead of pushing the value off the panel. */
.analysis-panel { padding: 0.9rem 1rem; margin-top: 0.6rem; }
.analysis-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0.3rem 0.5rem; }
.analysis-title { margin: 0; font-size: 1.05rem; min-width: 0; overflow-wrap: anywhere; }
.analysis-badge, .analysis-points {
    flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere;
    padding: 0.1rem 0.5rem; border-radius: 999px; font-size: 0.8rem; line-height: 1.4;
}
.analysis-badge { background: rgba(46,125,50,.24); color: #a5d6a7; }
.analysis-points { background: rgba(255,255,255,.08); color: var(--ink-dim); font-variant-numeric: tabular-nums; }

/* == ISSUE 650 - THE READING RESERVES ONLY ITS COMPACT ROWS, SO THE CONTROL DOES NOT MOVE =========
   Owner feedback c0bbac18 (in-app reporter, Android 10 / SamsungBrowser 30, 360x273 CSS px, dpr 3,
   /game/{id} at review position 8 of 20): "the accuracy info sometimes one line and sometimes two
   lines. this makes controls jump". `.move-analysis` had NO rule in this sheet at all - its height
   was pure content height with no floor - and in every layout OUTSIDE the two reorder blocks below
   (the wide grid, and portrait at 800px+) it sits in normal flow ABOVE the review row the player is
   tapping. So every step moved the next tap target out from under the finger. Four things varied
   with the position, and the fourth is the one the report itself missed:
     1. `.analysis-verdict` wraps. It is a sentence built from a five-band phrase table
        (analysis.band.*), it may break anywhere, and it had no floor - one line or two.
     2. `.analysis-match` renders only when the played move WAS the best one.
     3. `.analysis-quality` renders only where the stored review classified the move.
     4. THE WHOLE BLOCK was absent at position zero and present at position one, because it opened
        on `AnalysisLastMovePly` - so a single Forward tap moved the row by 90..138px, not by the
        48px the two optional rows are worth. Game.razor now opens the block on the READING (the
        balance of the position on screen, stored at every position) and keeps only the three
        move-specific rows behind the move; this rule holds the height those rows will take.
   Owner follow-up 9a3705d0 found the first reservation stable but wasteful at 360x531: Played and
   Best occupied two rows, Best move played was followed by a second standalone Best badge, and the
   whole block still reserved both. The compact shape has one four-cell move row and one shared
   status row: either Best move played OR the quality/loss, never both for the same best move.
   THE RESERVATION IS STILL DERIVED, NOT TUNED. Every term below is a row the compact block can draw,
   expressed in that row's own rem/line-height terms. The verdict owns its two-line slot directly,
   so a one-line sentence leaves its necessary reserve beside that sentence instead of as unexplained
   trailing space below the final status row.
   Nothing is truncated: the verdict still owns two lines and localized move labels may wrap; the
   landscape rail adds the two extra lines that its narrower declared width needs below.
   WHY THIS DOES NOT REOPEN ISSUE 621. That issue is "the review row is below the fold at 390x844",
   and there the row LEADS the rail (order: -1, the ordering block below), so the reading sits UNDER
   the control: measured at 390x844, `.rev-btn` is at the same 748.9 with the reservation as without
   it, in every position. What the reservation touches is issue 554's SPAN (.board-canvas through
   .move-analysis must fit one frame), and it makes that contract's WORST case better rather than
   worse - the span was 750px with a one-line verdict and 771px with a wrapped one against 781px of
   usable frame, and it is now a constant 761px. A contract that could already be missed by 10px is
   now missed by nothing. Measured at both stacked viewports by
   Application\test-section-fold-layout.ps1.
   The invariance itself is measured by Application\test-review-row-stability.ps1, which holds the
   viewport at the reported 360px and varies the POSITION instead. */
.move-analysis {
    /* ONE DECLARED LINE-HEIGHT, so every row below is a known multiple of it instead of a font
       metric this repo cannot measure on the reporter's device: normal is about 1.32 in this
       sheet's first font and about 1.17 in Roboto, which is the font the report came from. A
       reservation that is a different number on the phone than on the box that measures it is not
       a reservation. Nothing gets smaller - 1.3 is within a third of a pixel of what this stack
       already renders. */
    line-height: 1.3;
    min-height: calc(12px + 0.55rem + 0.4rem /* .analysis-bar: leading gap, track, trailing gap */
                   + 2.6rem + 0.4rem     /* .analysis-verdict: TWO 1.3rem lines, and the gap under them */
                   + 1.3rem              /* .analysis-moves: Played + Best share ONE row */
                   + 0.35rem + 1.22rem); /* shared status: the taller quality chip; match is shorter */
}

/* The evaluation bar: First's share painted over Second's colour, the same two colours the
   players strip's dots carry, so "blue is ahead" needs no legend. */
.analysis-bar { height: 12px; border-radius: 6px; overflow: hidden; margin: 0.55rem 0 0.4rem; background: #f4b91c; }
.analysis-bar-first { display: block; height: 100%; background: #1e78e0; }
.analysis-verdict { min-height: 2.6rem; margin: 0 0 0.4rem; font-weight: 600; overflow-wrap: anywhere; }

/* Both columns hug their content and the pair is packed to the READING edge. A 1fr value
   column looked identical in English and pushed the value to the far side of the panel in
   Hebrew - the label at the right margin and its own value 200px away at the left one -
   because the value's numeric container carries dir="ltr" and aligns to the LEFT of a column
   that RTL had already moved to the left. MoveText separately isolates the move in UI direction.
   Move tokens may not break internally; labels can give space to the whole notation. */
.analysis-moves { display: grid;
    grid-template-columns: repeat(2, minmax(0, max-content) minmax(0, max-content));
    justify-content: start; column-gap: 0.45rem; margin: 0; }
.analysis-moves .am-label { margin: 0; color: var(--ink-dim); font-size: 0.85rem; overflow-wrap: anywhere; }
.analysis-moves .am-value { margin: 0; min-width: 0; font-variant-numeric: tabular-nums; overflow-wrap: normal; }
.analysis-moves .am-value.am-best { color: #a5d6a7; font-weight: 700; }
.analysis-match { margin: 0.3rem 0 0; color: #a5d6a7; font-size: 0.85rem; overflow-wrap: anywhere; }

/* The determinate progress bar. Its width is written inline from the job's own position count,
   never from a clock, so it cannot claim progress the analyser has not made. */
.analysis-track { height: 10px; border-radius: 5px; overflow: hidden; background: rgba(255,255,255,.12); margin: 0.5rem 0 0.4rem; }
.analysis-fill { display: block; height: 100%; background: #1e78e0; }
.analysis-progress-line { margin: 0; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }

.analysis-offer, .analysis-refusal { margin: 0.4rem 0 0.2rem; overflow-wrap: anywhere; }
.analysis-note, .analysis-server-reason {
    margin: 0.3rem 0 0; font-size: 0.8rem; line-height: 1.4; overflow-wrap: anywhere;
}
/* The one action is full-width on a phone and hugs its label on a wide screen: a lone CTA that
   sits at 40% of a 360px panel reads as disabled. */
.analysis-actions { margin-top: 0.5rem; }
.analysis-actions .chip.cta, .analysis-actions a.chip { max-width: 100%; text-decoration: none; }
/* A chip that WRAPS says so in its own class list. `.chip` is nowrap by design (#200: a pill
   button must never wrap its label; the ROW wraps instead), and the in-run UX audit holds every
   `.chip.cta` to exactly that. This CTA is the case that rule was not written for - its label is a
   translated SENTENCE (`Проанализировать партию`), not the word or two on an Undo chip - so it opts
   out, once, visibly, and the audit skips the chips that carry the marker and no others. Setting
   `white-space: normal` on an ancestor selector instead is what made twelve gate steps report a
   UX-CRITICAL that was a deliberate design decision nobody could see from the markup. */
.chip.cta.wraps { overflow-wrap: anywhere; white-space: normal; }
@media (max-width: 599px) {
    .analysis-actions { display: flex; }
    .analysis-actions .chip.cta, .analysis-actions a.chip { flex: 1 1 auto; text-align: center; justify-content: center; }
}

/* ===== #460: the review over the stored analysis. Nothing here is fetched - every number is
   derived from the record the panel already holds, so the summary costs no analysis points.
   The 360px rule, which is the whole reason this block is shaped the way it is:
     1. The bucket row is a WRAPPING flex list of pills, never a five-column grid. Five localized
        labels beside five counts ("Неточность", "부정확한 수") cannot share one 360px line in any
        language, so the row becomes three lines instead of pushing the panel sideways.
     2. Every pill hugs its content, may shrink (min-width: 0) and may break inside a word
        (overflow-wrap: anywhere), so the longest label in the longest language still lands inside
        the box. A cap with no wrap is a clip, not a cap - and the string is never the fix.
     3. The head is a wrapping flex row, like .analysis-head: the accuracy readout drops under the
        player line rather than crushing it (the #393 inversion).
     4. The turning point and the worst move reuse .analysis-moves - the label/value pair #423
        already proved in RTL, where the value carries dir="ltr" so a move notation keeps its
        hyphen. */
.analysis-quality { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.4rem; margin: 0.35rem 0 0; min-width: 0; }
.aq-chip { padding: 0.05rem 0.45rem; border-radius: 999px; font-size: 0.8rem; line-height: 1.4; min-width: 0; overflow-wrap: anywhere; }
.aq-loss { font-size: 0.8rem; color: var(--ink-dim); font-variant-numeric: tabular-nums; min-width: 0; overflow-wrap: anywhere; }

/* The turning point and the costliest move are label/value PAIRS, laid out exactly like the
   played/best pair above them (#423's grid, and for its reason: both columns hug, the pair packs
   to the reading edge, and the value carries dir="ltr" so a move notation keeps its hyphen in
   Hebrew). Their own class rather than .analysis-moves so that "is the played/best pair on screen"
   stays a question with one answer. */
.review-pair { display: grid; grid-template-columns: minmax(0, max-content) minmax(0, max-content);
    justify-content: start; gap: 0.15rem 0.6rem; margin: 0.3rem 0 0; }
.review-pair .rv-label { margin: 0; color: var(--ink-dim); font-size: 0.85rem; overflow-wrap: anywhere; }
.review-pair .rv-value { margin: 0; min-width: 0; font-variant-numeric: tabular-nums; overflow-wrap: normal; }
.review-pair.analysis-highlight { appearance: none; max-width: 100%; padding: 0.15rem 0.25rem;
    border: 1px solid transparent; border-radius: 0.35rem; background: transparent; color: inherit;
    font: inherit; text-align: start; cursor: pointer; }
.review-pair.analysis-highlight:hover { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.review-pair.analysis-highlight:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.review-pair.analysis-highlight .rv-value { color: var(--accent); text-decoration: underline;
    text-decoration-thickness: 1px; text-underline-offset: 0.16em; }

.review-summary { margin-top: 0.55rem; padding-top: 0.45rem; border-top: 1px solid rgba(255,255,255,.10); min-width: 0; }
.review-note { margin: 0.25rem 0 0; font-size: 0.8rem; line-height: 1.4; overflow-wrap: anywhere; }
.review-player { margin-top: 0.5rem; min-width: 0; }
.rp-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0.15rem 0.5rem; }
.rp-side { min-width: 0; overflow-wrap: anywhere; font-weight: 600; font-size: 0.88rem; }
.rp-accuracy { min-width: 0; overflow-wrap: anywhere; font-size: 0.85rem; color: var(--ink-dim); font-variant-numeric: tabular-nums; }
.rp-buckets { display: flex; flex-wrap: wrap; gap: 0.25rem; margin: 0.3rem 0 0; padding: 0; list-style: none; min-width: 0; }
.rp-bucket { display: flex; align-items: baseline; gap: 0.25rem; max-width: 100%; min-width: 0; overflow-wrap: anywhere;
    padding: 0.05rem 0.45rem; border-radius: 999px; font-size: 0.78rem; line-height: 1.45; background: rgba(255,255,255,.07); }
.rpb-count { font-variant-numeric: tabular-nums; font-weight: 700; }
.rpb-label { min-width: 0; overflow-wrap: anywhere; }
/* One rung, one colour, in both places it is drawn - the chip on the reviewed move and the pill in
   the counts - so a colour cannot come to mean two different things. Declared AFTER .rp-bucket, at
   the same specificity, which is what lets the rung's own background win over the neutral one. */
.q-best { background: rgba(46,125,50,.24); color: #a5d6a7; }
.q-good { background: rgba(124,179,66,.18); color: #cfe6a8; }
.q-inaccuracy { background: rgba(244,185,28,.18); color: #f4d488; }
.q-mistake { background: rgba(230,126,34,.20); color: #ffbe8a; }
.q-blunder { background: rgba(198,40,40,.26); color: #ef9a9a; }

/* ===== #498: THE COACH. The top five moves for ONE position, each with a winning chance, at a
   strength the player picks. Its own panel rather than a corner of the analysis one: they are
   different capabilities, bought separately, and the coach is offered on boards the analysis is not.

   The 360px rule, which is the whole reason this block is shaped the way it is (measured by
   Application\test-coach-panel-layout.ps1 against this stylesheet at exactly 360px):
     1. Every row here is a WRAPPING flex row, never a grid of fixed columns. A candidate is
        "rank / move / chance / played-here", four items whose localized last one is a WORD
        (`сыграно`, `실제 수`), and four items cannot share one 360px line in every language - so
        the row becomes two lines instead of pushing the panel sideways.
     2. Every text span may shrink (min-width: 0) and may break inside a word
        (overflow-wrap: anywhere), so the longest string in the longest language lands inside its
        BOX. The box is what gives; the string is never truncated, because the next longer string
        would reopen the report.
     3. The strength picker is a wrapping row of chips with its label above/beside it, so three
        localized rung names (`Максимальная`) drop to a second line rather than crushing each other.
     4. The chance column is tabular-nums and dir="ltr": a percentage must not have its digits
        reordered in Hebrew, and five of them should line up under each other. */
.coach-panel { padding: 0.9rem 1rem; margin-top: 0.6rem; }
.coach-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0.3rem 0.5rem; }
.coach-title { margin: 0; font-size: 1.05rem; min-width: 0; overflow-wrap: anywhere; }
.coach-points {
    flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere;
    padding: 0.1rem 0.5rem; border-radius: 999px; font-size: 0.8rem; line-height: 1.4;
    background: rgba(255,255,255,.08); color: var(--ink-dim); font-variant-numeric: tabular-nums;
}
.coach-lead { margin: 0.4rem 0 0; font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.coach-note, .coach-none { margin: 0.3rem 0 0; font-size: 0.8rem; line-height: 1.4; overflow-wrap: anywhere; }
.coach-refusal, .coach-working { margin: 0.4rem 0 0.2rem; overflow-wrap: anywhere; }

/* The ranked candidates. An ordered list with its own marker suppressed - the rank is drawn as a
   numbered pill inside the row, because a browser's own marker sits OUTSIDE the padding and is the
   first thing to fall off a narrow panel. */
.coach-list { list-style: none; margin: 0.45rem 0 0; padding: 0; min-width: 0; }
.coach-move { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.5rem;
    padding: 0.2rem 0; border-top: 1px solid rgba(255,255,255,.07); min-width: 0; }
.coach-move:first-child { border-top: 0; }
.coach-move .cm-rank { flex: 0 0 auto; min-width: 1.4em; text-align: center;
    font-variant-numeric: tabular-nums; font-weight: 700; color: var(--ink-dim); font-size: 0.85rem; }
.coach-move .cm-move { min-width: 0; overflow-wrap: normal; font-variant-numeric: tabular-nums; font-weight: 600; }
.coach-move .cm-chance { margin-inline-start: auto; min-width: 0; overflow-wrap: anywhere;
    font-variant-numeric: tabular-nums; color: #cfe3ff; }
.coach-move .cm-tag { flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere;
    padding: 0.05rem 0.45rem; border-radius: 999px; font-size: 0.75rem; line-height: 1.45;
    background: rgba(255,255,255,.07); color: var(--ink-dim); }
/* The move the player actually made, marked on the row rather than only by the tag: on a wrapped
   two-line row the tag can be the far end of the second line, and the reader is looking at the
   move. */
.coach-move.cm-played .cm-move { color: #a5d6a7; }

.coach-actions { margin-top: 0.5rem; }
.coach-actions .chip.cta, .coach-actions a.chip { max-width: 100%; text-decoration: none; }
@media (max-width: 599px) {
    .coach-actions { display: flex; }
    .coach-actions .chip.cta, .coach-actions a.chip { flex: 1 1 auto; text-align: center; justify-content: center; }
}

/* ===== ISSUE 701: the finished-game portrait Coach dock =======================================
   The Coach answer leads .game-side, which is the block immediately after the board in the
   stacked DOM. The rest of the review and analysis rail follows it; none of those unrelated rows
   can push the answer away from the board again.

   THE RESERVATION IS DERIVED FROM THE ROWS IT HOLDS, like .move-analysis above. At the declared
   1.2 line-height it is: .7rem panel padding + 1.26rem heading + (.15 + 1.2) position line +
   (.15 + 6.6) five candidate rows + (.15 + 2.1) action + (.1 + 1.04) cost line = 13.45rem.
   The candidate term is five x (one 1.2rem line + .06rem padding on each edge). A localized row
   that needs more than its declared line scrolls INSIDE the ranked list; it is never clipped or
   truncated, and it cannot move the Ask action or everything below the panel. Offer and Working
   occupy the same box as Ready, while margin-block-start:auto pins the action to the identical
   y-offset in all three states.

   Issue 701 introduced this for portrait phones; issue 702 reuses the same bounded reading for
   landscape and square, where it leads the side rail beside the board. The portrait probe measures
   both portrait viewports plus the three-state 360x640 Russian invariant; the full probe measures
   every landscape/square catalogue and side. */
@media (max-width: 599px) and (orientation: portrait), (min-aspect-ratio: 1/1) {
    .game-layout:not(.in-play) > .game-side { display: flex; flex-direction: column; }
    .game-layout:not(.in-play) > .game-side > .coach-panel {
        --coach-dock-height: calc(.7rem + 1.26rem + .15rem + 1.2rem + .15rem + 6.6rem + .15rem + 2.1rem + .1rem + 1.04rem);
        order: -2; box-sizing: border-box; display: flex; flex-direction: column;
        height: var(--coach-dock-height); min-height: var(--coach-dock-height);
        margin-top: 0; padding-block: .35rem; line-height: 1.2;
    }
    .game-layout:not(.in-play) > .game-side > .coach-panel .coach-list {
        flex: 1 1 6.6rem; min-height: 0; margin-top: .15rem; overflow-y: auto;
        scrollbar-width: thin; overscroll-behavior: contain;
    }
    .game-layout:not(.in-play) > .game-side > .coach-panel .coach-working {
        flex: 1 1 6.6rem; min-height: 0; margin: .15rem 0 0;
    }
    .game-layout:not(.in-play) > .game-side > .coach-panel .coach-lead { margin-top: .15rem; }
    .game-layout:not(.in-play) > .game-side > .coach-panel .coach-move { padding-block: .06rem; }
    .game-layout:not(.in-play) > .game-side > .coach-panel .coach-actions { margin-top: auto; }
    .game-layout:not(.in-play) > .game-side > .coach-panel .coach-note { margin-top: .1rem; }
}

/* ui-space-value: Issue 702's 800x360 risk did falsify the full 13.45rem reservation: with the real Japanese
   result card the rail starts at 196.125px, leaving 163.875px in the first frame. Short landscape
   therefore gets its own third branch. The fixed header/action/note stay visible and the ranked
   list remains the one contained scroll; only that variable-height reading gives. 12.3rem is
   196.8px at the root size, retaining 0.675px of measured fold clearance in the worst catalogue. */
@media (min-aspect-ratio: 1/1) and (max-height: 480px) { /* ui-space-value: 480px is a media condition; this block gives vertical space back */
    .game-layout:not(.in-play) > .game-side > .coach-panel {
        --coach-dock-height: min(13.45rem, calc(100vh - 12.3rem));
    }
}

.game-chat { margin-top: 1.5rem; margin-inline: auto; border: 1px solid #ccc; border-radius: 4px; padding: 0.75rem; max-width: 400px; }  /* #311 */
.game-chat h2 { margin: 0 0 0.5rem 0; font-size: 1rem; }
.chat-messages { max-height: 200px; overflow-y: auto; margin-bottom: 0.5rem; }
.chat-line { margin-bottom: 0.25rem; word-break: break-word; }
.chat-send { display: flex; gap: 0.5rem; }
.chat-send input { flex: 1; padding: 0.35rem; }
.chat-send button { padding: 0.35rem 0.75rem; }

/* ===================== Premium modern theme (Phase 2 shell) ===================== */
:root {
    --ink: #f2efe6;
    --ink-dim: #9fb0c3;
    --gold: #c9a24b;
    --sheet: #f6f7f9;
    --sheet-ink: #1f2733;

    /* #483 - THE DESTRUCTIVE PALETTE, as this stylesheet's ONE copy of it. Before this block the
       reds were six literals in four rules - `.admin-btn.danger` and its hover, `.admin-badge.danger`
       and `.chip-danger` each picked their own - and OUTSIDE the admin console the `danger` class
       carried no red at all: four arm-then-confirm sites arm it on a plate control and matched no
       rule, so the second tap of a deletion rendered exactly like the first. Naming the colours
       here is what stops /admin and the rest of the product meaning different things by the same
       word - a new destructive surface reads a token instead of choosing a red. Every value below
       is one already shipping, moved not retuned, except the two the armed plate adds (-bg-lift,
       -stroke), which a 34px admin button had no use for. */
    --danger-bg: #7d2f2f;                /* a destructive BUTTON's fill */
    --danger-bg-hover: #953636;          /* ...and its hover, on the surfaces that take one */
    --danger-bg-lift: #9c3b3b;           /* the light end of the armed plate's top->bottom interior */
    --danger-edge: #a13a3a;              /* a 1px hairline, enough on a small dense button */
    --danger-stroke: #e06a6a;            /* the 2px stroke a PLATE-sized control needs - the gold plate's own idiom, in red */
    --danger-chip: #b71c1c;              /* .chip-danger: the game rail's resign/cancel chip, brighter by design */
    --danger-tint: rgba(220,70,70,.28);  /* a danger BADGE's wash */
    --danger-ink: #ffe4e4;               /* a label on a danger fill */
    --danger-ink-soft: #ffb3b3;          /* a label on a danger wash */

    /* #402 — THE BOARD PALETTE, as this stylesheet's ONE copy of it. CSS cannot ask a .js file for a
       colour, so these six are literals; what they are NOT is six literals scattered through the
       rules that use them. board.js's COLORS (rendered) / SWATCHES (declared) is the source and
       Caracri.Web.Tests/RulesSvgPaletteTests pins every line below to it BY KEY on every run.
       The rule this block exists to keep straight: AN ELEMENT THAT STANDS FOR A BOARD ENTITY TAKES
       THE BOARD PALETTE; CHROME TAKES THE BRAND PALETTE (--gold). The two clock seats label the two
       players, so they are board entities and take --piece-*; .clock.running and .clock.mine are
       STATE, so they keep --gold. Borrowing --gold for the yellow SEAT is what made that seat's
       running state invisible — same value before and after (cara-product, 2026-09-02). */
    --piece-blue: #4a90e2;    /* COLORS.odd[0]   — First's Sun, bright core */
    --piece-yellow: #f6d63f;  /* COLORS.even[0]  — Second's Sun, bright core */
    --cell-first: #1f3959;    /* SWATCHES.oddCell  — a cell First may create on */
    --cell-second: #5a5122;   /* SWATCHES.evenCell — a cell Second may create on */
    --cell-both: #1b4e32;     /* SWATCHES.bothCell — a shared cell (#97) */
    --cell-spent: #090c13;    /* SWATCHES.dead     — a spent cell */
    /* The same two piece colours as bare rgb triples, for the rules that need them inside rgba().
       NOT a second copy: RulesSvgPaletteTests requires each to be the decimal reading of the hex
       above it, so the pair cannot drift the way #402's two populations did. */
    --piece-blue-rgb: 74,144,226;
    --piece-yellow-rgb: 246,214,63;
}

/* == inline-chrome (generated) - do not edit by hand: Application/gen-inline-chrome.ps1 == */
/* Small always-rendered chrome (#227): each file here used to cost a cold load one request.
   Sources live in wwwroot/img/; the gate that keeps these fresh is InlineChromeAssetTests. */
:root {
    --inline-pill: url("data:image/png;base64,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"); /* /img/pill.png */
    --inline-pill-primary: url("data:image/png;base64,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"); /* /img/pill-primary.png */
    --inline-tile: url("data:image/png;base64,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"); /* /img/tile.png */
    --inline-tile-primary: url("data:image/png;base64,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"); /* /img/tile-primary.png */
    --inline-waitbar: url("data:image/png;base64,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"); /* /img/waitbar.png */
    --inline-waitbar-can: url("data:image/png;base64,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"); /* /img/waitbar-can.png */
}
/* == end inline-chrome (generated) == */

html, body {
    min-height: 100%;
    margin: 0;
    font-family: "Segoe UI", system-ui, -apple-system, Roboto, Helvetica, Arial, sans-serif;
    color: var(--ink);
    background: #141822 url('/img/bg-app.jpg') center top / cover fixed no-repeat;
}

.app { max-width: 1040px; margin: 0 auto; padding: 1rem 1.25rem 2rem; }

/* --- top nav as a graphical bar --- */
.topnav {
    display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
    padding: 0.4rem 0 1rem; margin-bottom: 1rem; border: none;
}
.topnav .brand { padding: 0; display: inline-flex; align-items: center; gap: 0.55rem; text-decoration: none; }
.topnav .brand .brand-mark { height: 42px; width: 42px; object-fit: contain; display: block; border-radius: 9px; filter: drop-shadow(0 2px 6px rgba(0,0,0,.5)); }
.nav-links { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; }

/* #380: the pill art is ONE 252x92 PNG whose rounded cap is drawn 17px inside its own edge with a
   ~24px radius. Painted `100% 100%` it was scaled independently on each axis, so the cap was never a
   shape - it was a function of the label. A .navpill's box is 57.6px tall in every locale (#399: a
   57.6px BORDER box, stated; it read `min-height: 40px` on an unstated content box until #399, which
   is the same number and was only ever the same number for an <a>), at which the art draws its cap at its own aspect
   only where the box is 252 x 57.6/92 = 157.8px wide. Past that the horizontal scale outran the
   vertical one, the cap curve elongated inward and the drawn gold stroke crossed a text box whose
   0.95rem padding had not moved: ru puzzles.publish, ja home.playPrivate, ru/he home.playLocal, and
   the widest of them by 12.6px. Nothing about the BOX measured wrong - every label had its correct
   padding and none spilled its column - which is why every layout gate in the suite was green while
   this shipped in all six locales.
   The 9-slice is what the shape is for and .tile.ai-tile below is already one (#257), so this is
   that idiom, with that idiom's exact value: `50%` top and bottom means the vertical slices are
   stretched over the whole box height, which is the mapping `100% 100%` already did at ANY height -
   the axis that was never wrong is left alone - while the caps render at a FIXED 20px however wide
   the pill gets and only the straight middle stretches. 20px rather than the 28.8px its own aspect
   would give is the same call #257 made on a 56px tile: the drawn cap then sits 10.0px in, clear of
   the 15.2px padding by the ~2px the gold stroke itself needs, and the two 9-sliced plates in this
   stylesheet draw the same end. `border: 0 solid` on purpose, as there: border-image paints in
   the border-image-width area regardless of border-width, and a real border would move the content
   box. Held on both axes by Application\test-plate-label-fit.ps1. */
.navpill {
    /* #399: the box `min-height` is measured against, said out loud - and, unlike #389's button
       rule, the box HEIGHT chosen, because this rule ships on both element types today and either
       answer moves chrome a user is looking at. The UA stylesheet gives a <button> a border box and
       leaves an <a> on the content box, so `min-height: 40px` with no `box-sizing` handed
       `a.navpill` 40px of CONTENT plus 2 x 0.55rem = 57.6px and `button.navpill` 40.0px - 17.6px
       apart in the SAME row (BoardProfile.razor:69 is a button between the two links on :74/:75,
       and PuzzleSolving pairs them the same way).
       The height is 57.6px, i.e. the box the <a> half already draws, on four measured grounds:
       * it is the box every derivation in this stylesheet is written against. #380's 157.8px aspect
         crossover, its 20px cap (drawn 10.0px in, clear of the 15.2px inline padding) and #385's
         "7px of clearance" are all readings of the 57.6px box, so "re-derive the clearance the art
         gives at the chosen height" comes out unchanged rather than needing the art re-cut;
       * clearance, which is what the fit probe judges. The plate's drawn top edge sits at 0.1848 of
         the box (art rows 17..75 of 92) and a nowrap label is centred in it, so the clearance is
         0.3152H - textH/2. Measured, this pill lays its label out at textH 20.0px in EVERY shipped
         locale, so 57.6px gives 8.2px (7.2px in ja, whose run sits ~1px lower), 44px would give
         3.9/2.9px and 40px 2.6/1.6px - and 1.6px is inside the ~2px gold stroke itself. The short
         pill is not a one-line change, it is a re-tune of the art; this one is neither;
       * it moves the half that is wrong. The 26 <a> sites keep the exact pixel they ship today and
         the 26 <button> sites grow 40 -> 57.6px, so each stops being the odd control in its own row;
       * 40px is under the 44px touch target the rest of this chrome clears.
       Written as a border box rather than as `content-box` + 40px because border-box is the model
       #389 put the button rule on and the one this family should be read in - and the two are the
       same function of the content (max(M, c) + 2p either way), so no <a> moves by a pixel.
       Held by Application\test-plate-label-fit.ps1, which pairs this rule's element types the way
       #389's does the button rule's: drop the line below and the <a> goes back to 75.2px. The rest
       of the --inline-* family (.chip, .tile, .menu-tile) is the same question and is deliberately
       NOT answered here - its heights want deciding per family, which is its own issue. */
    box-sizing: border-box;
    display: inline-flex; align-items: center; gap: 0.4rem; text-align: center;
    padding: 0.55rem 0.95rem; min-height: 57.6px; color: var(--ink); text-decoration: none;
    background: none; border: 0 solid transparent;
    /* #385: 48.91%, not the 50% this shipped with. 45/92 is the top slice's own share of the
       image, so every image row lands exactly where `100% 100%` put it; 50% stretches the two
       halves a hair further and shrinks the drawn band by ~0.8% of the box. It cost this pill
       nothing (7px of clearance) but the rest of the class sits at ~2px, so all eight plates in
       this stylesheet now use the one derivation. */
    border-image: var(--inline-pill) fill 45 46 / 48.91% 20px / 0 stretch;
    /* #401: font-family stated for the same reason box-sizing is, one tier down. The UA stylesheet
       gives a form control its own family, so this pill's 26 <button> sites lettered their labels
       in Arial (a 16.0px line box) beside the 26 <a> ones in the page's Segoe UI (20.0px), in the
       same rows, off one rule that names the size and the weight and not the family. The 57.6px
       floor absorbed it - the label asks for less than the floor on either element - which is
       exactly why every box reading was green on a divergence a reader could see. Nothing moves
       here: the box is the floor either way and the clearance goes 10.2 -> 7.2px, i.e. to the <a>
       half's own number, the one #399 derived. text-align above is the same omission and the same
       one line (the UA centres a <button>'s text and leaves an <a> on the inherited value); this
       pill is nowrap inside a flex box, so it changes nothing drawn today. */
    font-weight: 600; font-size: 0.92rem; font-family: inherit; white-space: nowrap;
    transition: transform 0.08s;
}
.navpill:hover { filter: brightness(1.14); }
.navpill:active { transform: translateY(1px); }
.navpill.primary { background: none; border-image-source: var(--inline-pill-primary); }
.navpill img { width: 18px; height: 18px; }

.badge { display: inline-flex; align-items: center; padding: 0.35rem 0.7rem; border-radius: 999px; font-size: 0.78rem; font-weight: 700; white-space: nowrap; flex: 0 0 auto; }  /* #194: a badge label (verified/unverified) must never break mid-word when a long email squeezes its row */
.badge.guest { background: rgba(255,255,255,.12); color: var(--ink); }
.badge.premium { background: linear-gradient(#e5c57a, #c9a24b); color: #3a2d06; }
/* #672 piece 5 (issue 696): the Premium badge during the final week before the entitlement ends.
   An INSET ring only - it spends no width, no height, no padding and no font size, so a badge that
   fits the userbar row in every locale today fits it unchanged in this state (class C18), and the
   label keeps its own #3a2d06-on-gold contrast because nothing under the text moves. Inset rather
   than an outline on purpose: an outer ring at this size reads as keyboard focus. */
.badge.premium.renew-due { box-shadow: inset 0 0 0 2px #6b5210; }
.userbar { display: inline-flex; align-items: center; gap: .5rem; }
.userbar-id { display: inline-flex; align-items: center; gap: .4rem; text-decoration: none; color: var(--ink); }
.userbar-icon { width: 24px; height: 24px; border-radius: 999px; }
/* #75: the userbar can carry the #61 initials disc — same look, bar-sized. */
.userbar-icon.initials-avatar { width: 24px; height: 24px; font-size: .68rem; }
/* #82: rename a stored local game right on its replay header. */
.local-rename-btn { margin-inline-start: .4rem; }
.local-rename-row { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.local-rename-input {
    flex: 1 1 160px; min-width: 120px; max-width: 320px; padding: .35rem .6rem;
    border-radius: 8px; border: 1px solid rgba(255,255,255,.25);
    background: rgba(255,255,255,.06); color: var(--ink);
}
.userbar-name { font-weight: 700; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- everything on the dark shell now (Phase 2.2) --- */
main { display: block; color: var(--ink); }
main h1, main h2, main h3 { color: var(--ink); }
main h1:focus { outline: none; }   /* programmatic focus ring on heading */
/* #647 (feedback c85ac959): the anchor colour, said ONCE and unscoped. Until now this sheet stated
   it only as container RESTATEMENTS - `main a` right here, `.about-legal a` down in the About
   dialog - and an anchor outside every one of those containers fell through to the user agent's
   own `a:link` blue, #0000EE, which is 1.80:1 on the .modal-card and unreadable.
   `color` inherits, so `html, body`'s --ink reaches any element this sheet forgets - EXCEPT an
   anchor, because the UA colours that one by name and breaks the inheritance chain. An anchor is
   therefore the one element here that has to be coloured by name too, and #10 renders
   <AppDialogs /> at the app ROOT, outside <main>, so `main a` was never going to be the rule that
   did it: it took a second report on the same dialog to find the third link.
   Contrast, WCAG 2.1 relative luminance, against the surfaces an anchor actually sits on:
   #86b7ff on the .modal-card #161d29 = 8.24:1; on the page background #141822 = 8.64:1; on the
   heaviest white wash a link sits over, rgba(255,255,255,.14) over the page (#353841) = 5.70:1.
   All well past the 4.5:1 body-text floor, so the established link colour is KEPT, not replaced.
   Author origin outranks UA origin whatever the specificity, so this one rule also retires the
   UA's #551A8B `:visited` purple - no `:visited` rule is needed and none is written.
   The state rule is wrapped in :where() so it stays at (0,0,1), the base rule's own weight. A
   plain `a:hover` would be (0,1,1) and would out-rank every per-family link colour this sheet
   sets from a CLASS (.navpill, .chip, .search-indicator, a.rc-click, a.rn-contributor ...),
   repainting on hover a dozen links that are correct today. This rule is the FLOOR nothing may
   fall through, not an opinion: every named family still wins, at rest and on hover.
   Guarded two ways: CssClassScopeReachTests' element pass (no anchor anywhere may be left to the
   UA) and AboutDialogLinkColourTests (this dialog's three links, by name). */
a { color: #86b7ff; }
a:where(:hover, :focus-visible, :active) { color: #a9ccff; }

/* ad slot styled for the dark shell */
.ad-slot { display: flex; align-items: center; gap: 0.5rem; margin-top: 1rem; padding: 0.5rem 0.9rem;
    background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.08); border-radius: 10px;
    font-size: 0.82rem; color: var(--ink-dim); max-width: max-content; }
.ad-slot .ad-label { font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase; opacity: 0.7; }
.ad-slot a { color: var(--gold); text-decoration: none; }
.ad-slot a:hover { text-decoration: underline; }

/* issue 476: `muted` is this stylesheet's word for secondary text, and until now it was carried
   by three CONTAINER-scoped rules (.stats-card, .admin-chat, .theater) against 74 call sites.
   Outside those three boxes class="muted" resolved to full --ink: same markup, same class, two
   different emphases, and the loud one was the retention nudge on /puzzles. A utility class is a
   promise about every element that carries it, so the promise is kept HERE, once, and the scoped
   rules keep only what they ADD beyond it (see .admin-chat .muted's margin).
   Caracri.Web.Tests/CssClassScopeReachTests holds the class, not just this selector. */
.muted { color: var(--ink-dim); }

/* --- Home menu tiles (text on image plates) --- */
.home-hero { padding-top: 0.5rem; }
/* #107 (report 7b4b78fb): compact vertical density — tighter tile padding, gaps and margins. */
.menu { display: flex; flex-direction: column; gap: 0.5rem; max-width: 460px; margin: 0.25rem auto 0.8rem; }  /* #311 */
/* #364: the plate class's worst case, because a menu tile is TWO stacked strings (title over
   description) before anything wraps - so the content the tile art has to contain is already two
   lines in English and reaches five in ru at 320px, where the drawn border cut 4.3px into the text.
   tile.png's drawn rect is ~81% of the image height (a gentler margin than the pill's 64%), but
   ~9.9% of a 124px box is 12px, and the padding was 8px.
   The min-height moves with the padding on purpose. The 1.25rem block inset is also the durable
   allowance for wrapped translations: the plate's curved top/bottom cap scales with the resulting
   box, so a long label keeps clearance instead of consuming the last pixels of the drawn border. */
.menu-tile {
    display: flex; align-items: center; gap: 1rem; text-decoration: none; color: var(--ink);
    padding: 1.25rem 1.2rem; min-height: 40px;
    /* #385: the tile art, 9-sliced. This is the plate whose cap moved furthest, because a menu tile
       is a full-width block: at 460px the 392px art was stretched 1.17x on x against 0.47x on y and
       the drawn end reached 5.5px past the title in ru. 18px is the cap tile.png draws at the
       un-wrapped 80px tile (40 * 80/172 = 18.6); a tile whose text wraps to 140px keeps the same
       end instead of growing one, which is the whole point, and the 1.2rem inline padding clears it
       by 6.4px even at that height (where the label's top row sits deepest in the curve). */
    background: none; border: 0 solid transparent;
    border-image: var(--inline-tile) fill 85 40 / 49.42% 18px / 0 stretch;
    transition: transform 0.1s;
}
.menu-tile:hover { filter: brightness(1.1); transform: translateY(-2px); }
.menu-tile.primary { border-image-source: var(--inline-tile-primary); }
.menu-tile.primary:hover { border-image-source: var(--inline-tile-primary); }
.menu-tile-compact { padding-block: .45rem; min-height: 34px; }
.menu-tile-compact .menu-icon { width: 28px; height: 28px; }
.menu-tile-compact .menu-title { font-size: 1.05rem; }
.menu-tile-compact .menu-sub { font-size: .82rem; }
.menu-tile .menu-lock { margin-inline-start: .2rem; }
.menu-icon { width: 40px; height: 40px; flex: 0 0 auto; filter: drop-shadow(0 2px 3px rgba(0,0,0,.4)); }
/* min-width:0 — a flex item's default min-width:auto refuses to shrink below the text's
   min-content, which blew the ja puzzles tile past 390px viewports (gate 20260731-135141). */
.menu-text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.menu-title { font-size: 1.2rem; font-weight: 800; letter-spacing: 0.3px; overflow-wrap: anywhere; }
.menu-sub { color: var(--ink-dim); font-size: 0.9rem; overflow-wrap: anywhere; }
/* #311: 460px, not the 620px this note used to carry. The guest note annotates the tile stack above
   it, so it belongs to the SAME column - and a cap is only a line-length choice while everything is
   flush left. Once the stack is centred, a wider cap centres a wider box, and the note's text began
   80px to the inline-start of the tiles it explains. Held with .menu and .cotw-card as one column by
   Application\test-shell-block-centring.ps1. */
.home-note { color: var(--ink-dim); max-width: 460px; margin-inline: auto; }
.home-note a { color: var(--gold); }
/* #281 Phase 3 (Q22): the Contributor-of-the-Week card under the menu tiles. Same 460px column as
   .menu / .stats-card; the avatar column is fixed and the TEXT column shrinks (min-width: 0), so a
   long unbroken name wraps inside the card instead of pushing past 360px (feedback f7d77400:
   constrain boxes, not strings). Meta fragments are nowrap pills; the rows wrap. */
/* #311: border-box, so the 460px this shares with .menu and .home-note is the width the READER
   sees. Content-box made it 460 + 2rem of padding + 2px of border = 494 outer, which was invisible
   while the stack was flush left (the extra fell off the inline-end, where nothing lined up with
   it) and became a 17px overhang at each end the moment the column centred. */
.cotw-card { display: flex; align-items: center; gap: .9rem; max-width: 460px; margin: 0 auto .8rem; box-sizing: border-box;
    padding: .7rem 1rem; border-radius: 14px; text-decoration: none; color: var(--ink);
    border: 1px solid rgba(201,162,75,.35); background: rgba(18,25,37,.5); transition: transform 0.1s; }
.cotw-card:hover { filter: brightness(1.1); transform: translateY(-2px); }
.cotw-card .icon-preview { flex: 0 0 auto; }
.cotw-text { display: flex; flex-direction: column; gap: .25rem; min-width: 0; }
.cotw-title { color: var(--gold); font-weight: 800; font-size: .85rem; letter-spacing: .3px;
    text-transform: uppercase; }
.cotw-name { font-weight: 700; font-size: 1.15rem; overflow-wrap: anywhere; }
.cotw-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem .8rem; font-size: .9rem; }
.cotw-badges { display: flex; flex-wrap: wrap; gap: .3rem; }
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ===================== Phase 2.2: dark form / panel / card system ===================== */

/* glass surfaces for forms and content groups */
.auth-form, .form, .panel {
    max-width: 460px; margin: 0.5rem auto 1rem;   /* #311 */
    padding: 1.5rem 1.6rem; border-radius: 16px;
    background: linear-gradient(180deg, rgba(52,64,82,.55), rgba(28,37,51,.62));
    border: 1px solid rgba(201,162,75,.30);
    box-shadow: 0 12px 30px rgba(0,0,0,.42), inset 0 1px 0 rgba(255,255,255,.06);
    -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
}
.panel-wide { max-width: 880px; }

/* fields + inputs */
.field { margin-bottom: 0.9rem; }
.field label { display: block; margin-bottom: 0.35rem; color: var(--ink-dim); font-weight: 600; font-size: 0.88rem; }
input, select, textarea {
    width: 100%; box-sizing: border-box; padding: 0.6rem 0.7rem;
    background: #1b2430; color: var(--ink); font-size: 0.95rem;
    border: 1px solid rgba(255,255,255,.15); border-radius: 9px;
}
input::placeholder { color: #6b7a8c; }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--gold); box-shadow: 0 0 0 2px rgba(201,162,75,.25); }
input[type=checkbox] { width: auto; }

/* buttons -> image plates (text on image) */
/* #364: 0.85rem, not the 0.6rem this rule carried, and the difference is a WRAPPED label. Same
   geometry as #306's board badge one control over: the pill art is stretched `100% 100%`, so its
   transparent margin is ~18.5% OF THE BOX HEIGHT however tall the box gets, and 0.6rem was a
   one-line padding. ru/ko "I have read the Terms and agree" already wrapped to two lines at 320-360px
   and the drawn border ran through the second one. The budget is TWO lines of the largest shipped
   type, held by Application\test-plate-label-fit.ps1; a third would need ~1.15rem. It costs almost
   nothing to look at: a <button> is border-box (UA stylesheet), so min-height 44px still decides a
   one-line button and it stays 44px - only a button that WRAPS grows. */
/* #385: and the OTHER axis of the same artwork, which is not latent - it ships today, in every
   locale. `background: <plate> 100% 100%` scales the PNG independently per axis, so the drawn
   rounded CAP is a fraction of the box: a wide button drew its cap far inside a padding tuned by
   eye, and the gold stroke ran through the first and last glyph. #257 fixed exactly one control
   this way (.tile.ai-tile) and #380 a second (.navpill); this is the same two-line change applied
   to the rest of the class, so every `--inline-*` plate in this stylesheet is now a 9-slice and
   none of them scales its chrome with its label. Both halves are held by
   Application\test-plate-label-fit.ps1, which now also asserts that a 9-sliced plate's drawn cap
   does not move when the box width does.

   THE TWO NUMBERS, and where each comes from:
   * `45 46` / `85 40` are properties of the ART, not of any control - the slice that contains the
     whole drawn cap. pill.png is 252x92 with a 17px transparent margin and a 24px corner radius,
     so its cap ends 41px in and 46 takes it plus 5px of straight side; tile.png is 392x172, margin
     17, radius 18, so its cap ends 35px in and 40 takes it plus 5. Re-cut the art and these move
     with it (Application\test-plate-label-fit.ps1 decodes the drawn edge out of the very bytes the
     browser paints with, so a re-cut re-derives the geometry instead of expiring a constant).
   * the border-image-WIDTHS are properties of the CONTROL. Vertically every plate uses
     slice/imageHeight - 45/92 = 48.91%, 85/172 = 49.42% - which draws the whole image at exactly
     the scale `100% 100%` drew it at, so the vertical band #306/#364 tuned every padding against
     does not move by a single pixel. (50%, which #257 and #380 used, is a hair off that and
     shrinks the band ~0.8% of the box - harmless on a 58px navpill with 7px of clearance, but
     four controls here sit at 2.1px.) Horizontally the cap is pinned in px, chosen per family as
     the art's own cap at that control's SHORTEST shipped box (46 * H/92), rounded down, and never
     wider than the control's own padding leaves clear of the ~2px gold stroke.
   `border: 0 solid` on purpose, as in #257: border-image paints in the border-image-width area
   regardless of border-width, and a real border would move the content box. Every rule that used
   to drop the plate by overriding `background` now also says `border-image: none` - background and
   border-image are different properties, and an override of one does not reset the other. */
main button, .btn, .provider,
.auth-form button, .form button {
    /* #389: the box `min-height` is measured against, said out loud, because the UA stylesheet does
       not say it the same way for every element this one rule styles. It gives a <button> a border
       box and leaves an <a> on the content box, so the identical declarations below used to hand an
       `a.btn` 44px of CONTENT plus 2 x 0.85rem of padding - a 71px plate beside a <button>'s 44px
       one, 27px taller for the same label. Nothing but a <button> carries these classes today,
       which is exactly why it had to be fixed here: a plate stretches to whatever box it is handed
       and the label stays inside it, so the first non-button adopter would have read as a styling
       choice and been re-tuned with a local override instead of traced back to this line.
       #401: and the FONT, which is the same class one tier down and the half #389 measured and
       left. This rule states the weight and the size and left font-family to the UA stylesheet,
       which gives a form control its own - so a <button> lettered its label in Arial (a 17.0px
       line box) and an <a> in the page's Segoe UI (20.0px), off one rule, one label and one size.
       Nothing about the box was wrong, so no clearance reading in the suite could see it: a plate
       stretches to whatever box it is handed and the label sits inside it.
       `font-family: inherit` is one line; the re-tune under it is the issue. Every <button> in the
       product gains 3px of line box, and what the plate art gives a label is 0.3152H - textH/2
       against a drawn band that starts at 0.1848 of the box (art rows 17..75 of 92) - so at the
       0.85rem padding-block this shipped with, a two-line label in the 250px game rail landed
       1.2px from the drawn stroke, inside the ~2px #364 bought for the whole family. The
       padding-block is what buys it back, because clearance rises 0.5 - 0.1848 per px of padding:
       0.9309rem is the least that holds two 20.0px lines at 2px, and 0.95rem is #364's own rule -
       the round number above the minimum - measuring 2.2px there and 5.9px on the single line
       every other button in the product lays out, which grows 44.2 -> 50.4px with it. min-height
       stays 44px: it is the touch target, and it no longer decides any labelled button.
       text-align is the same omission found again, by the reading #401 added rather than by a
       screenshot. The UA centres a <button>'s text and leaves an <a> on the inherited value, and
       `justify-content` hid it - that centres the anonymous flex item, not the lines inside a
       wrapped one. Held by Application\test-plate-label-fit.ps1, which now compares the two
       element types over the computed text properties as well as over the box.
       text-decoration is the third, and the one a reader would have named first: the UA underlines
       an <a> and not a <button>, so the same plate label came underlined or not depending on which
       element wore it. .navpill has said `text-decoration: none` since it was written, which is the
       evidence that this family means its labels unadorned; this rule never said it because no <a>
       had adopted it yet, which is #389's own reason and the reason it could not stay. */
    box-sizing: border-box;
    display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; text-align: center;
    width: auto; padding: 0.95rem 1.2rem; min-height: 44px; margin: 0.25rem 0;
    border: 0 solid transparent; cursor: pointer; color: var(--ink); text-decoration: none;
    font-weight: 700; font-size: 0.95rem; font-family: inherit;
    /* 18px, not the 20px .navpill and .tile.ai-tile wear. It was chosen against the 71px box #389
       removed, whose single line of text sat 13.6px down - 19% of the box, right where the drawn
       cap is still curving in - and where 20px reached that label to within 2.2px of the stroke.
       The shortest box the family lays out now is a 50.4px one and the tallest a 70.4px two-line
       one (#401 gave both element types the page font's 20.0px line and the 0.95rem of
       padding-block that holds two of them), so 18px has more room than it was cut for, and it is
       still well under the 25.2px the art draws at a 50.4px button. */
    background: none; border-image: var(--inline-pill) fill 45 46 / 48.91% 18px / 0 stretch;
    transition: transform 0.08s, filter 0.1s;
}
/* #676: ReportDialog lives outside main, so its four actions opt into the authored plate without
   widening either .cta or .sm onto already-correct controls. --ink resolves to #f2efe6, 14.71:1 against the feedback dialog's
   .modal-card #161d29 (WCAG AA body-text floor: 4.5:1). */
.report-button {
    box-sizing: border-box;
    display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; text-align: center;
    width: auto; padding: 0.95rem 1.2rem; min-height: 44px; margin: 0.25rem 0;
    border: 0 solid transparent; cursor: pointer; color: var(--ink); text-decoration: none;
    font-weight: 700; font-size: 0.95rem; font-family: inherit;
    background: none; border-image: var(--inline-pill) fill 45 46 / 48.91% 18px / 0 stretch;
    transition: transform 0.08s, filter 0.1s;
}
/* The dialog's named primary action pins its resolved ink directly, as #647 does for the named link. */
.report-button-primary { color: var(--ink); }
/* Hover = a subtle brightness lift, NEVER a different skin: an image swap that sticks after a touch
   tap reads as a third button state (repeated user reports) — brightness cannot be mistaken for one. */
main button:hover, .btn:hover, .provider:hover, .report-button:hover { filter: brightness(1.13); }
.report-button:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
main button:active, .report-button:active { transform: translateY(1px); }
main button:disabled, .report-button:disabled { filter: grayscale(.4) brightness(.75); cursor: default; }
main button[type=submit], .btn-primary { border-image-source: var(--inline-pill-primary); }
main button[type=submit]:hover { filter: brightness(1.08); }

/* chips + pickers -> plates (override the earlier light rules) */
/* #364: the padding-block lives HERE, next to the plate, because the plate is why it exists. The
   pill art's drawn rounded-rect is ~64% of the image height, so ~18.5% of the BOX is transparent
   margin above the border and a chip's label may only use [.185H, .826H]. At .5rem the 16px line box
   of a 32px chip cleared the drawn edge by 1.6px - inside the ~2px gold stroke itself, in every
   locale, at every viewport (Application\test-plate-label-fit.ps1 -Report). .55rem buys the 2px the
   stroke needs and costs 1.6px of chip height; .chip is `white-space: nowrap` (#200), so unlike the
   controls below there is no second line to budget for. */
/* #385: 16px caps. A .chip's box is 33.6px tall (.55rem of padding over a `line-height: 1` line),
   and 46 * 33.6/92 = 16.8 is the cap the art itself draws at that height - so 16 is the art's own
   end, not a smaller one, and it leaves the 1rem inline padding 6.5px clear of the gold stroke.
   The variants below swap the SOURCE only: their box is the same box, so their cap is too. */
.chip { background: none; color: var(--ink); border: 0 solid transparent;
    border-image: var(--inline-pill) fill 45 46 / 48.91% 16px / 0 stretch; padding-block: 0.55rem; }
.chip:hover { filter: brightness(1.13); }
.chip-primary { border-image-source: var(--inline-pill-primary); }
.chip-wait { border-image-source: var(--inline-pill); color: #ffcf8f; box-shadow: inset 0 0 0 2px rgba(230,120,20,.55); }
/* #385: 20px, the same cap .tile.ai-tile has worn since #257 - a .tile is a 56px box (40px of
   content plus .5rem of padding, content-box), the art draws a 28px cap at that height, and the
   .95rem inline padding clears a 20px one by 4.2px. The two tile variants are the same box. */
.tile { background: none; color: var(--ink); border: 0 solid transparent;
    border-image: var(--inline-pill) fill 45 46 / 48.91% 20px / 0 stretch; position: relative; }
.tile:hover { filter: brightness(1.13); }
/* `background: none` still spelled out on purpose: the legacy light rule
   `.tile.selected { background: #e8f5e9 }` has higher specificity than the `.tile` plate above, so
   without it the pale backdrop comes back under the 9-slice (it used to also reset
   size/position/repeat and draw the pill unstretched + repeating - clipped-label tiles). */
.tile.selected { background: none; border-image-source: var(--inline-pill-primary); color: var(--ink); }
/* #732: a classic board and a custom board are two shapes in ONE vs-AI picker. The primary pill
   image alone did not give the classic shape the direct gold selected indicator that the custom
   row carries, so at phone width it looked unselected beside Nova. Keep this scoped to board
   choices: player, side and strength tiles retain their existing primary-skin treatment. */
.tile.vsai-board-tile.selected { box-shadow: 0 0 0 2px rgba(201, 162, 75, .75) inset; }
/* #257 (report f7d77400, 360px viewport): the stretched pill art scales its rounded CAPS with the
   tile's width (art 252x92, caps ~46px = ~18% of the width on screen), so any label long enough
   outgrows a fixed padding and sits ON the drawn border — "Experimental" did, edge to edge. For
   the vs-AI tiles the art is a 9-slice instead: caps render at a FIXED 20px however wide the pill
   gets, the straight middle stretches, and the 1.5rem padding floor (24px > 20px cap) holds for a
   nickname of ANY length; one longer than its row wraps inside the pill (max-width + anywhere).
   `border: 0 solid` on purpose: border-image paints in the border-image-width area regardless of
   border-width, and a real border would move the content box. */
.tile.ai-tile { background: none; border: 0 solid transparent;
    border-image: var(--inline-pill) fill 45 46 / 48.91% 20px stretch;   /* #385: 45/92, see .navpill */
    max-width: 100%; padding-inline: 1.5rem; }
.tile.ai-tile.selected { background: none; border-image-source: var(--inline-pill-primary); }
.tile.locked { opacity: 0.62; padding-right: 2.05rem; }
.tile.locked::after { content: "🔒"; position: absolute; top: 50%; right: 0.95rem; transform: translateY(-50%); font-size: 0.72rem; }
.tile.locked.selected { opacity: 0.62; }
.lock-note { color: #ffcf8f; font-size: 0.88rem; margin: 0.35rem 0 0.5rem; }

/* provider (external sign-in) buttons keep a brand tint */
.external-signin { margin-top: 1rem; }
.divider { color: var(--ink-dim); font-size: 0.85rem; margin: 0.5rem 0; }
.provider.google { color: #ffe08a; }
.provider.apple { color: #e6ecf3; }
/* App-styled "Continue with Google" (incident #35 replaced Google's generic button): the app pill
   plate with the brand G mark, so it matches every other button instead of Google's white box. */
.provider-ico { width: 18px; height: 18px; flex: 0 0 auto; }
.provider.google .provider-ico { background: #fff; border-radius: 50%; padding: 2px; box-sizing: border-box; }
.more { color: var(--ink-dim); font-size: 0.85rem; margin-left: 0.5rem; }

/* card lists (replace Games / Friends tables) */
.card-list { display: flex; flex-direction: column; gap: 0.6rem; max-width: 880px; margin: 0.6rem auto 1rem; }  /* #311 */
.row-card {
    display: flex; align-items: center; gap: 0.9rem; text-decoration: none; color: var(--ink);
    padding: 0.75rem 1rem; border-radius: 12px;
    background: linear-gradient(180deg, rgba(52,64,82,.5), rgba(28,37,51,.55));
    border: 1px solid rgba(255,255,255,.10); transition: border-color 0.12s, transform 0.08s;
}
a.row-card:hover { border-color: rgba(201,162,75,.55); transform: translateY(-1px); }
.row-card .rc-icon { width: 34px; height: 34px; flex: 0 0 auto; opacity: 0.9; }
.row-card .rc-main { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.row-card .rc-title { font-weight: 700; overflow-wrap: anywhere; }   /* #275: a long custom-board name wraps inside the card at 360px instead of pushing past it */
.row-card .rc-board-slot { display: inline-flex; align-items: center; gap: .3rem; max-inline-size: 100%; vertical-align: middle; }
.row-card .rc-board-slot .rc-board { min-width: 0; overflow-wrap: anywhere; }
.row-card .side-marker { inline-size: 1.15rem; block-size: 1.15rem; flex: 0 0 1.15rem; border-radius: 4px; }
.row-card .rc-sub { color: var(--ink-dim); font-size: 0.85rem; }
.row-card .rc-spacer { flex: 1 1 auto; }
.row-card .rc-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; }
/* #306: same plate geometry as .board-topbar, same fix. These buttons sit in a wrapping flex row
   that squeezes them, so a localized action label ("game chodaehagi" in ko, ja's five-glyph invite)
   wraps at phone widths and crossed the pill's drawn border - the 0.32rem the .sm rule carries is a
   ONE-LINE padding. 0.9rem covered two lines of the longest string in every shipped locale; single
   line rows grew 34px -> ~44px, uniformly across Accept / Decline / Invite in the same row. Scoped
   to .rc-actions on purpose: every other plate-skinned control in the app has the same latent
   geometry, and pinning that whole class needs a reference sweep, not this rule (#364).
   #401: 1rem, re-derived against the 20.0px line box `font-family: inherit` now gives these
   labels - this override REPLACES the .sm padding-block, so neither the rule above nor the .sm
   floor reaches these three buttons and the number had to move here. At 0.9rem the ja invite's
   wrapped label sat 1.3px from the drawn stroke; 1rem measures 2.3px, and the single-line rows
   grow to 50.0px. */
.row-card .rc-actions button.sm { padding-block: 1rem; }
.status-pill { padding: 0.2rem 0.6rem; border-radius: 999px; font-size: 0.78rem; font-weight: 700;
    background: rgba(255,255,255,.10); color: var(--ink-dim); }
.status-pill.live { background: rgba(76,175,80,.22); color: #a5e0ae; }
.status-pill.done { background: rgba(255,255,255,.08); color: var(--ink-dim); }
/* #309: the row's right-hand element is not the thing that gives way. Without this, the same
   pressure that pushed a long opponent name under the pill also squeezed "Your move" from an 82x22
   pill into a two-line 52x40 one - so a row could stop overlapping by breaking its own pill instead
   of by cutting the name short. flex-shrink is the cause and the wrap was the symptom, so this says
   only that: a `white-space: nowrap` beside it measured as pure duplication (removing either one
   alone left test-row-name-fit.ps1 green). .rc-actions is deliberately NOT here - it is a wrapping
   button row and its second line is intended (#306). */
.row-card > .status-pill, .row-card > .rc-rating { flex: 0 0 auto; }
/* Issue 403, member 1 - #278/#393's rule, one family over. Below 360px the row's trailing element
   is RIGID (the line above is what makes it so, #309) while `.rc-main` is the only thing that
   yields, so the NAME inside it was the item that paid: measured at 320px, `.rc-main` 73px on
   /player and /games and 5-6 characters of "Ada Lovelace", while "Your turn" (74px) and
   "1500 -> 1512" (96px) rendered whole beside it. Same shape as the theater strip's - the identity
   is the yielding item and the metadata beside it is not - and identity outranks the metadata that
   describes it. 320px is IN SCOPE: four gate matrices in this repo already sweep it
   (test-row-name-fit.ps1, test-plate-label-fit.ps1, test-board-fold-layout.ps1,
   test-admin-layout.ps1), and #314's reporter was on a 283-320px board.
   So the ROW wraps, exactly as `.players-bar`'s card does: the lead mark and the name column keep
   the first line and the trailing element takes its own, where it renders at its natural size and
   presses on nothing. `.rc-main` needs a DEFINITE flex-basis for the line to break at all - its
   `auto` basis is the title's max-content, so a wrapping row would put it on a line of its own and
   orphan the lead mark above it, which a 12.5rem basis did on the first measured pass (the lead is
   34px + a 0.9rem gap, and 200px of basis beside it is 248.4 of a 246px card - 2.4px over). So the
   basis is a SHARE of the card, not a length: 60% leaves the lead its room at every width this
   block covers instead of at the one it was arithmetic for, and it is still far more than the
   trailing element can be added to. The spacer's job passes to the trailing element's own auto
   margin, which right-aligns it on whichever line it lands.
   Measured 320px, test-participant-name-fit.ps1: `.rc-main` 73 -> 198px on /player, 95 -> 198 on
   /games, 113 -> 198 on /live, and "Ada Lovelace" goes from 6 characters to all 12 on every one of
   them. At 360px and above the row is unchanged - it does not need this (7+ characters there
   already), and test-row-name-fit.ps1 (#309) pins the reported /games row exactly there. */
@media (max-width: 359px) {
    .row-card { flex-wrap: wrap; row-gap: .35rem; }
    .row-card .rc-main { flex: 1 1 60%; }
    .row-card .rc-spacer { display: none; }
    .row-card > .status-pill, .row-card > .rc-rating, .row-card > .rc-actions { margin-inline-start: auto; }
}
.empty-note { color: var(--ink-dim); }

/* pickers (Queue / setup) */
.picker-label { display: block; font-weight: 600; color: var(--ink-dim); margin: 0.75rem 0 0.35rem; }
/* Issue 532: `flex-wrap: wrap` is not decoration here, it is the other half of `.chip`'s own rule.
   #200 settled that a pill button never wraps its label and THE ROW WRAPS INSTEAD - which is why
   `.board-actions`, `.tile-row`, `.pause-bar`, `.rematch-row` and a dozen more all carry it.
   `.toggle-row` was the one chip row that did not, so the game view's controls row laid its chips
   out on one unwrappable line: at 1280x900 a spectator's "Stream view" chip started 155px into a
   278px row and painted 38px past its end in English, 69px in Russian, on 48 gate frames. Wrapping
   is the fix by construction - no viewport threshold to pick and no locale to stay ahead of. */
.toggle-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin: 0.75rem 0; color: var(--ink-dim); }
.toggle-row input { width: auto; }
.add-friend-row { display: flex; gap: 0.5rem; align-items: center; }
.add-friend-row input { flex: 1; }
/* #193-followup (Gate-2 review): in the narrow landscape side-column the long localized "Update
   stored" button ("Обновить сохранённую") crushed the name input to ~27px in this nowrap row. Let the
   hot-seat store row wrap and keep the input usable — the button drops below the input when the column
   is too narrow. Scoped to .store-local so the Friends add-row (wider page) is unaffected. */
.store-local .add-friend-row { flex-wrap: wrap; }
.store-local .add-friend-row input { min-width: 140px; }
/* Issue 533: the stored-game confirmation now sits BETWEEN the label and the row (HotSeatPanel.razor
   says why - a click scrolls its target to the frame's bottom edge, so anything under the button is
   off screen by construction on a short window). A default <p> band there would push the row it
   precedes down by a whole line for nothing; this is a status line for the block, not a paragraph. */
.store-local .stored-note { margin: .1rem 0 .45rem; }
/* #385: a small button is a 34px box, at which the art draws a 46 * 34/92 = 17px cap - and 14 is
   the round number under it that also keeps BOTH caps inside the narrowest .sm button the product
   lays out (the icon-only ✕ / share / QR buttons are ~37px wide, and a 9-slice whose two caps do
   not fit its box has all four widths reduced by the same factor, which would squash the plate
   vertically too). It clears the 0.85rem inline padding by 5.3px.
   #401: the box is 40px now, and the FLOOR is what moved rather than the padding. This label's
   line box grew 15.0 -> 18.0px when the rule above stated its font-family, and 18.0px plus 2 x
   0.32rem is 28.2px - under the floor, so the padding-block does not decide this box and raising
   it would have measured nothing. 34px left ja's run 0.2px from the drawn stroke vertically and
   its cap 1.8px from the label horizontally (the drawn cap is sampled at the rows the label
   occupies, so a label that grows towards the curve loses on both axes at once); 39.7px is the
   least that holds the 2px, and 40px is the round number above it, measuring 2.1px and 4.7px.
   The 14px cap stands as cut: the art draws a 46 * 40/92 = 20px cap at this height, but both caps
   must still fit the ~37px icon-only buttons, and 2 x 20 does not. */
main button.sm, .btn.sm, .report-button.sm { min-height: 40px; padding: 0.32rem 0.85rem; font-size: 0.85rem;
    border-image-width: 48.91% 14px; }

/* content page tweaks on dark */
.scaffold-note { color: var(--ink-dim); font-size: 0.9rem; }
.game-page .lede { color: var(--ink-dim); }
.picker-label { color: var(--ink-dim); }
.game-chat { border: 1px solid rgba(255,255,255,.12); border-radius: 12px;
    background: linear-gradient(180deg, rgba(52,64,82,.4), rgba(28,37,51,.5)); }
.game-chat h2 { color: var(--ink); }
.chat-line { color: var(--ink); }
code { color: #ffcf8f; background: rgba(255,255,255,.06); padding: 0.05rem 0.3rem; border-radius: 4px; }

/* Resign chip */
/* #385: `border-image: none` beside `background-image: none`, and every other rule below that drops
   the plate says both. The plate moved from `background` to `border-image`, and overriding one
   property does not reset the other - without this the pill would paint straight over the red. */
.chip-danger { background: var(--danger-chip); background-image: none; border-image: none; color: #fff; box-shadow: none; border: none; }
.chip-danger:hover { filter: brightness(1.15); }
/* #157: arm-then-confirm — the second-tap ("Confirm?") state of a destructive control. A red ring
   plus a steady pulse says "tap again to confirm"; deliberately NO background swap so a touch tap
   never leaves it stuck looking armed (check-css touch-stuck class / reports b2a8ae03). */
.armed { box-shadow: 0 0 0 2px #ff6e6e, 0 0 11px rgba(255, 110, 110, 0.55); animation: armedPulse 1s ease-in-out infinite; }
@keyframes armedPulse { 50% { filter: brightness(1.2); } }
/* #483: `danger` OUTSIDE the admin console. `.admin-btn.danger` and `.admin-badge.danger` were the
   only rules in this sheet that carried the class, so the four arm-then-confirm sites that spell
   the ARMED state as `danger` on a plate control - BoardProfile's remove pill, PuzzleView's publish
   and remove pills, Player's remove-friend [x] - armed a class with no rule at all. The whole
   safety of arm-then-confirm rests on the second state being unmistakable, and outside /admin the
   only thing that changed was the label.

   Scoped on purpose, not bare. A bare `.danger` is the shape #476 gave `.muted`, and it is the
   wrong shape here: it would repaint every element that happens to carry the word (`.admin-badge`
   first) and invent design for surfaces nobody looked at. What keeps the admin console and the
   rest of the product from drifting apart again is not one selector but the --danger-* palette
   above, which both sides read. A family that arms `danger` and has no rule here reddens
   Caracri.Web.Tests/CssArmedStateRenderTests, which derives its population from the markup rather
   than from a list.

   Drawn in a ::before rather than as a background because the BOX MUST NOT MOVE: an absolutely
   positioned pseudo has no layout at all, so padding, border-image sizing and font-size are
   untouched (measured at 360px: 57.594px tall armed and unarmed alike), and unlike a clip-path
   nothing it does can cut a label. Everything it is inset by is read off the art, which is why one
   pair of rules serves a 57.6px navpill and a 40px `main button.sm` alike:

     the art is 252x92 and draws its plate in rows 17..75 and columns 17..235, with a 24px corner;
     the 9-slice (`fill 45 46 / 48.91% <cap>px`) scales it by 48.91%H/45 down and <cap>/46 across.

   Down, that is the same fraction at every plate height, so the band is a pair of percentages:
   17/45 x 48.91% = 18.48% of the box above it, 16/45 x 48.91% = 17.39% below - the art's two
   transparent margins are 17 and 16 rows, not the same number - leaving 64.13%, which puts the
   drawn edge exactly where the gold one is and keeps #399's 8.2px of label clearance. Across, the
   scale is the family's own cap, so `inset-inline` and the corner are stated per family below.

   `border-image: none` beside `background: none` for #385's reason - overriding one does not reset
   the other, and the gold cap would otherwise show from under the red at the ends. No :hover rule
   on purpose: .navpill:hover and `main button:hover` already carry the brightness lift, and the
   touch guard at the end of this sheet already neutralises them (#410). */
.navpill.danger, .icon-btn.danger { position: relative; z-index: 0; background: none; background-image: none; border-image: none; color: var(--danger-ink); }
.navpill.danger::before, .icon-btn.danger::before {
    content: ""; position: absolute; inset-block: 18.48% 17.39%; z-index: -1; box-sizing: border-box;
    background: linear-gradient(180deg, var(--danger-bg-lift), var(--danger-bg));
    border: 2px solid var(--danger-stroke); box-shadow: 0 2px 7px rgba(0,0,0,.45);
}
/* The numbers the families do NOT share. The inline inset is 17px of art scaled by that family's
   own cap - 20/46 on the navpill, 14/46 on `main button.sm`, which is what Player's armed [x] is.
   Without it the red band runs the full box width and the pill's drawn left edge jumps ~7px outward
   on the tap that ARMS it, which reads as the button growing rather than as it changing state.
   Logical properties, so [dir="rtl"] is the same number on the other side.

   The corner is per family too, and its VERTICAL half is the one number here that is measured
   rather than derived. The art's nominal 24px corner predicts 24 x 48.91%H/45 = 40.67% of the band
   at every plate height, and that is what this rule shipped with first. A pixel scan of what the
   gold plate actually DRAWS says otherwise, because a 9-slice scales a corner anisotropically and
   the drawn stroke is not the geometric outline: traced at a true 360px viewport, dsf 4, by the
   outermost stroke pixel of each row, the gold navpill's cap curves for 15.00px of its 37.00px band
   and leaves a 7.25px flat vertical edge, while 40.67% curved for only 11.75px and left 13.75px
   flat - so the armed pill read as a rounded rect in a row of capsules. `50%` is a true capsule
   AND the ceiling (a larger radius is clamped back to half the box), and it measures 14.25px of
   curve over an 8.25px flat run: 0.75px and 1.00px from the gold it sits beside, i.e. inside the
   antialias band at 1x. `main button.sm` keeps the derived 40.67%: its own scan is 7.75px over a
   9.50px flat run against that family's gold at 8.50px / 8.75px, already within 0.75px, and it is
   a different box - the two are not one number pretending to be one shape. Re-measure before
   changing either, the same way: render the armed pill beside its gold twin on a flat chroma
   backdrop, trace the outermost pixel of each row that matches the plate's STROKE colour (so the
   drop shadow and the backdrop cannot move the edge), and compare the curve and the flat run. An
   eyeball on a 58px control is not a reading - two of them disagreed about this corner. */
.navpill.danger::before  { inset-inline: 7.39px; border-radius: 10.44px / 50%; }
.icon-btn.danger::before { inset-inline: 5.17px; border-radius: 7.30px / 40.67%; }
/* #163: pinned "reconnecting" pill — the game page must never look quietly alive on a dead hub.
   Fixed under the topnav, amber, gently pulsing (filter only, no background swap on any state). */
.conn-lost {
    position: fixed; top: 64px; left: 50%; transform: translateX(-50%); z-index: 60;
    background: #7a5a12; color: #ffe9b8; border: 1px solid rgba(255, 214, 130, .65);
    border-radius: 999px; padding: 0.3rem 0.9rem; font-size: 0.9rem; font-weight: 600;
    box-shadow: 0 2px 10px rgba(0, 0, 0, .45); animation: connLostPulse 1.4s ease-in-out infinite;
}
@keyframes connLostPulse { 50% { filter: brightness(1.25); } }

/* #277: floating "new message" pill — shown while an opponent's chat line waits off-screen; the
   dir-* class is the PHYSICAL direction the chat panel lies in (chat-alert.js measures the real
   viewport, so physical left/right here is already RTL-correct — do not swap to logical). Fixed
   like .conn-lost, same z-band. The pill must stay INSIDE a 360px viewport with margins intact
   (feedback f7d77400: constrain the box, not the string), hence max-width + ellipsis. dir-hidden
   is focus mode (panel display:none): same bottom-centre spot the panel returns to on exit. */
.chat-alert {
    position: fixed; z-index: 60; display: inline-flex; align-items: center; gap: .45rem;
    max-width: calc(100vw - 2rem); box-sizing: border-box; cursor: pointer;
    padding: .45rem 1rem; border-radius: 999px; font-size: .9rem; font-weight: 600;
    background: linear-gradient(180deg, rgba(52,64,82,.95), rgba(28,37,51,.97));
    border: 1px solid rgba(201,162,75,.55); color: var(--ink);
    box-shadow: 0 4px 18px rgba(0,0,0,.5); animation: chatAlertPulse 1.4s ease-in-out infinite;
}
.chat-alert .chat-alert-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-alert .chat-alert-arrow { flex: 0 0 auto; color: #e9c877; }
.chat-alert.dir-down, .chat-alert.dir-hidden {
    bottom: max(1rem, env(safe-area-inset-bottom)); left: 50%; transform: translateX(-50%);
}
.chat-alert.dir-up { top: 64px; left: 50%; transform: translateX(-50%); }
.chat-alert.dir-left { left: .75rem; top: 50%; transform: translateY(-50%); }
.chat-alert.dir-right { right: .75rem; top: 50%; transform: translateY(-50%); }
@keyframes chatAlertPulse { 50% { filter: brightness(1.2); } }

/* #281 Phase 2 (Q25): the gratitude bottom sheet — non-blocking, tappable (so NO pointer-events
   opt-out, unlike .toast-note), bottom-anchored like .chat-alert with the same safe-area floor
   and the same f7d77400 width bound. Below the toast band on purpose: a toast outranks thanks.
   The rise animation is opt-in via .gratitude-rise, applied only while the anim setting is on. */
.gratitude-sheet {
    position: fixed; left: 50%; transform: translateX(-50%);
    bottom: max(1rem, env(safe-area-inset-bottom)); z-index: 9000; cursor: pointer;
    max-width: min(480px, calc(100vw - 2rem)); box-sizing: border-box;
    padding: .8rem 1.1rem; border-radius: 14px; font-size: .95rem; line-height: 1.35;
    background: linear-gradient(180deg, rgba(52,64,82,.96), rgba(28,37,51,.97));
    border: 1px solid rgba(201,162,75,.55); color: var(--ink);
    box-shadow: 0 6px 22px rgba(0,0,0,.55);
}
.gratitude-sheet.gratitude-rise { animation: gratitudeRise .3s ease-out; }
@keyframes gratitudeRise { from { transform: translate(-50%, 16px); opacity: 0; } to { transform: translate(-50%, 0); opacity: 1; } }

/* Full-screen blocking notice (ToastOverlay): dark backdrop, centred card, captures all clicks */
.toast-overlay {
    position: fixed;
    inset: 0;
    z-index: 10000;
    background: rgba(0, 0, 0, 0.72);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
    animation: toast-fade 0.15s ease-out;
}
/* #136/#137: non-blocking confirmation chip — no backdrop, ignores the pointer, fades out. */
.toast-note {
    position: fixed;
    left: 50%;
    bottom: 86px;
    transform: translateX(-50%);
    z-index: 10000;
    pointer-events: none;
    background: rgba(24, 32, 46, 0.96);
    color: #e8edf5;
    border: 1px solid rgba(201, 162, 75, 0.45);
    border-radius: 999px;
    padding: 0.55rem 1.1rem;
    font-size: 0.95rem;
    box-shadow: 0 6px 22px rgba(0, 0, 0, 0.5);
    opacity: 1;
    transition: opacity 0.4s ease;
}
.toast-note.leaving { opacity: 0; }

/* #138: colorblind-friendliness marker on the skin tile (icon + tooltip, not label text). */
.cb-badge { margin-inline-start: 6px; font-size: 0.9em; opacity: 0.85; }

/* #136: with a custom board, the rated picker is replaced by this one-line note. */
.custom-unrated-note { margin: 0.15rem 0 0.4rem; }

.toast-card {
    background: #1f2937;
    color: #fff;
    padding: 1.25rem 1.75rem;
    border-radius: 12px;
    font-size: 1.15rem;
    line-height: 1.4;
    max-width: 90vw;
    text-align: center;
    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.55);
}
@keyframes toast-fade { from { opacity: 0; } to { opacity: 1; } }

/* Invite QR code — white quiet-zone so it scans against the dark theme */
/* #152: searchable friend picker (modal). */
.friend-picker-search { width: 100%; }
.friend-picker-list { display: flex; flex-direction: column; gap: 4px; max-height: 46vh; overflow-y: auto; margin-top: 6px; }
.friend-picker-row { display: flex; align-items: center; gap: 0.6rem; width: 100%; text-align: start; padding: 8px 10px; border-radius: 10px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-image: none; cursor: pointer; color: inherit; }
/* No :hover BACKGROUND swap (touch devices leave it stuck - check-css C12). Hover brightens the
   border + the go-arrow only; the base row already carries its fill. */
.friend-picker-row:hover:not(:disabled) { border-color: rgba(255,255,255,0.28); }
.friend-picker-row:hover:not(:disabled) .friend-picker-go { opacity: 0.9; }
.friend-picker-row:disabled { opacity: 0.5; pointer-events: none; }
.friend-picker-name { flex: 1 1 auto; font-weight: 600; }
.friend-picker-go { opacity: 0.5; font-size: 0.85rem; }
/* #152: friend-invite waiting card (friend-request style, no link/QR). */
.friend-wait-who { display: flex; align-items: center; gap: 0.7rem; margin: 0.2rem 0 0.8rem; }
.friend-wait-name { margin: 0; font-weight: 600; }
.friend-wait-summary { margin: 0.15rem 0 0; font-size: 0.85rem; }
.friend-wait-declined { margin: 0.2rem 0 0.8rem; }
.friend-wait-spin { width: 16px; height: 16px; margin-inline-start: auto; border: 2px solid rgba(255,255,255,0.25); border-top-color: #3ba85e; border-radius: 50%; animation: friend-wait-rot 0.9s linear infinite; }
@keyframes friend-wait-rot { to { transform: rotate(360deg); } }
.invite-qr-wrap { margin-top: 0.4rem; }
.invite-qr { width: 180px; height: 180px; image-rendering: pixelated; background: #fff; padding: 10px; border-radius: 10px; }
.invite-qr-id { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.5rem; max-width: 200px; }
.invite-qr-id code { font-size: 0.72rem; line-height: 1.25; word-break: break-all; opacity: 0.85; }
/* Friend code (#49): your shareable short numeric id, shown on the profile. */
.friend-code-row { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem; flex-wrap: wrap; }
.friend-code-row .fc-label { color: var(--ink-dim); }
.friend-code-row .fc-code { display: inline-flex; align-items: center; gap: 0.4rem; }
.friend-code-row .fc-value { font-size: 1.05rem; font-weight: 700; letter-spacing: 0.06em; }
/* #61: default avatar = an initials disc when no icon is set. Same box as .icon-preview. */
.initials-avatar { display: inline-flex; align-items: center; justify-content: center;
    width: 48px; height: 48px; border-radius: 50%; flex: 0 0 auto; font-weight: 700; font-size: 1.05rem;
    color: #eaf1ff; background: radial-gradient(circle at 35% 30%, #4a6da8, #24406e 70%); }
/* #62: account deletion — a clearly-dangerous, single-purpose button. */
.delete-account-btn { color: #ff9a9a; border-color: rgba(255, 110, 110, 0.45); }
/* #63: profile icon-button links (My games / Friends). */
.profile-links { display: flex; gap: 0.6rem; margin-top: 0.7rem; flex-wrap: wrap; }
.profile-links .icon-link { display: inline-flex; align-items: center; gap: 0.45rem; text-decoration: none; }
.profile-links .icon-link img { width: 20px; height: 20px; }
.profile-social-editor { border: 0; padding: 0; margin: 1rem 0 0; min-width: 0; }
.profile-social-editor legend { font-weight: 700; margin-bottom: .6rem; }
.social-edit-row { display: grid; grid-template-columns: minmax(7rem, .75fr) minmax(0, 1.6fr) auto; gap: .45rem; margin-bottom: .55rem; align-items: center; }
.social-edit-row select, .social-edit-row input { min-width: 0; width: 100%; }
.social-remove { min-width: 2.25rem; padding-inline: .55rem; }
.social-add { margin-top: .2rem; }
.social-link-list { display: grid; gap: .5rem; }
.social-link { display: flex; align-items: center; gap: .65rem; min-width: 0; padding: .65rem .75rem; border: 1px solid rgba(255,255,255,.16); border-radius: 10px; color: var(--ink); text-decoration: none; background: rgba(255,255,255,.04); }
.social-link:hover, .social-link:focus-visible { border-color: var(--gold); color: var(--gold); }
.social-icon { display: inline-grid; place-items: center; flex: 0 0 2.25rem; min-height: 2rem; padding: 0 .25rem; border-radius: 8px; background: rgba(255,255,255,.09); font-weight: 800; font-size: .82rem; }
.external-mark { margin-inline-start: auto; }
.social-untrusted { margin: .65rem 0 0; font-size: .82rem; }
@media (max-width: 420px) { .social-edit-row { grid-template-columns: 1fr auto; } .social-edit-row select { grid-column: 1 / -1; } }
/* #64: the game share button sits at the END of the status line (RTL-safe via margin-inline-start). */
.game-status { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.game-status .share-inline { margin-inline-start: auto; }
/* ── Language switcher (globe menu in the top nav) ────────────────────────── */
.lang-switch { position: relative; display: inline-flex; }
.lang-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 40px; height: 40px; padding: 0;
    background: transparent; border-image: none; border: 1px solid rgba(255,255,255,0.15);
    border-radius: 10px; color: #cfd6e4; cursor: pointer;
}
.lang-btn:hover { background: rgba(255,255,255,0.07); color: #eef2f8; }
.lang-menu {
    position: absolute; top: calc(100% + 6px); inset-inline-start: 0; max-width: calc(100vw - 1.25rem);
    margin: 0; padding: 6px; list-style: none; min-width: 168px;
    background: #141b28; border: 1px solid rgba(255,255,255,0.12);
    border-radius: 12px; box-shadow: 0 12px 34px rgba(0,0,0,0.45); z-index: 60;
}
.lang-item {
    display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
    width: 100%; padding: 8px 11px; text-align: start;
    background: transparent; border: 0; border-radius: 8px; color: #e6ebf3; cursor: pointer;
}
.lang-item:hover { background: rgba(255,255,255,0.07); }
.lang-item.active { background: rgba(56,161,105,0.22); color: #fff; }
.lang-native { font-size: 0.95rem; line-height: 1.1; }
.lang-en { font-size: 0.72rem; opacity: 0.55; }

/* ── RTL: the switcher flips <html dir>; keep icon/text spacing symmetric ──── */
[dir="rtl"] .navpill img,
[dir="rtl"] .menu-tile .menu-icon { transform: scaleX(1); }
[dir="rtl"] .home-note,
[dir="rtl"] .auth-form { text-align: right; }
/* Language-specific brand wordmark (image swapped by LocalizationState.Img). */
.brand-word-img { height: 1.7rem; width: auto; display: block; }
/* #433: `class="ltr"` was markup-only. The ONLY rule naming it was `[dir="rtl"] input.ltr` below,
   so on a <span>, <td> or <code> it declared NOTHING and the content still took the VIEWER's
   direction: a "12-5" win-loss cell read "5-12" in Hebrew (the hyphen is neutral between two
   numbers, so the runs reorder). The class now declares what it always claimed to. */
.ltr { direction: ltr; unicode-bidi: isolate; }
/* Left-to-right fields (email, password, URLs, user IDs) must stay LTR even in an RTL (Hebrew) UI. */
[dir="rtl"] input[type="email"],
[dir="rtl"] input[type="password"],
[dir="rtl"] input[type="url"],
[dir="rtl"] input.ltr {
    direction: ltr;
    text-align: left;
}
/* Home icon lives inside the brand link (game icon | wordmark | home icon); whole brand is clickable → home. */
.brand-home { height: 1.5rem; width: auto; display: block; opacity: 0.8; margin-inline-start: 0.15rem; }
.topnav .brand:hover .brand-home { opacity: 1; }
/* 6-digit invite code shown on the invite screen (always LTR, easy to read/copy). */
.invite-code {
    font-size: 2rem; font-weight: 800; letter-spacing: 0.32em;
    text-align: center; padding: 0.5rem 0.25rem 0.5rem 0.6rem; color: #eef2f8;
    background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12);
    border-radius: 10px; direction: ltr; user-select: all;
}
/* ── Wide, prominent primary action buttons (Start game, Join queue, Create invite, Sign in, …) ── */
/* #385: and it WRAPS, for #236's reason one control over. .chip is `white-space: nowrap` (#200)
   because a chip normally shares a row and the ROW is what gives way - but a .cta is `width: 100%`,
   it IS its own row, so there is nothing left to give: in the ~250px game side rail ru
   "Отменить приглашение?" is 24px wider than the pill and ran straight out of both drawn ends,
   which no cap width can fix. Two lines inside the plate is the only reading that stays true for
   the next longer translation, and the box grows with them (padding-block, not min-height). */
.cta {
    display: flex; width: 100%; box-sizing: border-box; white-space: normal;
    padding: 0.95rem 1.2rem; font-size: 1.1rem; font-weight: 800;
    margin: 1.15rem 0 0.15rem;
}
.cta-row { display: flex; flex-direction: column; gap: 0.55rem; margin-top: 1.15rem; }
/* #639: a primary action must read wider than the option row it confirms, not merely inherit
   the same content width. The panel keeps .6rem of its 1.1rem inline padding on each side. */
.cta-row .cta { width: calc(100% + 1rem); margin-block: 0; margin-inline: -.5rem; }

/* ── Find tabs: the active tab connects into the panel below it ─────────────────────────────────── */
.tab-strip { display: flex; gap: 0.3rem; padding: 0 0.7rem; margin: 0.5rem auto 0; max-width: 460px; }  /* #311 */
.tab {
    /* #236: .tab-strip is a flex row, so every tab already STRETCHES to the tallest one — but the
       label inside sat at the top of the box, so when RU "Принять приглашение" wrapped to two lines
       its siblings became two lines tall with their text stuck against the top edge, and the strip
       read as uneven. Centring the label makes a strip of mixed 1- and 2-line tabs look deliberate
       in any language, instead of depending on every translation being short enough.
       INLINE-flex, not flex (gate 20260816-052813): `.tab` is also worn by the buttons inside a
       plain `.tabs`, a container with no layout rule of its own anywhere in this file. (#646 took
       /hotseat's vs-Human / vs-AI pair off the page — that was the strip the gate caught — so the
       `.tabs` still riding this rule is /feed's follow strip. The invariant is the container's, not
       that page's, which is why removing one of its users does not retire it.)
       `display: flex` is BLOCK-level, so there it stopped being two buttons on one line and became
       two stacked rows — +35px of chrome above the board, which put the local board below the fold
       on every desktop persona (#193 guard, 24 of them). `inline-flex` centres the label exactly
       the same way, stays inline-level where nothing lays the tabs out, and is blockified back to
       `flex` when the tab IS a flex item — i.e. inside `.tab-strip`, #236's actual subject, the
       computed display is unchanged. */
    display: inline-flex; align-items: center; justify-content: center;
    flex: 1 1 0; min-width: 0; text-align: center; line-height: 1.15;
    padding: 0.55rem 0.5rem; font-weight: 700; font-size: 0.9rem; cursor: pointer; user-select: none;
    color: var(--ink-dim); background: rgba(18,25,37,.6); border-image: none;
    border: 1px solid rgba(201,162,75,.18); border-bottom: none;
    border-radius: 12px 12px 0 0; position: relative; top: 1px;
}
.tab:hover { color: var(--ink); }
.tab:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }
.tab.active {
    color: var(--ink); background: rgba(52,64,82,.62);
    border-color: rgba(201,162,75,.30); z-index: 2;
}
.panel-tabbed { margin-top: 0; border-top-left-radius: 3px; }
/* #230: try mode renders no tab strip, so the panel is a panel again at the top - but it keeps
   .panel-tabbed, because the wide-layout board sizing rules below key off that class.
   #646: /hotseat renders no strip on ANY entry path now, so it wears this modifier unconditionally
   and the pair stops being a live branch with one unreachable arm: .panel-tabbed is the wide-layout
   BOARD-SIZING hook (/create, /queue and /hotseat all wear it; /queue has no strip either, so the
   name records where the hook came from rather than claiming tabs), and .panel-untabbed is /hotseat
   saying it has nothing above it to connect to. */
.panel-tabbed.panel-untabbed { margin-top: 0.5rem; border-top-left-radius: 16px; }
[dir="rtl"] .tab { border-radius: 12px 12px 0 0; }

/* ============================================================
   Admin console (web-only /admin). Bare AdminLayout, no game chrome.
   ============================================================ */
.admin-root { min-height: 100vh; background: #10141d; color: var(--ink);
    font-size: 0.9rem; }
.admin-muted { color: var(--ink-dim); }
.admin-error { color: #ff8a8a; background: rgba(255,60,60,.10); border: 1px solid rgba(255,60,60,.35);
    padding: .5rem .7rem; border-radius: 8px; margin: .5rem 0; }

/* login */
.admin-login { max-width: 360px; margin: 12vh auto; padding: 1.6rem; text-align: center;
    background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.08); border-radius: 14px; }
.admin-login h1 { margin: 0 0 .4rem; }
.admin-login input { margin: .8rem 0; }

/* header + tabs */
.admin-header { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; gap: 1rem;
    padding: .6rem 1rem; background: #0c0f16; border-bottom: 1px solid rgba(255,255,255,.08); }
.admin-header strong { font-size: 1.05rem; color: var(--gold); }
.admin-spacer { flex: 1; }
.admin-tabs { display: flex; gap: .25rem; flex-wrap: wrap; }
.admin-tab { background: transparent; border-image: none; border: 1px solid transparent; color: var(--ink-dim);
    padding: .4rem .8rem; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: .88rem; }
.admin-tab:hover { color: var(--ink); }
.admin-tab.active { color: var(--ink); background: rgba(255,255,255,.07); border-color: rgba(201,162,75,.35); }

.admin-main { padding: 1.2rem 1rem 4rem; max-width: 1200px; margin: 0 auto; }

/* buttons */
.admin-btn { display: inline-flex; align-items: center; justify-content: center; gap: .3rem;
    padding: .45rem .85rem; min-height: 34px; border-radius: 8px; cursor: pointer; font-weight: 600;
    font-size: .85rem; color: var(--ink); background: #1e2634; border-image: none; border: 1px solid rgba(255,255,255,.15); }
.admin-btn:hover { background: #26303f; }
.admin-btn:disabled { opacity: .5; cursor: default; }
.admin-btn.sm { padding: .25rem .55rem; min-height: 28px; font-size: .8rem; }
.admin-btn.primary { background: #2f7d4f; border-color: #38935d; }
.admin-btn.primary:hover { background: #369059; }
.admin-btn.danger { background: var(--danger-bg); border-color: var(--danger-edge); }
.admin-btn.danger:hover { background: var(--danger-bg-hover); }
.admin-btn.ghost { background: transparent; }

/* overview cards */
.admin-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: .75rem; margin-bottom: 1.5rem; }
.admin-card { display: flex; flex-direction: column; gap: .2rem; padding: 1rem;
    background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.08); border-radius: 12px; }
.admin-card .n { font-size: 1.7rem; font-weight: 800; color: var(--gold); }
.admin-card .l { color: var(--ink-dim); font-size: .8rem; }
.cara-provider-usage { grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); }
.cara-provider-usage .n { overflow-wrap: anywhere; }

/* toolbar */
.admin-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .8rem; }
.admin-toolbar input, .admin-toolbar select { width: auto; min-width: 180px; }
.admin-check { display: inline-flex; align-items: center; gap: .35rem; color: var(--ink-dim); white-space: nowrap; }
.admin-check input { width: auto; }

/* tables */
/* Wide admin tables scroll INSIDE their own box on narrow viewports (block+overflow-x) — the
   page itself must never overflow horizontally (UX-audit C1; caught live by the admin walk when
   the Feedback tab's Reason column widened the table past a phone viewport).
   #350: the head and the body are a table-header-group and a table-row-group - NOT two boxes of
   display: table. As two tables they each sized their columns from their own content, so every
   header but the first sat off the column it named: 353px off on Users, 646px on Feedback, at
   every viewport including the 1280x900 the release gate walks. As row groups they are laid out
   by the ONE anonymous table the block box generates around them, so they share a single column
   layout - aligned by construction, and still free to grow past the box and scroll it.
   border-collapse and the font size are inherited, so the anonymous table keeps both. */
.admin-table { width: 100%; border-collapse: collapse; font-size: .85rem; display: block; overflow-x: auto; }
.admin-table thead { display: table-header-group; }
.admin-table tbody { display: table-row-group; }
.admin-table th, .admin-table td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid rgba(255,255,255,.07);
    vertical-align: top; }
.admin-table thead th { color: var(--ink-dim); font-weight: 600; font-size: .78rem; text-transform: uppercase; letter-spacing: .03em; }
.admin-table tbody tr:hover { background: rgba(255,255,255,.03); }
.admin-table.sm th, .admin-table.sm td { padding: .3rem .5rem; }
.admin-sub { color: var(--ink-dim); font-size: .72rem; word-break: break-all; }
.admin-msg { max-width: 380px; white-space: pre-wrap; word-break: break-word; }

/* badges */
.admin-badge { display: inline-block; padding: .12rem .5rem; border-radius: 999px; font-size: .74rem; font-weight: 700;
    background: rgba(255,255,255,.10); color: var(--ink); }
.admin-badge.guest { background: rgba(255,255,255,.12); }
.admin-badge.user { background: rgba(90,140,220,.25); color: #bcd4ff; }
.admin-badge.premium { background: rgba(201,162,75,.28); color: #f0d79a; }
.admin-badge.danger { background: var(--danger-tint); color: var(--danger-ink-soft); }

.admin-pager { display: flex; align-items: center; gap: .8rem; margin-top: .8rem; }

/* modal */
.admin-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.55); display: flex; justify-content: center;
    align-items: flex-start; padding: 4vh 1rem; overflow-y: auto; z-index: 50; }
.admin-modal { width: 100%; max-width: 720px; background: #161b26; border: 1px solid rgba(255,255,255,.12);
    border-radius: 14px; padding: 1.2rem; }
.admin-modal-head { display: flex; align-items: center; justify-content: space-between; }
.admin-modal-head h2 { margin: 0; }
.admin-modal details { margin-top: .6rem; border-top: 1px solid rgba(255,255,255,.08); padding-top: .5rem; }
.admin-modal summary { cursor: pointer; color: var(--ink-dim); font-weight: 600; }
/* #327: the profile VIEW (Users tab -> Profile): a panel in the main column laid out like the modal's
   body - the identity grid the old user modal had, a management block (standing, note, locks,
   badges) and the satellite tables. .admin-pills wraps, so eight 24-character badges (the server's
   bounds) fold into rows at 360px instead of widening the page (C1). */
.admin-profile { max-width: 900px; }
.admin-profile-head { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; margin: .4rem 0 .8rem; }
.admin-profile-head h2 { margin: 0; font-size: 1.2rem; }
.admin-avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; flex: 0 0 auto;
  border: 2px solid rgba(201,162,75,.4); background: rgba(255,255,255,.08);
  display: inline-flex; align-items: center; justify-content: center; font-weight: 800; }
.admin-profile h3 { margin: 1rem 0 .2rem; color: var(--ink-dim); font-size: .95rem; }
.admin-profile details { margin-top: .6rem; border-top: 1px solid rgba(255,255,255,.08); padding-top: .5rem; }
.admin-profile summary { cursor: pointer; color: var(--ink-dim); font-weight: 600; }
.admin-grid .admin-field { display: flex; flex-direction: column; gap: .25rem; color: var(--ink-dim); font-size: .82rem; grid-column: 1 / -1; }
.admin-pills { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; }
.admin-pill { display: inline-flex; align-items: center; gap: .3rem; padding: .15rem .3rem .15rem .6rem; border-radius: 999px;
  font-size: .78rem; font-weight: 700; background: rgba(90,140,220,.25); color: #bcd4ff; white-space: nowrap; }
.admin-pill button { background: none; border-image: none; border: 0; color: inherit; cursor: pointer; padding: 0 .2rem; font-size: .9rem; line-height: 1; }
.admin-pills .admin-badge-input { width: 12rem; max-width: 100%; }
/* #335: the chat block of the profile view — the thread in its own scroll box (a long conversation
   never grows the panel), lines that wrap (a 2000-character line stays inside a 320px panel), a
   composer whose textarea takes the row's width, and a seen/unseen mark on the administrator's own
   lines. .admin-badge.unread is the Users list's unread-from-user mark. */
.admin-thread { max-height: 260px; overflow-y: auto; padding: .4rem .6rem; border-radius: 10px;
    background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.08); }
.admin-thread .admin-line { margin: .25rem 0; word-break: break-word; white-space: pre-wrap; }
.admin-thread .admin-line .admin-who { font-weight: 700; color: #9cc4ff; }
.admin-thread .admin-line.from-admin .admin-who { color: #f0d79a; }
.admin-thread .admin-line .admin-when { color: var(--ink-dim); font-size: .72rem; margin-inline-start: .4rem; }
.admin-compose { display: flex; gap: .5rem; align-items: flex-end; margin-top: .5rem; }
.admin-compose textarea { flex: 1; min-width: 0; }
.admin-badge.unread { background: rgba(233,200,119,.28); color: #f0d79a; }
.admin-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: .7rem; margin: .9rem 0; }
.admin-grid label { display: flex; flex-direction: column; gap: .25rem; color: var(--ink-dim); font-size: .82rem; }
.admin-grid label.admin-check { flex-direction: row; align-items: center; }
/* #375: a READ-ONLY field is text in an input-shaped box, never a disabled <input>. A disabled
   control takes no focus, so a value wider than its box was cut mid-glyph at the border with no
   ellipsis and no way to reach the rest - not by scrolling it, not by selecting it (the feedback
   modal's From at 360px, a joined-session id: nothing after the prefix was readable, and nothing
   said so). Per #257 the fix is the BOX and never the string, so this box keeps the input's own
   chrome, font and padding and lets the value WRAP inside it: the whole value is readable at any
   width with no focus and no gesture, and still selectable to copy. min-height keeps an empty
   value (a game with no second player yet) a box rather than a collapsed line.
   Held by Application\test-admin-layout.ps1 over every read-only field of every .admin-grid at
   every viewport, and by AdminMobileLayoutTests over the page's markup. */
.admin-ro { display: block; box-sizing: border-box; width: 100%; min-height: 2.35rem;
    padding: 0.6rem 0.7rem; background: #1b2430; color: var(--ink); font-size: 0.95rem;
    border: 1px solid rgba(255,255,255,.15); border-radius: 9px;
    overflow-wrap: anywhere; word-break: break-word; }
.admin-actions { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; margin: .4rem 0 .6rem; }
/* #323 (client feedback 7c2fe24b, owner, Samsung browser 360x273): the admin CHROME on a phone.
   Until here the console's only narrow rules collapsed two content grids; the sticky header kept
   its title, the 8-tab strip, the flex: 1 spacer and two buttons on ONE flex line with no wrap, so
   the strip wrapped inside whatever slot the title and buttons left and the header held that
   height on screen permanently; .admin-main spent 32px of a 360px viewport on side padding; every
   toolbar control kept a 180px floor. Here the header becomes two rows - the title with Refresh
   and Sign out, then the tab strip on a full-width row of its own, still WRAPPING so that every
   tab stays visible (a sideways-scrolling strip hides tabs behind no affordance); the main column,
   the toolbars, the modal and the login card give up their desktop padding. And on a SHORT
   viewport (a landscape phone, or a portrait one with the keyboard up - the reporter's 273px) the
   header scrolls with the page instead of pinning a third of the screen. Measured by
   Application\test-admin-layout.ps1 (check-main.ps1) on the reporter's viewport and the phone
   matrix; AdminMobileLayoutTests pins this block against the probe and the page. */
@media (max-width: 680px) {
    .admin-grid { grid-template-columns: 1fr; }
    .admin-login { box-sizing: border-box; width: calc(100% - 1.2rem); margin: 6vh auto; padding: 1.2rem 1rem; }
    .admin-header { flex-wrap: wrap; gap: .4rem .5rem; padding: .5rem .6rem; }
    .admin-header strong { font-size: 1rem; }
    .admin-tabs { order: 1; flex: 1 0 100%; gap: .2rem; }
    .admin-tab { padding: .3rem .55rem; font-size: .82rem; }
    .admin-main { padding: .8rem .6rem 3rem; }
    .admin-toolbar input, .admin-toolbar select { min-width: 0; flex: 1 1 140px; max-width: 100%; }
    .admin-overlay { padding: 1.5vh .4rem; }
    .admin-modal { padding: .9rem .75rem; }
    /* The six provider columns are useful together but not legible when squeezed into a phone.
       Keep every field in the DOM and stack each row as labelled facts instead of hiding columns
       behind an initially clipped horizontal scroll area. */
    .cara-provider-workers { display: block !important; width: 100%; white-space: normal !important; }
    .cara-provider-workers thead { display: none !important; }
    .cara-provider-workers tbody { display: block !important; }
    .cara-provider-workers tr { display: block; padding: .35rem 0; border-bottom: 1px solid rgba(255,255,255,.12); }
    .cara-provider-workers td { display: grid; grid-template-columns: minmax(5.5rem, .45fr) minmax(0, 1fr); gap: .5rem;
        padding: .22rem .35rem; border: 0; overflow-wrap: anywhere; }
    .cara-provider-workers td::before { content: attr(data-label); color: var(--ink-dim); font-size: .72rem;
        font-weight: 600; text-transform: uppercase; letter-spacing: .03em; }
}
/* The header pins only where a screen can afford it: below 480px of height it scrolls away with the page. */
@media (max-height: 480px) { .admin-header { position: static; } }

/* ── Corner app menu (hamburger → Settings / Report / About) ───────────────── */
.app-menu { position: relative; display: inline-flex; }
.app-menu-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 40px; height: 40px; padding: 0;
    background: transparent; border-image: none; border: 1px solid rgba(255,255,255,0.15);
    border-radius: 10px; color: #cfd6e4; cursor: pointer;
}
.app-menu-btn:hover { background: rgba(255,255,255,0.07); color: #eef2f8; }
.app-menu-list {
    position: absolute; top: calc(100% + 6px); inset-inline-start: 0; max-width: calc(100vw - 1.25rem);
    margin: 0; padding: 6px; list-style: none; min-width: 190px;
    background: #141b28; border: 1px solid rgba(255,255,255,0.12);
    border-radius: 12px; box-shadow: 0 12px 34px rgba(0,0,0,0.45); z-index: 60;
}
.app-menu-item {
    display: flex; align-items: center; gap: 0.6rem;
    width: 100%; padding: 9px 12px; text-align: start; font-size: 0.95rem;
    background: transparent; border-image: none; border: 0; border-radius: 8px; color: #e6ebf3; cursor: pointer;
}
.app-menu-item:hover { background: rgba(255,255,255,0.07); }
.app-menu-item .ico { font-size: 1.05rem; width: 1.3rem; text-align: center; }
.app-menu-item .menu-lock { margin-inline-start: auto; font-size: 0.85rem; opacity: 0.75; }
/* #335: the unread mark of the administrator's chat — a count pill on the feedback entry (the same
   right-aligned slot the guest lock uses) and a dot on the hamburger, so a waiting reply shows before
   the menu opens. Both are bounded boxes: the pill holds a number, never a string. */
.app-menu-btn { position: relative; }
.app-menu-dot { position: absolute; top: 5px; inset-inline-end: 5px; width: 9px; height: 9px; border-radius: 50%;
    background: #e9c877; box-shadow: 0 0 0 2px #141b28; }
.app-menu-item .menu-count { margin-inline-start: auto; min-width: 1.4rem; padding: 0 .4rem; border-radius: 999px;
    font-size: .75rem; font-weight: 700; line-height: 1.4rem; text-align: center; background: #e9c877; color: #1a1408; }

/* ── #435: unread marks ───────────────────────────────────────────────────────
   The aggregated unread counter, on every surface on the path to what changed. NOT `.badge`: that
   name already carries the premium/guest tier chip and the #281 contribution level and award chips,
   and a third meaning on it would collide with contributions on sight.

   Both dimensions of the label are bounded, which is what keeps this off the C1/C18 list at phone
   width. The STRING is bounded by UnreadTally.Format — 99+ above the cap, never more than three
   characters, with the exact number in the accessible label rather than on screen. The BOX is
   bounded here: max-width holds those three characters at this size, `flex: 0 0 auto` stops the
   row's other items from paying for it (the #309 rule, one family over), and `overflow: hidden` is
   the floor under both. Measured at 360px on /games, on the home tiles and in the corner menu. */
.unread-mark { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
    box-sizing: border-box; min-width: 1.35rem; max-width: 2.75rem; height: 1.35rem; padding: 0 .35rem;
    border-radius: 999px; font-size: .72rem; font-weight: 800; line-height: 1; white-space: nowrap;
    overflow: hidden; background: #e9c877; color: #1a1408; }
/* The web's stand-in for the mobile app icon: the TOTAL, over the brand mark, on every screen. */
.topnav .brand { position: relative; }
.unread-mark.on-brand { position: absolute; top: -5px; inset-inline-start: -5px; z-index: 1;
    box-shadow: 0 0 0 2px rgb(35,45,61); }
/* The corner menu button. The opposite corner from the #335 dot on purpose — they are marks about
   two different things, and one covering the other would hide whichever arrived second. */
.unread-mark.on-menu-btn { position: absolute; top: -5px; inset-inline-start: -5px; z-index: 1;
    box-shadow: 0 0 0 2px rgb(35,45,61); }
/* Inside a menu entry — the same right-aligned slot the #335 count and the guest lock use. */
.app-menu-item .unread-mark.in-menu-item { margin-inline-start: auto; }
/* Beside a home tile's label. */
.unread-mark.in-tile { margin-inline-start: .45rem; vertical-align: middle; }
/* Inside a store/download link on /download (issue 443): the END of the path the owner enumerated,
   so it sits ON the link's own text rather than over a corner of it. In flow on purpose - the link
   is prose in a wrapping strip, not a fixed-size tile, so an absolute mark would fall off it. */
.unread-mark.on-store-link { margin-inline-start: .4rem; vertical-align: middle; }
/* On a games-list row the mark rides ON the row's lead element and takes NO width of its own.
   Issue 403 / #309 established that this row's trailing items are rigid and `.rc-main` is the only
   thing that yields, so a new fixed-width item in the flex line is paid for by the NAME: measured at
   360px, a 24-character opponent name went from three wrapped lines to six-character fragments when
   the mark sat beside the pill. A badge over the existing 34px lead costs the row nothing. */
.row-card .rc-lead { position: relative; display: inline-flex; flex: 0 0 auto; }
.unread-mark.on-lead { position: absolute; top: -7px; inset-inline-start: -7px; z-index: 1;
    box-shadow: 0 0 0 2px rgb(35,45,61); }

/* ── Profile stats + ratings ──────────────────────────────────────────────── */
.stats-card { max-width: 460px; margin: 1.2rem auto; padding: 1rem 1.1rem; border-radius: 14px;   /* #311 */
    border: 1px solid rgba(201,162,75,.18); background: rgba(18,25,37,.5); }
.stats-card h2 { margin: 0 0 .8rem; font-size: 1.15rem; }
.stats-record { display: flex; gap: .6rem; margin-bottom: 1rem; }
.stats-record .stat { flex: 1 1 0; min-width: 0; text-align: center; padding: .6rem .3rem;
    border-radius: 10px; background: rgba(255,255,255,.04); }
.stats-record .stat-num { display: block; font-size: 1.7rem; font-weight: 800; color: var(--ink); }
.stats-record .stat-label { display: block; font-size: .72rem; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: .5px; }
.stats-sub { margin: .4rem 0 .5rem; font-size: 1rem; color: var(--ink); }
.ratings-table { width: 100%; border-collapse: collapse; }
.ratings-table th, .ratings-table td { padding: .45rem .5rem; text-align: start;
    border-bottom: 1px solid rgba(255,255,255,.06); }
.ratings-table th { font-size: .72rem; color: var(--ink-dim); text-transform: uppercase; letter-spacing: .4px; }
.ratings-table .rating-val { font-weight: 800; color: var(--gold); font-variant-numeric: tabular-nums; }

/* ── #487: the two statistics charts (PlayerStatsDimensions, /profile + /player) ────────────────
   The first series this product has ever drawn, and both are read TWICE - once as a shape and once
   as words (the activity strip carries a summary aria-label, the rating line a table of the same
   points behind a disclosure). Neither shape is load-bearing on its own.

   The activity strip is one flex item per DAY of the window, gaps filled with zero: a render that
   walked the SPARSE series would squeeze the empty days out and draw a busier player than the one
   who played. At the 365-day window inside a 460px card each bar is well under a pixel wide, so the
   item carries no min-width and the strip's only gap is 1px: the bars butt up and it reads as a
   DENSITY, which is the honest reading at that width. min-height keeps a day with nothing on it as
   a baseline tick rather than a hole in the row, so the axis stays visible end to end. */
.act-strip { display: flex; align-items: flex-end; gap: 1px; height: 56px; margin: .2rem 0 .6rem;
    padding: .25rem .3rem; border-radius: 8px; background: rgba(255,255,255,.04); }
.act-day { flex: 1 1 0; min-width: 0; min-height: 2px; border-radius: 1px;
    background: var(--gold); opacity: .75; }
/* The rating line stretches to whatever width the card has (preserveAspectRatio="none" in the
   markup), which is what lets it fit a phone and a desktop with no measurement of either. Only the
   GEOMETRY is distorted by that: the stroke opts out through vector-effect, so the line keeps one
   weight at every width. currentColor here is the whole of its palette. */
.rating-spark { display: block; width: 100%; height: 64px; margin: .2rem 0 .6rem; color: var(--gold); }

/* ── #459: the public rankings board (/rankings) ───────────────────────────────────────────────
   A leaderboard is a TABLE on every other product and a row card here, deliberately: the phone
   width is where a ranking table breaks (the admin tables already have to scroll their own box at
   360px), and .card-list/.row-card is the family this repo has measured there - the name wraps
   inside the card (#275), the trailing rating is rigid so the name is the item that yields (#309),
   and below 360px the row takes a second line rather than cutting the name to six characters
   (issue 403). Reusing it buys every one of those measurements; only the rank badge is new.
   The badge's whole rule is THE BOX GIVES, NOT THE STRING (owner directive, feedback f7d77400):
   min-width keeps the column steady while ranks are single digits, and horizontal padding - never a
   fixed width - is what lets a four-figure rank render whole instead of clipping inside it. */
.rank-row .rc-rank { flex: 0 0 auto; min-width: 34px; height: 34px; padding: 0 .4rem; box-sizing: border-box;
    display: inline-flex; align-items: center; justify-content: center; border-radius: 8px;
    background: rgba(255,255,255,.07); color: var(--ink-dim);
    font-weight: 800; font-size: .95rem; font-variant-numeric: tabular-nums; }
.rank-row .rc-rating { color: var(--gold); }
/* The reader's own row, wherever it lands: a border tint and a badge tint - this sheet's sanctioned
   emphasis. Never a background swap on a hover-capable element (the touch-stuck class, C12). */
.rank-row.rank-you { border-color: rgba(201,162,75,.55); }
.rank-row.rank-you .rc-rank { background: rgba(201,162,75,.20); color: var(--gold); }
/* The pager's position line shares the wrapping chip row with its buttons, so at phone width it
   takes a line of its own instead of squeezing them. */
.rank-range { align-self: center; color: var(--ink-dim); font-size: .85rem; font-variant-numeric: tabular-nums; }

/* ── App modal dialogs (Report / Settings / About) ────────────────────────── */
.modal-overlay {
    position: fixed; inset: 0; z-index: 200; display: flex; justify-content: center; align-items: flex-start;
    padding: 6vh 1rem 2rem; overflow-y: auto; background: rgba(6,9,14,0.62);
}
.modal-card {
    width: 100%; max-width: 460px; background: #161d29; color: var(--ink);
    border: 1px solid rgba(255,255,255,0.12); border-radius: 16px;
    box-shadow: 0 20px 60px rgba(0,0,0,0.5); padding: 1.1rem 1.2rem 1.3rem;
}
.modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.6rem; }
.modal-head h2 { margin: 0; font-size: 1.2rem; }
.modal-x {
    width: 34px; height: 34px; padding: 0; font-size: 1rem; line-height: 1;
    background: transparent; border-image: none; border: 1px solid rgba(255,255,255,0.15); border-radius: 9px;
    color: var(--ink-dim); cursor: pointer;
}
.modal-x:hover { background: rgba(255,255,255,0.07); color: var(--ink); }
.modal-actions { display: flex; justify-content: center; margin-top: 0.4rem; }
.modal-card textarea { width: 100%; box-sizing: border-box; resize: vertical; min-height: 96px; }
/* #335: the conversation with the administrator — a section of the feedback dialog, always on. The
   thread scrolls in its own box (max-height), so a long conversation never grows the card past a
   360x531 screen, and every line wraps (word-break), so a 2000-character line stays inside the card
   (#257's rule: constrain the box, never the string). */
.admin-chat { margin-top: 1rem; padding-top: .8rem; border-top: 1px solid rgba(255,255,255,.1); }
.admin-chat h3 { margin: 0 0 .5rem; font-size: .95rem; color: var(--ink-dim); }
.admin-chat .muted { margin: 0 0 .4rem; }
.admin-chat .admin-chat-thread { max-height: 180px; padding: .4rem .5rem; border-radius: 10px; background: rgba(255,255,255,.04); }
.admin-chat .chat-line .chat-name { color: #9cc4ff; }
.admin-chat .chat-line.from-admin .chat-name { color: #e9c877; }
.admin-chat .chat-send { margin-top: .5rem; }
.admin-chat .chat-send input { min-width: 0; }
.report-shot { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; }
.report-shot img {
    max-width: 100%; max-height: 220px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.15);
}
/* Up to two attached screenshots side by side; each shrinks to fit narrow (mobile/RTL) widths. */
.report-shots { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-bottom: 0.4rem; }
.report-shots .report-shot { flex: 1 1 140px; min-width: 0; }
.report-shots .report-shot img { max-height: 150px; }

/* Rules-page terms gate: the ToS text scrolls inside a bounded box; Agree enables at its end. */
.terms-scroll {
    max-height: min(46vh, 340px); overflow-y: auto; margin: 0.6rem 0; padding: 0.8rem 1rem;
    border: 1px solid rgba(255,255,255,0.18); border-radius: 10px;
}
.terms-scroll h3 { font-size: 1.3rem; margin-top: 0; }
.terms-scroll h4 { font-size: 1.05rem; }
.rules-terms-gate .terms-accept-btn { margin-top: 0.4rem; }

/* "Open in the Caracri app" chip on game pages (issue #30, desktop browsers only). */
.app-banner {
    display: inline-flex; align-items: center; gap: 0.4rem; margin: 0 0 0.6rem;
    padding: 0.3rem 0.6rem; border: 1px solid rgba(255,255,255,0.18); border-radius: 999px;
}
.app-banner-link { text-decoration: none; font-weight: 600; }
.app-banner-dismiss { line-height: 1; }

/* admin feedback screenshot + action cell */
.admin-shot { max-width: 100%; max-height: 320px; border-radius: 10px; border: 1px solid rgba(255,255,255,.15); cursor: zoom-in; }
.admin-nowrap { white-space: nowrap; }

.info-body { white-space: pre-wrap; line-height: 1.55; color: var(--ink); font-size: 0.95rem; }

.menu-scrim { position: fixed; inset: 0; z-index: 55; background: transparent; }

/* admin JSON blocks (report state/telemetry) + telemetry bars */
.admin-json { max-height: 260px; overflow: auto; background: #0c0f16; border: 1px solid rgba(255,255,255,.1);
    border-radius: 8px; padding: .6rem .7rem; font-size: .74rem; line-height: 1.4; white-space: pre-wrap; word-break: break-word; color: #cbd5e6; }
.admin-bar { display: grid; grid-template-columns: 1fr auto; gap: .5rem; align-items: center; padding: .18rem 0; }
.admin-bar .lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .8rem; }
.admin-bar .track { grid-column: 1 / -1; height: 6px; background: rgba(255,255,255,.07); border-radius: 4px; overflow: hidden; }
.admin-bar .fill { height: 100%; background: var(--gold); }

.admin-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin: 1rem 0; }
@media (max-width: 640px) { .admin-cols { grid-template-columns: 1fr; } }

/* settings checkbox */
.app-check { display: inline-flex; align-items: center; gap: .45rem; color: var(--ink); cursor: pointer; }
.app-check input { width: auto; }

/* nav layout: menu (left) | brand (centre) | language (right); auth + links on a second row.
   Keeps the menu and language buttons in opposite corners so they aren't misclicked. */
.topnav { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: .5rem; padding: .4rem 0 .5rem; margin-bottom: .4rem; }
.nav-corner { display: inline-flex; align-items: center; }
.nav-left { justify-self: start; }
.nav-right { justify-self: end; }
.topnav .brand { justify-self: center; }
.subnav { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .4rem; margin: 0 0 .5rem; min-height: 40px; }  /* #107: tighter below the profile row */

/* ==========================================================================
   Responsive layouts (vertical vs horizontal) + app-level Game Focus Mode.
   Breakpoint: horizontal when the viewport is both reasonably wide AND at least
   as wide as it is tall (landscape phones/tablets + desktop windows); otherwise
   vertical. It is a pure CSS @media query, so it re-evaluates live on resize /
   rotation in the browser AND the MAUI WebView with zero JS and zero game-state
   loss. The board lives in ONE stable DOM element instance (.game-board-wrap)
   that is never re-parented between layouts or focus mode, so the <canvas> + JS
   module + live game are never re-mounted.
   ========================================================================== */

/* Vertical (default): plain block flow — today's stacked design (info/clock
   above the board, moves/controls below). */
.game-layout { display: block; }
.game-board-wrap { min-width: 0; }

/* #312 (client feedback 5a33eea4, owner: "the board must not push controls outside the visible
   screen"): every viewport-height cap on the board lived inside the wide-layout query below, so
   under it the board was sized by WIDTH alone — a 700x700 window, a landscape phone at 740x360 or
   800x360, a portrait tablet at 768x1024 all got a board up to 680px tall on a screen that could not
   hold it, and the action row (hot-seat) / the Resign row (online) went below the fold by the
   board's own height. Same shape as #193/#302: the board is min(width, 680px, max(300px floor,
   100vh - chrome)), chrome being the FIXED pixel stack that must share the screen with it —
   everything from the top of the page to the canvas plus the controls under it, MEASURED with
   Application\test-board-fold-layout.ps1 -Report (the real stylesheet, the real markup, headless
   Chromium): online 480px above (498 guest; a player of a timed game adds a 45px pause bar the probe template lacked until #314) + 221-259px below to the Resign row; hot-seat 399px
   above (timed local game) + 44px below (one chip line; +40 for a locale that wraps it). The cap is
   on the WRAP, not the stage, so the player strip narrows and centres WITH the board instead of
   stretching across a column the board no longer fills. The floor is #193's: on a 360px-tall
   landscape phone the calc collapses and a 300px board scrolls, as it does in the wide layout.
   SCOPE — landscape/square viewports below the wide breakpoint, and portrait viewports of tablet
   width. A portrait PHONE is left alone on purpose: there the board is width-bound (283-390px) and
   never the thing that overflows — its chrome is (480+259 > 531 on the reporter's own screen with a
   0px board), and a px floor would only shrink the board for nothing; that half is the follow-up
   filed from the #312 thread. Pinned by test-board-fold-layout.ps1 (geometry at nine viewports) and
   BoardFoldLayoutTests (these constants == the probe's). The wide block below RESETS this: its own
   caps (#302/#193) size the stage, and an auto margin on a grid item would shrink the board column. */
/* #364: online +25px (771 -> 796) and online-focus +25 (601 -> 626). Containing the localized labels
   in the review row made its own controls taller - .rev-btn 48 -> 55px and .rev-save-pos 35 -> 50 -
   which is 23px more chrome under the online board. The constants are the stack this gate MEASURES,
   so they move with it; test-board-fold-layout.ps1 caught the knock-on and BoardFoldLayoutTests
   holds these two numbers equal to the ones that probe judges against. */
/* #401: online +12 (796 -> 808) and online-focus +12 (626 -> 638) - and this time the growth is
   ABOVE the board, not under it. Stating font-family on the button rule grew that rule's .sm
   variant from a 34px floor to a 40px one (its label's line box went 15.0 -> 18.0px), and two .sm
   buttons stand in the ONLINE chrome above the canvas: the share control on the status line and
   the Pause button in a timed game's pause bar. Measured with test-board-fold-layout.ps1 -Report at
   740x360, the stack went 794 -> 806px as a guest and 624 -> 636 in focus mode, so each constant
   moves by the 12px and keeps the 2px of headroom it had. Nothing BELOW the board moved (242px
   either way, all chips) and neither hot-seat view has a .sm button in its chrome, so 500 and 265
   stand unchanged - which is the check on the attribution as much as it is a saving. */
@media (min-aspect-ratio: 1/1), (min-width: 600px) {
    .game-board-wrap {
        --fold-chrome: 808px;
        max-width: min(680px, max(300px, calc(100vh - var(--fold-chrome))));
        margin-inline: auto;
    }
    .panel-tabbed .game-board-wrap { --fold-chrome: 500px; }
    /* Focus mode sheds the header (and the hot-seat card) but keeps the status/clock block. */
    .app.focus-mode .game-board-wrap { --fold-chrome: 638px; }
    .app.focus-mode .panel-tabbed .game-board-wrap { --fold-chrome: 265px; }
}

/* #224: the compact player/opponent block that lived here (.player-block/.pb-*) is gone — it was a
   second/third rendering of the same opponent. The .players-bar strip is the one who-vs-who surface
   in every layout, focus mode included. */

/* #314 (client feedback 5a33eea4, owner, Android 360x531 — the reporter's OWN viewport class, a PORTRAIT
   PHONE, which the #312 cap above leaves out on purpose): there the board is width-bound (283–320px) and
   the CHROME is what overflows. Measured with Application\test-board-fold-layout.ps1 -Report before this
   block: 480px from the page top to the online canvas (16 page padding + 68 brand row + 48 second header
   row + 52 back link + 52 status line + 54 turn line + 70 clocks + 43 player strip + 16 board margin +
   58 skip bar) and 259px from the canvas to the Resign row (moves line + review row + a Full screen /
   Resign row whose chips wear .cta: 50px tall with 21px of margin): 739px of a 531px screen with a 0px
   board; hot-seat 399 + 84. A height cap with any usable floor would only shrink the board, so here the
   chrome gives instead. The budget (cara-product on #314): 531 − 320 board = 211px online, 248 hot-seat,
   for EVERYTHING that is not the board — and it is a design cut, not a trim. While a game is IN PLAY
   (.game-layout.in-play — Game.razor and HotSeatPanel.razor set it while !GameOver; a finished game keeps
   the full page) on a portrait phone:
   - the second header row (profile / sign-in: the menu carries both) and the hot-seat heading leave the
     screen; the page's top padding shrinks and the brand row slims to its 40px corner buttons — the menu
     and language corners stay. (#646: the hot-seat TAB STRIP used to leave with them — hiding it in play
     was #230's "switching tabs abandons the game" one layer down. The strip is gone from the page on
     every entry path now, so the rule that hid it, and the one that restored the panel's corner once it
     was hidden, went with it: the panel is permanently .panel-untabbed and already owns that corner.);
   - the status line, the moves line and the review row are NOT hidden: they move BELOW the controls (one
     scroll down, the back link last). The release gate steps the review, reads the share chip and the
     moves count on LIVE games at its 390x844 persona — a control that scrolls into view is still a
     control, a hidden one is not;
   - ONE row above the board carries whose turn it is and both clocks: .game-info-top and .game-side are
     display: contents, so their children lay out as .game-layout's own flex items and can be ORDERED
     across the two subtrees; the turn text truncates rather than wraps — a second line IS the fold. The
     #286 first-move countdown (and the pause countdown) is one slim line above that row, ellipsised in
     a locale too long for it rather than wrapped;
   - each player card is one line (the rating leaves; the acting glow, the dot, the name and the #278
     readout stay), and the skip bar — the Pass/Skip control the feedback names — is slimmer, never gone;
   - the Full screen / Resign row sits right under the board at chip size (not .cta size), and the pause
     bar shares that row: Pause beside Resign, and in its wide states (PAUSED + Resume, the opponent's
     request with Approve / Decline — controls, and the one thing that must never wait below the fold)
     it takes the row itself and the Full screen / Resign row wraps under it.
   Composes WITH focus mode (which then hides the brand row and the chat on top of this) instead of
   standing in for it: focus mode stays a state the player chooses. Its fixed Exit button sits over the
   inline-end corner of the first row, so there the clocks drop their side labels (the colour bars and
   the players strip's dots still say which is which) and keep clear of it.
   Scope: the complement of the #312 cap's — portrait viewports narrower than 600px; an exactly square
   window meets both (the cap floors the board, this compacts the chrome), harmless. Pinned by
   test-board-fold-layout.ps1 (the promise at 320x568 / 360x531 / 360x640 / 375x667 / 390x844 / 412x915,
   in and out of focus mode, guest header, first-move window, the pause bar on the controls row: controls
   on screen with the board width-bound, every reference row present below them, a finished game keeping
   the full chrome) and PortraitPhoneInPlayChromeTests (the in-play marker in both Razor files and at
   runtime, this scope == the probe's, and nothing the gate drives hidden). */
@media (max-width: 599px) and (orientation: portrait) {
    .app:has(.game-layout.in-play) { padding-top: .25rem; }
    .app:has(.game-layout.in-play) > .subnav,
    .app:has(.game-layout.in-play) main > h1 { display: none; }
    .app:has(.game-layout.in-play) > .topnav { padding: .1rem 0; margin-bottom: .15rem; }
    .app:has(.game-layout.in-play) > .topnav .brand .brand-mark { height: 34px; width: 34px; }
    /* #646: /hotseat's panel is permanently .panel-untabbed now, which gives back the 0.5rem a plain
       .panel keeps above it - correct while the heading is there to separate from, and 8px of a
       211px chrome budget while it is not. In play on a portrait phone the heading is hidden, so
       take it back; this is the same margin the panel had here before the strip came out, so the
       in-play geometry this block is measured against is unchanged. (The 16px corner the rule that
       stood here used to restore is .panel-untabbed's own value now.) */
    .app:has(.game-layout.in-play) .panel-tabbed { margin-top: 0; }
    /* The back link (the matched game's exit link, asserted by the gate) goes to the END of the page. */
    .game-page:has(> .game-layout.in-play) { display: flex; flex-direction: column; }
    .game-page:has(> .game-layout.in-play) > p:first-child { order: 9; margin-bottom: 0; }
    /* One wrapping flex row-set. order −1 = the countdown lines, 0 = the turn / clocks row (and a stored
       local game's header), 1 = the board, 2 = the controls row (Full screen / Resign; a wide pause bar),
       3 = the pause bar beside the controls, 4 = an action error (#288: next to the controls), 5 = the
       rest in source order (status, moves, review, chat, store-on-profile). Every item is a full-width
       row unless a rule below says otherwise. */
    .game-layout.in-play { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; column-gap: .5rem; }
    .game-layout.in-play > .game-info-top, .game-layout.in-play > .game-side { display: contents; }
    .game-layout.in-play > .game-board-wrap,
    .game-layout.in-play > .game-info-top > *,
    .game-layout.in-play > .game-side > * { flex: 1 1 100%; min-width: 0; order: 5; }
    .game-layout.in-play > .game-info-top > .first-move-countdown,
    .game-layout.in-play > .game-info-top > .pause-countdown { order: -1; margin: 0 0 .1rem; padding: 0; max-width: none; font-size: .8rem; line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .game-layout.in-play > .game-info-top > .turn,
    .game-layout.in-play > .game-info-top > .clocks,
    .game-layout.in-play > .game-info-top > .local-header,
    .game-layout.in-play > .game-info-top > .chip { order: 0; }
    .game-layout.in-play > .game-info-top > .turn { flex: 1 1 0; margin: 0; min-height: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .game-layout.in-play > .game-info-top > .clocks { flex: 0 0 auto; margin: 0; max-width: none; gap: .35rem; }
    .game-layout.in-play > .game-info-top > .clocks .clock { flex-direction: row; align-items: baseline; gap: .35rem; padding: .15rem .5rem; border-radius: 8px; }
    .game-layout.in-play > .game-info-top > .clocks .clock .clock-side { font-size: .62rem; }
    .game-layout.in-play > .game-info-top > .clocks .clock .clock-time { font-size: 1rem; }
    .game-layout.in-play > .game-info-top > .local-header { margin: 0 0 .15rem; }
    .game-layout.in-play > .game-info-top > .chip { flex: 0 1 auto; margin: 0 0 .15rem; }
    .game-layout.in-play > .game-board-wrap { order: 1; }
    .game-layout.in-play > .game-side > .toggle-row:not(.review-row) { order: 2; flex: 1 1 auto; margin: .15rem 0 .4rem; }
    .game-layout.in-play > .game-side > .toggle-row:not(.review-row) > .chip.cta { flex: 1 1 auto; width: auto; margin: 0; padding: .55rem .9rem; font-size: 1rem; min-height: 40px; }
    .game-layout.in-play > .game-info-top > .pause-bar { order: 3; flex: 0 1 auto; margin: .15rem 0 .4rem; max-width: none; }
    /* #401: the pause bar's own control is a `main button.sm`, and #401 took that floor to 40px -
       the same box as the .chip.cta controls this bar shares its row with. What then made the row
       48px was the button rule's own `margin: 0.25rem 0`, which put the bar's bottom 5px under the
       fold on the reporter's 360x531 phone, i.e. exactly the "pause request that waits below the
       fold goes unseen" this row exists to prevent. The margin is what is spare here: it separates
       STACKED buttons, and on this row the flex gap already does that. */
    .game-layout.in-play > .game-info-top > .pause-bar button.sm { margin-block: 0; }
    .game-layout.in-play > .game-info-top > .pause-bar:has(.pause-badge),
    .game-layout.in-play > .game-info-top > .pause-bar:has(.pause-msg) { order: 2; flex: 1 1 100%; }
    .game-layout.in-play > .game-side > .action-error { order: 4; margin: .2rem 0; }
    .game-layout.in-play > .game-info-top > .game-status { margin: .6rem 0 0; }
    .game-layout.in-play > .game-side > .moves { margin: .5rem 0 0; }
    /* The strip and the skip bar: one line per card, a slimmer bar. */
    .game-layout.in-play .players-bar { margin: .15rem 0 .2rem; gap: .4rem; }
    .game-layout.in-play .players-bar .p-card, .game-layout.in-play .players-bar .p-card.p-right { flex-wrap: nowrap; padding: .12rem .45rem; gap: .3rem; }
    /* Issue 393: this card keeps ONE line by design (#314's chrome budget has ~2px of slack at
       360x531, so it cannot afford a second), which left the name as the only shrinkable item in a
       row also holding the dot, the no-shrink readout and #188's engine label - measured at 360px,
       34px of name beside a vs-AI opponent, 2 characters. It cannot have the 60% the wrapping card
       gets: the row has no second line to push anything onto, so every pixel the name takes has to
       come from something else in it.
       Issue 403, member 2 - the residue #393 stated and could not buy. IDENTITY OUTRANKS THE
       METADATA THAT DESCRIBES IT: when only one of the two fits, the one that survives is the name.
       So on this card the engine label is not abbreviated, it is not painted - #188's label is
       still rendered, still carries its text, and now carries it in `data-engine` as well, which is
       what the release gate reads (Scenario.cs `{t}.vs-ai-start` / `.vs-ai-move`); it asserts
       exactly what it asserted before and no longer needs the label PAINTED to do it. And the
       progress readout gives up its own pill padding here - every digit stays, 0.9rem of decoration
       does not - because at 320px it was 53px of a 118px card beside a 36px name, which is the same
       inversion one element over. Measured, vs-AI opponent, "Ada Lovelace": 320px 36 -> 50px
       (2 -> 4 characters), 360px 42 -> 70px (3 -> 7), 390px 54 -> 85px (5 -> 9), and at 412px
       55 -> 96px, which is the whole name where it used to be 5 characters. The floor rises with
       it, from 30% to the 42%
       the geometry now really delivers at 320px - the whole of what a 118px card has left once the
       side dot, the readout and the two gaps have taken theirs.
       The wrapping strip (a finished game, a tablet, landscape) is untouched: it has a second line,
       so it shows the label there. */
    .game-layout.in-play .players-bar .p-card .p-name { min-width: 42%; max-width: none; flex: 1 1 auto; }
    .game-layout.in-play .players-bar .p-card .p-ai { display: none; }
    .game-layout.in-play .players-bar .p-card .p-progress { padding: 0 .1rem; }
    .game-layout.in-play .players-bar .p-card .p-rating { display: none; }
    .game-layout.in-play .board.board-stage { margin: 0 0 .15rem; }
    /* #306: the compact bar cannot pay for the padding above - #314's chrome budget has 2px of slack
       left at 360x531 with the first-move line, so growing this bar puts the controls below the fold.
       At 30px the plate's proportional margin leaves only 19.9px of drawn interior for a 22px label,
       which is why even ENGLISH "Skip!" sat on the border here. So the art is drawn to FILL the box
       instead: sized 1/0.66327 of the height and offset so the drawn rows (17..81 of 98) land exactly
       on [0, H]. The box keeps its 30px, the clearance stops depending on the line count at all, and
       the art's vertical scale (0.46 at this size) comes back into step with its horizontal one
       (0.45) - today it is squashed to 0.31. Re-cutting waitbar.png means re-deriving these two
       numbers; test-plate-label-fit.ps1 measures the band from the art itself and will say so. */
    .game-layout.in-play .board-topbar { min-height: 30px; margin-bottom: .15rem; padding-block: .25rem;
        background-size: 100% 150.77%; background-position: 50% 51.52%; }
    /* Focus mode: the fixed Exit button (top 8px, inline-end 8px, ~70px wide) shares the first row's
       band, so the clocks end before it and lose their side labels to keep the turn text its room. */
    .app.focus-mode .game-layout.in-play > .game-info-top > .clocks { margin-inline-end: 72px; }
    .app.focus-mode .game-layout.in-play > .game-info-top > .clocks .clock .clock-side { display: none; }
    .app.focus-mode .game-layout.in-play > .game-info-top > .first-move-countdown,
    .app.focus-mode .game-layout.in-play > .game-info-top > .pause-countdown { padding-inline-end: 72px; }
}

/* ===== ISSUE 702: finished landscape/square below the wide breakpoint ==========================
   A finished game needs the same board + rail relationship at every landscape aspect, including
   800x360 and an exactly-square 700x700 window. Keep the established 820px rule below for live
   games: their action chrome has a different fold contract. This narrow branch is deliberately
   scoped to `:not(.in-play)` and keeps the established 258px rail floor instead of inventing a
   third column or shrinking the reading below its usable width.

   The Coach answer leads the rail while it exists. Its derived Ready-state reservation is the
   same bounded reading used by the portrait dock; excess localized candidate copy scrolls inside
   `.coach-list`, so short landscape gives the box rather than truncating the string or moving the
   board. The whole-game summary follows it and can scroll below the fold without separating the
   answer from the position it describes. Measured by test-coach-panel-layout.ps1 at land-800,
   land-844 and sq-700, all six catalogues and all four priced board sides. */
@media (max-width: 819px) and (min-aspect-ratio: 1/1) {
    .app:has(.game-layout:not(.in-play)) > .topnav { padding: .3rem 0 .15rem; margin-bottom: .15rem; }
    .app:has(.game-layout:not(.in-play)) > .subnav { justify-content: flex-end; margin: 0 0 .25rem; min-height: 32px; }
    .app:has(.game-layout:not(.in-play)) { padding-block-start: .5rem; }
    .app:has(.game-layout:not(.in-play)) .page-back { margin-block: 0 .15rem; }

    .game-layout:not(.in-play) {
        display: grid;
        grid-template-columns: minmax(0, 1fr) clamp(258px, 30%, 360px);
        grid-template-areas: "board top" "board side" "board fill";
        grid-template-rows: auto auto 1fr;
        gap: 0 1.25rem;
        align-items: start;
        max-width: 100%;
    }
    .game-layout:not(.in-play) > .game-info-top { grid-area: top; }
    .game-layout:not(.in-play) > .game-board-wrap {
        grid-area: board; position: sticky; top: 3.75rem;
        display: flex; justify-content: center; align-items: flex-start;
    }
    .game-layout:not(.in-play)::after { content: ''; grid-area: fill; min-height: 2rem; }
    .game-layout:not(.in-play) > .game-side {
        grid-area: side; min-width: 0; display: flex; flex-direction: column;
    }
    .game-layout:not(.in-play) > .game-info-top,
    .game-layout:not(.in-play) > .game-side {
        margin: 0; box-sizing: border-box;
        background: linear-gradient(180deg, rgba(52,64,82,.42), rgba(28,37,51,.5));
        border: 1px solid rgba(201,162,75,.20); border-radius: 14px; padding: .5rem .7rem;
    }
    .game-layout:not(.in-play) > .game-info-top { border-bottom: 0; border-end-start-radius: 0; border-end-end-radius: 0; padding-bottom: .15rem; }
    .game-layout:not(.in-play) > .game-side { border-top: 0; border-start-start-radius: 0; border-start-end-radius: 0; padding-top: .3rem; }
    .game-layout:not(.in-play) > .game-info-top > .clocks .clock { flex-direction: row; align-items: baseline; gap: .35rem; padding: .15rem .5rem; border-radius: 8px; }
    .game-layout:not(.in-play) > .game-info-top > .clocks .clock .clock-side { font-size: .62rem; }
    .game-layout:not(.in-play) > .game-info-top > .clocks .clock .clock-time { font-size: 1rem; }
    .game-layout:not(.in-play) .result-summary { margin: 0; }

    /* The fixed 258px landscape rail wraps the compact move/status rows at large device text
       scales. The derived state sweep measures a 308px natural maximum at 170% (Hebrew Best move
       played); 12px + 11rem is 311.2px at that scale and keeps every state on one footprint. */
    .game-layout:not(.in-play) > .game-side > .move-analysis {
        min-height: calc(12px + 11rem);
    }

    .game-layout:not(.in-play) > .game-board-wrap { gap: 1rem; }
    .game-layout:not(.in-play) > .game-board-wrap .players-bar { flex-direction: column; justify-content: flex-start; align-self: stretch; margin: 0; flex: 0 0 auto; max-width: 200px; }
    .game-layout:not(.in-play) > .game-board-wrap .players-bar .p-card { flex: 0 0 auto; }
    .game-layout:not(.in-play) > .game-board-wrap .players-bar .p-card.p-right { flex-direction: row-reverse; justify-content: flex-end; }
    .game-layout:not(.in-play) > .game-board-wrap .players-bar .p-card.p-right .p-name { text-align: start; }
    main:has(.game-layout:not(.in-play)) { --page-col: 100%; }

}

/* ── Horizontal (wide / landscape): board centre-left, ONE consolidated side
      column on the inline-end (flips under RTL because it is grid + logical
      source order, not left/right). ── */
@media (min-width: 820px) and (min-aspect-ratio: 1/1) {
    /* Slim, horizontal header that uses the width: the brand row stays compact
       and the account/guest controls move to the inline-end (not centred). */
    .topnav { padding: .3rem 0 .15rem; margin-bottom: .15rem; }
    .subnav { justify-content: flex-end; margin: 0 0 .6rem; }

    .game-layout {
        display: grid;
        grid-template-columns: minmax(0, 1fr) clamp(258px, 30%, 360px);
        grid-template-areas: "board top" "board side" "board fill";
        grid-template-rows: auto auto 1fr;
        gap: .8rem 1.25rem;
        align-items: start;
        max-width: 100%;
    }
    .game-info-top { grid-area: top; }
    .game-board-wrap { grid-area: board; display: flex; justify-content: center; align-items: flex-start; }
    /* #302 (stage/2.3 review, gate 20260824-152652): the board column is SHORT (the 78vh cap below
       keeps the whole board on screen) while the side column is as tall as its cards, and on 844x390
       the page scrolls BY DESIGN (the #193 floor) — so every side-column control below the fold
       (Resign, chat) is reached by scrolling the board off the screen. A resign then ended on a page
       showing neither the board nor the result: the chip vanished under the finger and the screen held
       the move navigator and the chat. Sticky keeps the board — winner bar included — on screen
       through the whole scroll range of its grid area (the area spans all three rows, so it is as tall
       as the side column). The offset is the PINNED .topnav's height (report 46231039: 42px brand
       mark + .3rem/.45rem padding + 1px border = 55px) plus a 5px gap — a smaller offset slides the
       board bar UNDER the bar (measured with .5rem: the winner text was inside the viewport by every
       DOM box and invisible to the player). Focus mode hides the topnav, so it keeps the small gap.
       NOT for the hot-seat panel: its board carries the action chips underneath and can be taller
       than a 390px viewport, and a sticky block taller than the viewport hides its own bottom for
       good. Pinned by GameEndVisibilityTests + GameEndVisibilityE2ETests. */
    .game-board-wrap { position: sticky; top: 3.75rem; }
    .app.focus-mode .game-board-wrap { top: .5rem; }
    .panel-tabbed .game-board-wrap { position: static; }
    /* #312: the vertical layout's viewport cap lives on this element too (see the block above the
       breakpoint); here the stage carries the caps and the wrap must fill its grid column. */
    .game-board-wrap { max-width: none; margin: 0; }
    /* #302: a sticky item stops following the viewport where its grid area ends, and the area used to
       end 2rem (the .app padding) above the page's end — so at the chat input the column was pushed
       up by exactly that. The 2rem moves INSIDE the grid: the `fill` row (which the board area spans)
       carries it, so the area reaches the page's end and the side column keeps its breathing room. */
    .app:has(.game-layout) { padding-bottom: 0; }
    .game-layout::after { content: ''; grid-area: fill; min-height: 2rem; }
    .game-side { grid-area: side; min-width: 0; }

    /* The two side blocks read as one consolidated panel (stacked cards, single column). */
    .game-info-top, .game-side {
        margin: 0; box-sizing: border-box;
        background: linear-gradient(180deg, rgba(52,64,82,.42), rgba(28,37,51,.5));
        border: 1px solid rgba(201,162,75,.20); border-radius: 14px; padding: .85rem 1rem;
    }
    .game-info-top:empty { display: none; }
    .game-side .moves { margin-top: 0; }
    .game-side .game-chat { max-width: none; margin-top: 1rem; }

    /* Board fills its column but is capped by viewport height so it stays fully
       visible (the wait-bar + action chips add ~130px of chrome). */
    .game-board-wrap .board.board-stage { width: 100%; max-width: min(680px, 78vh, calc(100vh - 130px)); margin: 0; }
    /* #302: the third term is what lets the sticky column above keep its promise on a SHORT screen:
       the whole column — sticky top 3.75rem (60px, below the pinned .topnav) + bar 52px + its .4rem
       gap — plus the canvas has to fit in 100vh, i.e. canvas <= 100vh - 118px; the extra 12px is
       breathing room, because a board whose last row sits ON the viewport's bottom edge reads as
       cut off (screenshot review, 2026-08-24). On 844x390 that is a 260px board instead of 304
       (-14%): the price of a board that is whole and uncovered wherever the side column is
       scrolled. 78vh still wins everywhere taller than ~590px, so desktops and tablets are untouched. */

    /* #193: the LOCAL (hot-seat / vs-AI) game carries chrome the online /game view does not — a
       page <h1> plus the Hotseat|vs-AI tab bar (~95px) on top of the nav + wait-bar, so a small
       board (e.g. 4x4) sized only by its column width pushed its last row past the fold on short
       desktops. That chrome is a FIXED pixel stack (~324px to the canvas top), so cap by
       calc(100vh - px), not a vh fraction (which would clip on short screens and waste room on tall
       ones). Focus mode (line below) hides this chrome and keeps its own, more generous cap.
       The max(300px, …) floor is REQUIRED: this same wide/landscape media query also matches SHORT
       landscape (e.g. 844x390 mobile), where calc(100vh - 352px) collapses to ~38px — an unusable
       board. Floor it so short viewports keep a playable board and simply scroll (as they did before
       #193), while tall viewports still get the fold-fit cap. */
    .panel-tabbed .game-board-wrap .board.board-stage { max-width: min(680px, max(300px, calc(100vh - 352px))); }

    /* #65 (report 08fbc0d7): in the wide layout the players form a vertical rail BESIDE the board —
       first mover above the second — instead of a cramped row with dead space below it. Mobile keeps
       the horizontal strip above the board.
       Issue 491 (2.16 screenshot review, flags on `vs-ai-start-after` and `live-order-after`): the
       rail used to be `justify-content: space-between`, which pins the two seats to the rail's two
       ENDS - and `align-self: stretch` makes the rail as tall as the BOARD. This block deliberately
       lets the board exceed a short viewport (the cap above keeps a 300px playable board and the
       page scrolls; `position: sticky` rescues the board after one scroll), so tying a seat to the
       board's bottom edge handed the SECOND SEAT the same scroll. Measured by
       test-board-fold-layout.ps1 at 844x390 - the playbook's own landscape persona, and a real
       landscape phone: the left seat sits at 177..234 and the right one at 439..496, i.e. 106px
       ENTIRELY below a 390px fold, with the identical shape at 900x400. Two independent review
       windows read that as "the AI seat is absent from the frame" and "second mover's card missing
       from the players-bar", and both were right: the card is rendered, Playwright-visible, and off
       the user's screen.
       The seats are IDENTITY chrome - who is playing - so neither may cost a scroll. They stack at
       the rail's start instead, one .6rem gap apart (`.players-bar`'s own gap, read as a row-gap in
       a column), which keeps the first-above-second reading and makes both seats' position depend
       on the rail's TOP rather than on the board's height. That is a fix by construction, not by a
       viewport threshold: no matter how tall the board grows, the second seat stays one card below
       the first. The rail still stretches, so the board keeps the column beside it. */
    .game-board-wrap { gap: 1rem; }
    .game-board-wrap .players-bar { flex-direction: column; justify-content: flex-start; align-self: stretch; margin: 0; flex: 0 0 auto; max-width: 200px; }
    .game-board-wrap .players-bar .p-card { flex: 0 0 auto; }
    .game-board-wrap .players-bar .p-card.p-right { flex-direction: row-reverse; justify-content: flex-end; }
    /* #278: in the rail the right card is packed toward its LEFT edge with the dot first, so its name
       hugs that dot from the start side - the mobile strip's end-alignment would push it away. */
    .game-board-wrap .players-bar .p-card.p-right .p-name { text-align: start; }

    /* The hot-seat game lives inside a narrow (460px) .panel card meant for the
       setup pickers — let it grow wide once it holds the board, so the board is
       the large central element. Only the game panel matches (not Find/Create). */
    .panel-tabbed:has(.game-layout) { max-width: none; }
    /* #447: and with it goes the page COLUMN. The hot-seat <h1> titles that panel, so where the
       panel is the whole shell the heading is flush with the shell too - the same alignment, read
       off a different column width. Everywhere else the column is the cap (see the #447 block at
       the end of this sheet). */
    main:has(.game-layout) { --page-col: 100%; }

    /* == ISSUE 533, INSTANCE 3 — THE REVIEW NAVIGATOR IS NOT ONE OF THE READINGS IT DRIVES =======
       Stage 2.18 screenshot review (runId 20260912-122114, shot 3390-g11r0-h), `review-return-to-game`
       at 844x390 and scroll 0,0: the frame showed the finished game's result and the top of its
       board, and no review controls at all. The step's own declared subject — `.rev-btn`, the same
       selector Scenario.cs asserts — was MEASURED at y=615..670 against a 390px frame, i.e. 225px
       below it: rendered, Playwright-visible, and off the user's screen. Reproduced at the identical
       geometry in run 20260912-012522. Same instrument gap as #491 and as this issue's first two
       instances: `ToBeVisibleAsync` is a DOM fact and is true of an element scrolled off screen.
       WHY IT WAS LATE. In this grid the side rail is a COLUMN beside a sticky board, and a finished
       game's rail opened with the move analysis (#554) and the best-move toggle (#556) — two
       READINGS OF THE POSITION the review row selects — so the only control on the surface came
       after them. A reading's height has no bound: the verdict wraps, the quality chip and the loss
       ride under it, and the longest catalogue this repo ships adds a line to each. The navigator's
       distance from the rail's top therefore grew with the text of the thing it controls.
       THE RULE, and it is construction rather than a threshold: THE NAVIGATOR IS NOT A READING. The
       review row chooses the position every card in this rail describes, so it LEADS them, and its
       offset now depends on the rail's TOP instead of on how tall the reading above it grew — the
       same shape as #491's seats stacking at their rail's start rather than at the board's bottom
       edge. Inside the row the four step controls are the navigation itself, so they take the row's
       first line and Autoplay — a mode over them — follows.
       The stacked (narrow) layout is untouched: there the rail follows the board in ONE column and
       "immediately under the board" is exactly what #554's reading is for. Only the rail-beside-the-
       board arrangement has a first screenful that can end before its own control.
       Measured by Application\test-section-fold-layout.ps1 (the real sheet, the real markup out of
       Game.razor, real 844x390 frames) and by the in-run subject-fold audit on every declaring step. */
    .game-layout:not(.in-play) > .game-side { display: flex; flex-direction: column; }
    .game-layout:not(.in-play) > .game-side > .review-row { order: -1; }
    .game-layout:not(.in-play) > .game-side > .review-row > .rev-steps { order: -1; }
    /* `.toggle-row`'s 0.75rem top margin separates it from a row ABOVE it; leading the rail there is
       no row above it, and the card's own padding already holds it off the border. */
    .game-layout:not(.in-play) > .game-side > .review-row { margin-top: 0; }

    /* Focus mode, horizontal: give the board more room, slimmer side column. */
    .app.focus-mode .game-layout { grid-template-columns: minmax(0, 1fr) clamp(240px, 24vw, 320px); }
    .app.focus-mode .game-board-wrap .board.board-stage { max-width: min(1000px, calc(100vh - 130px)); }
}

/* == ISSUE 533, INSTANCE 3, the height half ======================================================
   Ordering alone answers every window tall enough to hold the rail's first card; a 390px one is not.
   At 844x390 this grid spends 160px on page chrome before the rail starts and then gives the rail a
   258px column, in which the result heading and the sides line each take two lines of the Russian
   catalogue — so the rail's own top was at 451 in a 390px frame and NOTHING in it could be above the
   fold, whatever its order. Two things are simply spare here, and neither costs the user a word:
     * WIDTH. In this media query the board is capped by HEIGHT (#302: min(680px, 78vh, 100vh-130px)
       — 260px at 844x390), so its column holds a 260px board in 534px of column: 274px of dead
       space beside it, on the one screen that has no height to spare. The rail is where the height
       is, so it takes that width back and its cards stop wrapping to pay for it.
     * A FINISHED GAME'S CLOCK IS A RECORD, not a live readout. It keeps every digit and gives up the
       stacked pill the running clock wears — the identical compaction the #314 in-play block applies
       on a portrait phone, one surface out.
   Both are height given back, not information removed, which is what separates this from the
   viewport threshold #491 ruled out: the ORDER above is what keeps the navigator at the rail's top
   as the rail's content grows, and this block is what gets the rail's top into the frame at all. */
@media (min-width: 820px) and (min-aspect-ratio: 1/1) and (max-height: 480px) { /* ui-space-value: the 480px is a media CONDITION, not a box - this query's whole content gives height back (the sheet's existing short-window block uses the same line) */
    .game-layout { grid-template-columns: minmax(0, 1fr) clamp(258px, 44%, 420px); row-gap: 0; }
    .game-info-top, .game-side { padding: .5rem .7rem; }
    /* The page's own rhythm above and inside the layout, on the same terms as the #533 heading block
       below: margins only, no type scale moves, so nothing gets smaller and nothing re-wraps — the
       blocks simply sit closer together on a window that has 390px to spend. */
    .app:has(.game-layout) { padding-block-start: .5rem; }
    .app:has(.game-layout) .subnav { margin-bottom: .25rem; }
    .app:has(.game-layout) .page-back { margin-block: 0 .15rem; }
    .app:has(.game-layout) .subnav { min-height: 32px; }
    .game-layout .result-summary { margin: 0; }
    /* And the two rail cards stop being two. The block that styles them says they "read as one
       consolidated panel (stacked cards, single column)" already - on a short window they simply
       ARE one, which gives back the row gap, one card's inner padding pair and one pair of borders
       without removing a single line of content. */
    .game-layout > .game-info-top { border-bottom: 0; border-end-start-radius: 0; border-end-end-radius: 0; padding-bottom: .15rem; }
    .game-layout > .game-side { border-top: 0; border-start-start-radius: 0; border-start-end-radius: 0; padding-top: .3rem; }
    .game-layout:not(.in-play) > .game-info-top > .clocks .clock { flex-direction: row; align-items: baseline;
        gap: .35rem; padding: .15rem .5rem; border-radius: 8px; }
    .game-layout:not(.in-play) > .game-info-top > .clocks .clock .clock-side { font-size: .62rem; }
    .game-layout:not(.in-play) > .game-info-top > .clocks .clock .clock-time { font-size: 1rem; }
}

/* == ISSUE 621 — THE SAME CLASS AT THE PORTRAIT PERSONA, AND A DIFFERENT CAUSE ====================
   Full gate 20260912-122114 fired the subject-fold detector on `.rev-btn` FOUR times at the 390x844
   portrait persona, every one at scroll 0,0: 40..104 x 1005..1060, 40..104 x 1004..1059,
   40..104 x 1022..1077 and 286..350 x 1005..1060. The FOURTH is what sets the rule here — it is a
   different column on the same 390-wide frame, so there is more than one review control below the
   fold and what has to come into the picture is the ROW, not a button.
   WHY THE LANDSCAPE FIX STRUCTURALLY CANNOT REACH IT. Every rule of it lives inside
   (min-width: 820px) and (min-aspect-ratio: 1/1) and (max-height: 480px) — ui-space-value: these
   are #533's media CONDITIONS quoted verbatim so the claim can be checked against them, not boxes
   this rule reserves; nothing here declares any spacing. A portrait phone
   cannot match that query — deliberately, and the probe proves it rather than arguing it: the finished-game
   page rendered at 390x844 before and after 2.19.21 is a byte-identical PNG. Landscape was an
   ORDERING problem in a two-column grid. Portrait is a BUDGET problem in ONE column, measured with
   test-section-fold-layout.ps1 -Report over the Russian catalogue: 450px of page chrome and record
   card before the canvas starts, a 350px width-bound board, then the rail — and the navigator at
   1051..1106 in an 844px frame, 0px of a 55px control on screen.
   SO THE ANSWER IS TWO BLOCKS, and they are deliberately not one, because they have different
   scopes and only one of them is bounded by another contract. THIS block is the BUDGET half. The
   ORDERING half — #533's rule generalised out of the wide grid — is the block immediately below,
   with the height condition its own comment derives; between them `.rev-btn` goes from 1051..1106
   to 749..804 in an 844px frame. The fold probe judges that subject at all five of its case's
   viewports now, instead of the four landscape ones.
   AND THE BUDGET GIVES, on #533's terms rather than by trimming copy. A finished game's clock is a
   RECORD, not a live readout, so it drops the stacked pill and keeps every digit — the identical
   compaction the #314 in-play block applies on this very viewport, one game-state out; and the page
   rhythm above the layout tightens on MARGINS ONLY. Nothing gets smaller, nothing re-wraps, and no
   string is shortened. The budget half is not decoration: ordering ALONE puts the row's top at 816
   in an 844px frame — 28 of 55px, a pass by arithmetic that the next line of copy takes away again.
   This half is safe at EVERY portrait phone and applies at every one of them: it moves nothing and
   changes no span, it only spends less height on the same content.
   SCOPE is the #314 block's complement in state and its twin in viewport: portrait phones, and only
   a FINISHED game. While a game is in play that block already owns this screen, and its budget has
   2px of slack, so nothing here may reach it.
   Measured by Application\test-section-fold-layout.ps1 (case `review` at 390x844, the real sheet
   over markup read out of Game.razor) and by the in-run subject-fold audit on every declaring step. */
@media (max-width: 599px) and (orientation: portrait) {
    /* The record card, on the #314 terms: one line per seat, every digit kept. */
    .game-layout:not(.in-play) > .game-info-top > .clocks { margin-block: .25rem .35rem; }
    .game-layout:not(.in-play) > .game-info-top > .clocks .clock { flex-direction: row; align-items: baseline;
        gap: .35rem; padding: .15rem .5rem; border-radius: 8px; }
    .game-layout:not(.in-play) > .game-info-top > .clocks .clock .clock-side { font-size: .62rem; }
    .game-layout:not(.in-play) > .game-info-top > .clocks .clock .clock-time { font-size: 1rem; }
    /* The page's own rhythm above the layout — margins and one min-height, no type scale moves. */
    .app:has(.game-layout:not(.in-play)) { padding-block-start: .5rem; }
    .app:has(.game-layout:not(.in-play)) .subnav { margin-bottom: .25rem; min-height: 32px; }
    .app:has(.game-layout:not(.in-play)) .page-back { margin-block: 0 .15rem; }
    .game-layout:not(.in-play) .result-summary { margin: 0 0 .25rem; }
}

/* == ISSUE 621, the ordering half — AND THE ONE CONTRACT THAT DECIDES ITS SCOPE ===================
   THE RULE IS #533'S, UNCHANGED: THE NAVIGATOR IS NOT A READING. The review row chooses the position
   every card in this rail describes, so it LEADS them — and here it does so in the STACKED layout,
   which is the thing this block adds to the sheet. Inside the row the four step controls are the
   navigation itself, so they take the row's first line and Autoplay — a mode over them — follows.
   At 390x844 that carries `.rev-btn` from 1051..1106 to 749..804: the whole 55px control inside the
   frame at scroll 0,0, with 40px of slack (Russian, the longest copy this repo ships).
   WHY THIS HAS A HEIGHT CONDITION WHEN #533'S ORDERING HALF DID NOT, and it is a CONSTRUCTION
   argument rather than a threshold. In one column the review row is not a 55px strip: at 390px wide
   it wraps to about 210px, because it carries Autoplay, the four steps, the counter, Share position,
   Save as position and the coach-mode chip. Leading the rail with it therefore pushes `.move-analysis`
   down by that whole height — and ISSUE 554's shipped contract is a SPAN,
   `BoardDriver.FrameTogetherAsync(.board-canvas, .move-analysis)`, which its gate step drives at
   360x640 (Scenario.cs: the analyst actor is created 360x640). Measured on this sheet: at 360x640 the
   reorder takes that span from 483px to 760px against 577px of usable frame below the sticky topnav —
   it would make a shipped owner requirement ("the user must not need to scroll between the board and
   the move analysis") false, and take the gate red with it. So the reorder applies only where the
   first frame can hold the thing being reordered INTO it: 383px of chrome and record card + a
   width-bound board (350px at 390 CSS px) + the 55px navigator = 788px, and 800px is the first round
   number above that. Below it nothing in the rail is in the first frame whatever its order — at
   360x640 the BOARD itself ends at 703 in a 640px frame — so there is no fold question to answer
   there, only #554's adjacency, and that is left exactly as it shipped. The two portrait personas the
   playbook drives that clear the condition are 390x844 and 412x915.
   AT 390x844 ISSUE 554 IS RE-EXPRESSED, NOT WEAKENED, and the re-expression is measured rather than
   asserted. Its contract constrains the SPAN, not the order — nothing in it says a control may not
   sit between the two — and putting the navigator there makes that span LONGER, so the contract gets
   HARDER to satisfy, never easier: 504px before this block, 750px after, against 781px of usable
   frame. That is 31px of headroom in the longest catalogue shipped, which is thin ON PURPOSE and
   held: `test-section-fold-layout.ps1` measures that span at both stacked viewports, so a locale or a
   card that spends it reddens the probe instead of surfacing as a gate failure nobody attributed.
   What the reader gets is the order the screen's own job asks for — the board, the control that
   moves it, and the reading of the move that control just produced — with the analysis still
   immediately adjacent to the board and still visible while navigating, which is what issue 554's
   own words require. */
@media (max-width: 599px) and (orientation: portrait) and (min-height: 800px) { /* ui-space-value: the 800px is a media CONDITION, not a box - it is the frame this block's own arrangement needs to exist in (chrome 383 + board 350 + navigator 55 = 788) */
    .game-layout:not(.in-play) > .game-side { display: flex; flex-direction: column; }
    .game-layout:not(.in-play) > .game-side > .review-row { order: -1; }
    .game-layout:not(.in-play) > .game-side > .review-row > .rev-steps { order: -1; }
    /* `.toggle-row`'s top margin separates it from a row ABOVE it; leading the rail there is no row
       above it, and the board's own bottom margin already holds it off the canvas. */
    .game-layout:not(.in-play) > .game-side > .review-row { margin-top: 0; }
}

/* ── Game Focus Mode — app-level, CSS only. NOT the Fullscreen API: there is no
      requestFullscreen / window expansion / OS-chrome hiding anywhere. MainLayout
      toggles the .focus-mode class on .app from FocusModeState; the same
      .game-layout media query above keeps focus mode itself responsive. ── */
.focus-exit { display: none; }
.app.focus-mode > .topnav,
.app.focus-mode > .subnav,
.app.focus-mode .ad-slot,
.app.focus-mode main > h1,                     /* page heading (e.g. hot-seat "Play local") */
.app.focus-mode .home-note,                    /* hot-seat "Back" home link */
.app.focus-mode .game-page > p:first-child,    /* back-to-games link */
.app.focus-mode .game-page > .lede,            /* hot-seat lede */
.app.focus-mode .game-chat,                    /* chat is non-essential during focus */
.app.focus-mode .focus-enter-chip {            /* already in focus — hide the enter button */
    display: none !important;
}
/* C18 (#77 class): the fullscreen chip's icon+label wrapped mid-pill in RU — chips are
   single-line by design. Where the un-wrapped label would instead widen the action rail past
   the viewport (long locales on phones, C1), the chip degrades to icon-only: the ⛶ glyph plus
   title/aria carry the meaning. */
.focus-enter-chip { white-space: nowrap; }
/* Issue 495: the capture chip is the same shape of control, so it degrades the same way — icon-only
   where the un-wrapped label would widen the action rail past the viewport. */
.capture-enter-chip { white-space: nowrap; }
/* Issue 532, the floor under `.toggle-row`'s wrap: wrapping stops two chips from pushing each other
   out of the row, but a chip ALONE on its line must not outgrow the line either. A flex item's
   automatic minimum size is its min-content width, and for a nowrap chip that is whatever its
   translation happens to measure - which is what pinned these two at their text's width and made
   them paint outside. `min-width: 0` releases that floor and `max-width: 100%` caps the box at the
   row, so the ellipsis lands on the LABEL and never on the glyph: the degrade is the one C18
   already declares for this control - the icon plus title/aria carry the meaning - reached by
   running out of ROOM instead of by crossing a width somebody guessed. Note which way round the
   media query below is: at 900px and under the row is the full page width, and it is the DESKTOP
   grid (`@media (min-width: 820px) and (min-aspect-ratio: 1/1)`) that squeezes it into a 258-360px
   side column, so a viewport threshold could never have been the guard here. */
.focus-enter-chip, .capture-enter-chip { max-width: 100%; min-width: 0; }
.focus-enter-label, .capture-enter-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* Issue 532, and the reason the wrap alone was not enough: these chips wear `.cta`, which is
   `width: 100%` because "a .cta IS its own row" (#385). In the side rail that width was never what
   the row wanted - the comment on the #314 chrome budget calls this "the Full screen / Resign row
   ... at chip size (not .cta size)" - it was simply invisible, because a nowrap flex row shrank
   both chips back down to their text and then overflowed. Wrap them without this and they stop
   overflowing by becoming two full-width pills instead, which is a different control. The phone
   block already spells `width: auto` for exactly this row; it belongs at every width, not below
   599px, so the row reads the same everywhere and only the LINE it breaks on changes. */
.game-side > .toggle-row:not(.review-row) > .chip.cta { width: auto; }
/* Issue 532, found by LOOKING at the fixed row rather than by measuring it - two reviewers of the
   1280x900 and 901x900 shots independently read the leading glyph as fused to the first letter
   (1-2px of ink between them, against 5px for the space inside "Full screen"). The markup does
   write a space - `<glyph> <span class="...-label">` - but `.cta` makes the chip a FLEX container,
   and a flex container drops the whitespace between two items, so that space has never rendered.
   `gap` is the flex-box way to say it and it costs nothing when the label is hidden below 900px:
   with one item left there is no gap to draw, so the icon-only degrade is untouched. */
.focus-enter-chip, .capture-enter-chip { gap: .35rem; }
@media (max-width: 900px) { .focus-enter-label, .capture-enter-label { display: none; } }
.app.focus-mode { max-width: 100%; padding: .5rem .65rem 1rem; }
/* Shed the hot-seat setup card in focus so the board gets the whole area. */
.app.focus-mode .panel-tabbed:has(.game-layout) {
    max-width: none; background: none; border: none; box-shadow: none;
    padding: 0; -webkit-backdrop-filter: none; backdrop-filter: none;
}
/* Issue 495: ONE exit control for BOTH modes — the capture layout is a sibling of focus mode, not a
   mode with an exit of its own. */
.app.focus-mode .focus-exit,
.app.capture-mode .focus-exit {
    display: inline-flex; align-items: center; justify-content: center; gap: .3rem;
    position: fixed; top: 8px; inset-inline-end: 8px; z-index: 300;
    padding: .42rem .8rem; min-height: 36px; border-radius: 10px; cursor: pointer;
    background: rgba(20,26,38,.86); border-image: none; border: 1px solid rgba(255,255,255,.20);
    color: #eef2f8; font-size: .9rem; font-weight: 700;
}
.app.focus-mode .focus-exit:hover, .app.capture-mode .focus-exit:hover { background: rgba(30,38,52,.96); }
/* In the capture layout it is OUT of the frame until it is wanted: an Exit button is chrome the
   VIEWER of a stream does not need, and the streamer gets it back on hover or on keyboard focus.
   Opacity only — the button never moves, so revealing it cannot shift the frame. */
.app.capture-mode .focus-exit { opacity: 0; transition: opacity .15s; }
.app.capture-mode .focus-exit:hover, .app.capture-mode .focus-exit:focus-visible { opacity: 1; }

/* -- Capture layout (issue 495, roadmap track E "watch and share") - a DECLARED SIBLING of focus
      mode, not a second mechanism beside it. Both classes come off ONE field (FocusModeState.Mode),
      so .app is never wearing both, and MainLayout's single .focus-exit button leaves whichever is
      on. App-level and CSS-only for the same reason focus mode is, and one more: capture software
      frames a WINDOW REGION, so a page that grabbed OS fullscreen would fight the tool capturing it.

      WHAT THIS IS FOR. Lichess "simply puts the names and clocks around the board so that a capture
      frames correctly". The compaction a capture wants was already written - the #314 block above
      collapses the page to board-plus-essentials - but it is keyed on a PORTRAIT PHONE media query,
      so it can never fire at 1280x720 or 1920x1080, which is where a stream is captured. This block
      makes that reachable BY INTENT: the same mechanism (display: contents over the two info
      subtrees, then `order` across them) at any width, keyed on the viewer's chosen mode.

      IT IS ITS OWN MARKER. `in-play` still means one thing and only one thing - the game is not over
      - and this needed a different condition, so it took `capture-frame` rather than widening that
      one. The two are ANDed in Game.razor: a finished game gets the full page back, because the
      post-game screen is where the result, the rating line and the share affordance live.

      THE FRAME IS STABLE BY CONSTRUCTION, which is the whole point - a region that shifts under
      capture is worse than no region. Its geometry is a function of the VIEWPORT ALONE and of
      nothing inside it:
        * the frame's height is --cap-h and its width min(100%, --cap-side), both derived from 100vh;
        * the names row and the clocks row are `flex: 0 0 --cap-band` - a fixed band each, whatever
          they hold - and the board takes what is left (`flex: 1 1 auto`), so a taller or shorter
          occupant of the middle cannot move either band;
        * each seat and each clock is `flex: 0 0 calc(50% - 1.8rem)`, so the #245 watcher badge
          appearing between them, a name growing, the #278 readout going from 0(+0) to 12(+3), or a
          clock crossing 10:00 -> 9:59 -> 9.9 -> 1:00:00 cannot move anything: the clock is
          space-between inside a box it cannot resize, so only the digits' own edge moves;
        * everything that can appear or disappear mid-game is OUT of the frame, by a CATCH-ALL rather
          than by a list - every child of .game-info-top and .game-side is display: none and exactly
          three members are shown again, so a control added to this page tomorrow is outside the
          frame by default and has to be argued in.

      THE ROLE IT SHIPS FOR. The chip that enters this is offered to a SPECTATOR (Game.razor), which
      is the roadmap item's own case - a commentator framing someone else's game - and the only role
      whose chrome the frame can drop in full without hiding something actionable: chat, resign and
      the pause bar are ABSENT on that path, not disabled. The rules below are role-blind on purpose,
      so the day a player's own capture layout is argued through, the frame it lands in is this one.

      The top nav STAYS (slimmed to the #314 numbers): the brand mark in the corner is the only mark
      this frame carries, and it is why it needs no watermark or overlay. Pinned by
      Application\test-capture-frame-layout.ps1 (the real stylesheet laid out at capture and phone
      viewports in headless Chromium: the frame's rect identical across a move landing, a clock digit
      change, a watcher badge and a chat notification) and by CaptureLayoutTests. -- */
.app.capture-mode {
    /* The two fixed bands, and the frame the viewport allows. --cap-chrome is what the frame spends
       on everything that is not the board: 2x44 band + 52 skip bar + its .4rem margin + the gaps. */
    --cap-band: 44px;
    --cap-chrome: 160px;
    --cap-h: calc(100vh - 62px);
    --cap-side: clamp(240px, calc(var(--cap-h) - var(--cap-chrome)), 680px);
    max-width: 100%; padding: .25rem .65rem .5rem;
}
/* The #314 slimming of the brand row, reached here by INTENT instead of by viewport. */
.app.capture-mode > .topnav { padding: .1rem 0; margin-bottom: .15rem; }
.app.capture-mode > .topnav .brand .brand-mark { height: 34px; width: 34px; }
/* What the frame sheds. Focus mode's list minus the top nav (the brand mark is the frame's only
   mark), plus the page furniture the game view carries outside .game-layout. Everything INSIDE the
   layout is handled by the catch-all below rather than by name. */
.app.capture-mode > .subnav,
.app.capture-mode .ad-slot,
.app.capture-mode main > h1,
.app.capture-mode main > .tabs,
.app.capture-mode .home-note,
.app.capture-mode .game-page > p:first-child,
.app.capture-mode .game-page > .lede,
.app.capture-mode .game-page > .chat-alert {
    display: none !important;
}

/* The frame. A flex column whose height is the viewport's, with the middle row taking the slack -
   so the two bands sit at fixed offsets no matter what the board does. Overrides the wide layout's
   grid (0,2,0 beats its 0,1,0) and its `fill` pseudo-row with it. */
.game-layout.capture-frame {
    display: flex; flex-direction: column; align-items: stretch; gap: .4rem;
    width: min(100%, var(--cap-side)); margin-inline: auto;
    height: var(--cap-h); min-height: calc(240px + var(--cap-chrome));
}
.game-layout.capture-frame::after { display: none; }
/* The same mechanism #314 uses: the two info subtrees stop being boxes, so their children lay out as
   the frame's own items and can be ORDERED across them (names above the board, clocks below it). */
.game-layout.capture-frame > .game-info-top,
.game-layout.capture-frame > .game-side,
.game-layout.capture-frame > .game-board-wrap { display: contents; }
/* THE CATCH-ALL. Nothing in the frame renders unless a rule below names it. This is what makes
   "everything a viewer needs and nothing they do not" survive the next control somebody adds. */
.game-layout.capture-frame > .game-info-top > *,
.game-layout.capture-frame > .game-side > *,
.game-layout.capture-frame > .game-board-wrap > * { display: none; }

/* MEMBER 1 - the names, in a fixed band above the board. Each seat is exactly half the frame less
   the centre the #245 watcher badge lives in, so the badge arriving cannot move either seat. */
.game-layout.capture-frame > .game-board-wrap > .players-bar {
    display: flex; order: 1; flex: 0 0 var(--cap-band); flex-direction: row;
    width: 100%; max-width: none; margin: 0; gap: .6rem;
    align-items: stretch; justify-content: space-between; align-self: auto;
}
.game-layout.capture-frame > .game-board-wrap > .players-bar > .p-card,
.game-layout.capture-frame > .game-board-wrap > .players-bar > .p-card.p-right {
    flex: 0 0 calc(50% - 1.8rem); max-width: none; min-width: 0;
    flex-wrap: nowrap; flex-direction: row; align-items: center; gap: .4rem; box-sizing: border-box;
}
.game-layout.capture-frame > .game-board-wrap > .players-bar > .p-card.p-right { justify-content: flex-end; }
/* Issue 403's rule, one surface over: on a ONE-LINE card that cannot grow, IDENTITY OUTRANKS THE
   METADATA THAT DESCRIBES IT. Measured at 1280x720 - the commonest capture resolution - the frame is
   498px, so a seat is 214px, and the rating and the engine label took 110px of it: "Blue Player"
   rendered as "Blue Pla...". The rating and the engine label leave (#314's phone card drops the same
   two, for the same reason); the side dot, the name and the #278 readout stay. Nothing is traded
   away that the frame is for: a viewer reads WHO is playing and HOW the position stands. */
.game-layout.capture-frame > .game-board-wrap > .players-bar > .p-card .p-rating,
.game-layout.capture-frame > .game-board-wrap > .players-bar > .p-card .p-ai { display: none; }
.game-layout.capture-frame > .game-board-wrap > .players-bar > .p-card .p-progress { flex: 0 0 auto; }
.game-layout.capture-frame > .game-board-wrap > .players-bar > .p-card .p-name {
    min-width: 0; max-width: none; flex: 1 1 auto;
}

/* MEMBER 2 - the board, in the slack between the two bands, centred in it. */
.game-layout.capture-frame > .game-board-wrap > .board.board-stage,
.game-layout.capture-frame > .game-board-wrap > .board-paused {
    display: flex; flex-direction: column; justify-content: center;
    order: 2; flex: 1 1 auto; min-height: 0;
    width: 100%; max-width: none; margin: 0;
}

/* MEMBER 3 - the clocks, in a fixed band below the board, each one under the seat it belongs to
   (same basis as the cards above). space-between pins the side label to the band's start edge and
   the time to its end, so a digit crossing only moves its own edge. */
.game-layout.capture-frame > .game-info-top > .clocks {
    display: flex; order: 3; flex: 0 0 var(--cap-band);
    width: 100%; max-width: none; margin: 0; gap: .6rem;
    align-items: stretch; justify-content: space-between;
}
.game-layout.capture-frame > .game-info-top > .clocks > .clock {
    flex: 0 0 calc(50% - 1.8rem); min-width: 0; box-sizing: border-box;
    flex-direction: row; align-items: center; justify-content: space-between;
    gap: .5rem; padding: .2rem .7rem; border-radius: 10px;
}
.game-layout.capture-frame > .game-info-top > .clocks > .clock .clock-side { flex: 0 0 auto; }
.game-layout.capture-frame > .game-info-top > .clocks > .clock .clock-time { flex: 0 0 auto; font-size: 1.25rem; }

.about-meta { color: var(--ink-dim); font-size: 0.8rem; margin: 0.4rem 0 0; }
/* #647: two rules used to sit here restating the base anchor colour for this ONE paragraph,
   because report 40d7ea73 was read as a fact about the legal links rather than about every link
   outside <main>. Feedback c85ac959 then found the third anchor in the same dialog - the Release
   Notes link, one <p> above and outside .about-legal - still on the UA blue. The colour is now
   stated once, bare, beside `main` at the top of this sheet, and a restatement here could only go
   stale against it. `about-legal` stays on the markup: it is what Scenario.cs's `about-open` step
   selects on to read the computed colour. */

/* Icon-only chips (share / QR / add-friend — reports 164d0312, 18216ca6, b8f8b06e). */
.icon-btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.25rem 0.55rem; }
.share-ico { width: 17px; height: 17px; display: block; }

/* Account-required lock panel. `panel`'s max-width is otherwise a CONTENT width, so at a 320px
   viewport its inline padding made the guest explanation wider than the page. This panel owns the
   available border box; both prose and the sign-in control yield to it for any future translation. */
.guest-lock { width: 100%; min-width: 0; box-sizing: border-box; text-align: center; overflow-wrap: anywhere; }
.guest-lock .navpill { display: inline-flex; max-width: 100%; white-space: normal; }

/* #626: the account INVITATION, which is the opposite of the lock above and must not be mistaken for
   it. A guest reads a tournament and watches its rotation; this sits above that content and offers an
   account for the things an account is for, so it never takes the whole screen and never centres like
   a wall. Same 360px discipline as .guest-lock - the BOX is constrained and the sentence is never
   shortened - so the prose takes the lines it needs and the pill drops to its own line rather than
   pushing past the panel's border (#275/#306/#380). */
.watch-cta { width: 100%; min-width: 0; box-sizing: border-box; overflow-wrap: anywhere;
    display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.watch-cta p { margin: 0; flex: 1 1 12rem; min-width: 0; }
.watch-cta .navpill { display: inline-flex; max-width: 100%; white-space: normal; }

/* Sign-in methods section (account linking): label/value rows + inline password form. */
.collapsible-head { cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.collapse-caret { font-size: 0.8em; opacity: 0.7; }
.signin-methods .signin-row { display: flex; align-items: center; gap: 0.6rem; margin: 0.35rem 0; flex-wrap: wrap; }
.signin-methods .signin-label { color: var(--ink-dim); min-width: 8rem; }
.signin-methods .signin-val { display: inline-flex; align-items: center; gap: 0.4rem; word-break: break-word; }
.signin-methods .signin-actions { margin-top: 0.6rem; }
.signin-pw-form { margin-top: 0.6rem; }
.signin-pw-form input { width: 100%; box-sizing: border-box; }

/* Issue 511 (#504 slice C surface): the account's own activity-report download.
   The BOX gives, never the string. A zone id ("America/Argentina/ComodRivadavia" is a real one) and
   two localized date labels are the shape feedback f7d77400 (#257) reported at 360px, so every
   control here keeps a full-width row of its own inside .field - the sheet's inputs are already
   width:100%;box-sizing:border-box - and the only row that holds more than one thing WRAPS.
   Measured by Application\test-activity-report-layout.ps1 at 360px in the longest language. */
.activity-report .ar-scope { display: flex; flex-wrap: wrap; gap: .35rem 1rem; }
.activity-report .ar-choice { display: inline-flex; align-items: center; gap: .4rem; color: var(--ink-dim); font-size: .92rem; }
.activity-report .ar-choice input { width: auto; flex: 0 0 auto; margin: 0; }
/* The zone hint and the saved-file line: both hold an unbroken token (an IANA id, a file name), and
   an unbroken token is the one thing a narrow column cannot wrap by itself. */
.activity-report .ar-hint { margin: .3rem 0 0; font-size: .82rem; overflow-wrap: anywhere; word-break: break-word; }

/* Games-list finished rows: result icon + rating change */
.row-card .rc-result { flex: 0 0 auto; width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center; border-radius: 8px; font-weight: 900; font-size: 1.15rem; }
.rc-result.res-win { background: rgba(46,125,50,.18); color: #6ed88a; }
.rc-result.res-loss { background: rgba(200,60,60,.16); color: #ff8a8a; }
.rc-result.res-abort { background: rgba(255,255,255,.08); color: var(--ink-dim); }
.rc-rating { font-variant-numeric: tabular-nums; font-weight: 700; white-space: nowrap; }
.rc-rating.up { color: #6ed88a; }
.rc-rating.down { color: #ff8a8a; }

/* Profile picture upload */
.icon-upload { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; }
.icon-preview { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; border: 2px solid rgba(201,162,75,.4); }

/* ── /rules: full illustrated rules page ─────────────────────────────────────────────────────────── */
.rules-page { max-width: 760px; margin-inline: auto; }  /* #311 */
.rules-lede { margin-top: -0.15rem; }
.rules-sec { margin: 1.7rem 0; }
.rules-sec > h2 { margin: 0 0 .5rem; font-size: 1.25rem; }
.rules-sub { margin: 1.15rem 0 .35rem; font-weight: 700; color: var(--ink); font-size: 1.02rem; }
.rules-fig { margin: .95rem 0; text-align: center; }
.rules-fig svg, .rules-figrow svg, .rules-flow svg { max-width: 100%; height: auto; }
.rules-cap { color: var(--ink-dim); font-size: .85rem; margin: .4rem 0 0; }
.rules-legend { display: flex; flex-wrap: wrap; gap: .35rem 1rem; justify-content: center; margin: .55rem 0 0; color: var(--ink-dim); font-size: .85rem; }
.rules-legend span { display: inline-flex; align-items: center; gap: .4rem; }
.rules-actions { margin: .45rem 0 .5rem; padding-inline-start: 1.25rem; }
.rules-actions li { margin: .2rem 0; }
.rules-tag { display: inline-block; padding: .12rem .5rem; border-radius: 999px; font-size: .78rem; font-weight: 800; letter-spacing: .3px; }
.rules-tag.legal { background: rgba(46,125,50,.2); color: #6ed88a; }
.rules-tag.illegal { background: rgba(200,60,60,.18); color: #ff8a8a; }
/* formal-distance formula (kept LTR even under RTL) */
.rules-formula { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .5rem;
    direction: ltr; font-size: 1.12rem; margin: .55rem 0 .85rem; color: var(--ink); }
.rules-frac { display: inline-flex; flex-direction: column; align-items: center; line-height: 1.15; }
.rules-frac .num { padding: 0 .45rem .1rem; border-bottom: 2px solid var(--ink-dim); }
.rules-frac .den { padding: .1rem .45rem 0; }
/* distance table */
.rules-tablewrap { overflow-x: auto; margin: .8rem 0; }
.rules-table { border-collapse: collapse; width: 100%; font-size: .9rem; }
.rules-table th, .rules-table td { text-align: start; padding: .4rem .6rem; border-bottom: 1px solid rgba(255,255,255,.09); }
.rules-table td.d, .rules-table th.d { text-align: end; font-variant-numeric: tabular-nums; font-weight: 800; color: var(--gold); }
.rules-table thead th { color: var(--ink-dim); font-weight: 700; }
/* confirmed-rule callout */
.rules-rule { display: flex; gap: .55rem; align-items: center; margin: .7rem 0; padding: .55rem .8rem;
    background: rgba(201,162,75,.08); border-inline-start: 3px solid var(--gold); border-radius: 8px; color: var(--ink); font-weight: 600; }
/* example + end-state cards */
.rules-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: .7rem; margin: .95rem 0; }
.rules-card { padding: .6rem; border: 1px solid rgba(255,255,255,.1); border-radius: 10px; background: rgba(255,255,255,.03); text-align: center; }
.rules-card .rc-title { font-weight: 700; font-size: .9rem; margin-bottom: .35rem; }
.rules-card .rc-math { direction: ltr; font-variant-numeric: tabular-nums; color: var(--ink-dim); font-size: .85rem; margin-top: .35rem; }
.rules-ends { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: .7rem; margin: .95rem 0; }
.rules-end { padding: .7rem; border: 1px solid rgba(255,255,255,.1); border-radius: 10px; background: rgba(255,255,255,.03);
    display: flex; flex-direction: column; align-items: center; gap: .45rem; text-align: center; }
.rules-end .re-label { font-size: .86rem; color: var(--ink); }
.rules-verdict { padding: .12rem .5rem; border-radius: 999px; font-size: .76rem; font-weight: 800; }
.rules-verdict.go { background: rgba(46,125,50,.2); color: #6ed88a; }
.rules-verdict.stop { background: rgba(200,60,60,.18); color: #ff8a8a; }
/* real-render board diagrams (StaticBoard): a square canvas + an optional cell-unit SVG overlay */
.rules-static { position: relative; margin-inline: auto; max-width: 100%; }
.rules-static .board-static { display: block; width: 100%; aspect-ratio: 1 / 1; border-radius: 8px; }
.rules-static-ov { position: absolute; inset: 0; pointer-events: none; }
.rules-static-ov svg { width: 100%; height: 100%; display: block; }
.rules-ends .rules-static { margin-bottom: .1rem; }
/* side-by-side figure rows + the creation-lifecycle flow */
.rules-figrow { display: flex; flex-wrap: wrap; gap: 1.1rem; justify-content: center; align-items: flex-start; margin: .9rem 0; }
.rules-figrow figure { margin: 0; text-align: center; max-width: 220px; }
.rules-flow { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .55rem; margin: .95rem 0; }
.rules-flow figure { margin: 0; text-align: center; }
.rules-flow figcaption { color: var(--ink-dim); font-size: .8rem; margin-top: .3rem; }
.rules-flow .rf-arrow { color: var(--ink-dim); font-size: 1.3rem; }
/* the five-line summary box at the top of /rules */
.rules-tldr-sec { margin-top: 1.15rem; }
/* skin-aware legend swatches (colour chip in colour skins, CB shape in colourblind) */
.rules-sw-svg { width: 16px; height: 16px; flex: 0 0 auto; border-radius: 4px; }
/* game-length ballparks */
.rules-lengths { margin: .5rem 0 0; padding-inline-start: 1.25rem; color: var(--ink); }
.rules-lengths li { margin: .2rem 0; font-variant-numeric: tabular-nums; }
/* timers table: each mode cell stacks a starting bank over the per-move increment */
.rules-timers td, .rules-timers th { text-align: center; vertical-align: top; }
.rules-timers td.tb, .rules-timers th:first-child { text-align: start; font-weight: 700; white-space: nowrap; }
.rules-timers .tc-start { display: block; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.rules-timers .tc-inc { display: block; font-size: .8rem; color: var(--ink-dim); }

/* ── Worked example: a real AI game, stepped move by move (replaces the old live sandbox) ─────────── */
.rules-example-sec { margin-top: 2.1rem; }
.rex-stage { display: flex; flex-wrap: wrap; gap: 1.1rem; align-items: flex-start; margin: 1rem 0 0; }
.rex-board { flex: 1 1 260px; min-width: 240px; text-align: center; }
.rex-board svg { max-width: 100%; height: auto; }
.rex-panel { flex: 1 1 260px; min-width: 240px; }
.rex-controls { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .7rem; }
/* #364: padding-INLINE, not the shorthand. This was the tightest chip in the product (a .85rem label
   in a 23px pill) and the block half of the shorthand reset .chip's plate-derived padding to a
   one-line .3rem, leaving 0.5px of clearance inside a ~2px gold stroke. The vertical room a plate
   needs is a property of the plate, so it comes from .chip and this rule only narrows the sides. */
/* #385: a rex chip is the smallest chip in the product - .85rem type in a 31px box with .7rem of
   inline padding - so it gets the cap the art draws at 31px (46 * 31/92 = 15.5, rounded to 14),
   which its padding clears by 3.4px. At the family's 16px it would have been 1.7px, inside the
   gold stroke itself. */
.rex-controls .chip { padding-inline: .7rem; font-size: .85rem; border-image-width: 48.91% 14px; }
/* #681: Play and Pause have different widths in every catalogue. Both labels occupy the same grid
   cell, so the toggle's box is sized by the wider translation even while that label is invisible.
   This keeps every later chip on the same flex line across states without fixing the box to today's
   strings: a longer translation grows BOTH states together. aria-label on the chip carries the
   current action; this sizing pair is presentation-only. */
.rex-toggle-label { display: inline-grid; }
.rex-toggle-label > span { grid-area: 1 / 1; }
.rex-toggle-label > [aria-hidden="true"] { visibility: hidden; }
.rex-indicator { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .8rem; margin-bottom: .55rem; }
.rex-move-of { font-weight: 800; color: var(--ink); font-variant-numeric: tabular-nums; }
.rex-tomove { display: inline-flex; align-items: center; gap: .35rem; color: var(--ink-dim); font-size: .9rem; }
/* Reserve a stable height for the commentary so autoplay stepping does not change the page height
   under the reader. #317 removed the ranked-score list and the two lines that captioned it, so the
   commentary is ONE paragraph now - the action - and it wraps to more lines in a narrow panel than
   in a wide one, which is why the reserve is banded rather than constant.
   The bands are the PANEL width, not the viewport: .rex-panel is 'flex: 1 1 260px; min-width:
   240px' in a 'gap: 1.1rem' .rex-stage inside .rules-page (max-width 760px) inside .app (max-width
   1040px, 1.25rem side padding), so the panel is the page below 538px (one column, 280px+), bottoms
   out at 240px from 538px to 617px (two columns just after the wrap), and settles at 371px from
   800px up. Each value is the tallest that paragraph measures over all 36 plies x 6 locales at the
   narrowest panel in its band (8.2rem = 5 lines, 6.7rem = 4, 5.2rem = 3; ru and ko are the long
   ones). min-height only prevents shrinking, so a longer translation grows the panel rather than
   overflowing it - and a change to any of the four widths above moves the bands, which is why
   RulesWorkedExampleTests pins them. */
.rex-commentary { min-height: 6.7rem; }
.rex-commentary p { margin: .35rem 0; color: var(--ink); line-height: 1.5; }
@media (min-width: 538px) and (max-width: 617px) { .rex-commentary { min-height: 8.2rem; } }
@media (min-width: 800px) { .rex-commentary { min-height: 5.2rem; } }

.live-stats { color: #9aa7bd; font-size: .9rem; margin: -.25rem auto .85rem; opacity: .9; }  /* #447 */

/* ── Margin diet (user report: "any pixel must have a purpose") ─────────────────────────────── */
main h1 { margin: .2rem auto .55rem; }                    /* browser default h1 margins were dead space; auto per #447 */
.page-back { margin-block: 0 .35rem; }                    /* #550: navigation is not a content paragraph */
.panel > .picker-label:first-child { margin-top: 0; }     /* no empty band above the first picker */

/* "My games" grouping: games waiting on you vs. on the opponent */
.games-group { margin: .7rem auto .35rem; font-size: .95rem; color: var(--ink-dim); font-weight: 700;  /* #447 */
    text-transform: uppercase; letter-spacing: .04em; }

/* Queue: search settings freeze while in queue (cancel to change them) */
.row-locked .tile { pointer-events: none; opacity: .55; filter: saturate(.5); }

/* Profile / public player: compact view line (avatar · name · edit) */
.profile-view { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; }
/* #327: the administrator's badges and standing on a profile card - their own wrapping row UNDER the
   name line (.profile-view already holds the avatar, the name and four buttons at 360px), so a
   24-character badge (the server's bound) never squeezes the name or runs past the card. */
.profile-badges { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .6rem; }
.badge.award { background: rgba(90,140,220,.25); color: #bcd4ff; }
.badge.standing { background: rgba(220,70,70,.28); color: #ffb3b3; }
.badge.lock-note { margin-inline-start: .4rem; padding: .15rem .5rem; font-size: .7rem; }
/* #281 Phase 2: the Contribution level pill and the admin-granted contribution badges — same
   wrapping .profile-badges row discipline as #327 (pills wrap as a unit, never squeeze the name). */
.badge.level { background: rgba(90,190,120,.22); color: #a9e6bd; }
.badge.contrib { background: rgba(201,162,75,.22); color: #e9c877; }
/* The own-profile Contribution row: score, streak and the grace note wrap as pills do — each
   fragment is nowrap-solid, the ROW wraps (feedback f7d77400: constrain boxes, not strings). */
.contrib-row { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem .9rem; }
.contrib-stat { white-space: nowrap; flex: 0 0 auto; font-weight: 600; }
.contrib-grace { white-space: nowrap; flex: 0 0 auto; font-size: .85rem; }
.profile-admin-note { margin: .4rem 0 0; }
.profile-name { font-weight: 700; font-size: 1.15rem;
    /* A heading should WRAP a long unbroken name, not hide it (same gate finding as .rc-player). */
    overflow-wrap: anywhere; min-width: 0; }

/* Every mention of a player's name is a doorway to their public page (/player/{id}) */
.rc-player, a.rc-player, a.chat-name { color: #86b7ff; cursor: pointer; text-decoration: none; }
.rc-player:hover, a.rc-player:hover, a.chat-name:hover { text-decoration: underline; }
/* Long unbroken display names truncate, never widen the page (gate 20260731-112700: a 38-char
   no-space name overflowed every live-lobby row at 390px; names are user-controlled, so this is
   reachable in prod). Inline-block so max-width applies inside the row flow.
   #309: min(260px, 100%), never a bare 260px. white-space: nowrap makes this box's min-content the
   WHOLE string, so shrink-to-fit never goes below the cap - a FIXED cap is therefore the box's
   width whatever its container is. In a /games row at 360px that container (.rc-main) is only
   ~150px, so the ellipsis was applied 50-85px BEHIND the status pill instead of before it. The
   percentage resolves against .rc-main, so the cut lands at the edge of the column the name has;
   the 260px arm keeps the original protection where the column is wider than that. */
.rc-player, a.rc-player, a.chat-name {
    display: inline-block; max-width: min(260px, 100%); overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
/* #353: …but a seat GameParticipants.ProfileHref refuses (a vs-AI opponent) is NOT a doorway, so it
   must not READ as one either — body ink, default cursor, no hover reaction. The report said
   "should not be clickable", and a name that still looks and hovers like a link is only half fixed.
   Same rule as the play screen's `.p-card span.p-name` (#224); the row variants are both spans, so
   the modifier class carries it. RowPlayerName is the only thing that renders it. */
.rc-player.rc-plain { color: var(--ink); cursor: default; }
.rc-player.rc-plain:hover { text-decoration: none; }

/* The lobby stats line links to the spectator lobby */
a.live-stats-link { color: inherit; text-decoration: none; }
a.live-stats-link:hover { color: #a9ccff; text-decoration: underline; }

@keyframes ctaPulse { 50% { filter: brightness(1.18); } }

/* ── Pinned chrome (report 46231039): the top bar sticks to the top and the primary action row
   (queue / invite CTA) to the bottom, so both stay reachable while the middle scrolls. Focus mode
   hides the chrome entirely, so it is naturally exempt. ─────────────────────────────────────── */
/* Glassy like the panels, slim, LOW z-index (below every scrim/modal/menu).
   ⚠️ NEVER put backdrop-filter / filter / transform on .topnav: any of those properties turns the
   bar into the containing block for position:fixed descendants — fixed elements inside it (the
   hamburger dropdown) get trapped and clipped in the strip (real outage, report afc6b68a; the
   About/Report/Settings dialogs were hoisted to the app root in #10, but the menu still lives
   here and check-css still guards the property). Translucency comes from the gradient alpha. */
.topnav { position: sticky; top: 0; z-index: 30;
    /* FULLY opaque (#84): went .88 → .97 → 1.0. At .97 the residual bleed measures 2-4 luminance
       units — invisible to people, but contrast-amplifying screenshot review reads whole words
       through the bar and keeps re-reporting it. Opaque retires the class; the audit enforces it. */
    background: linear-gradient(180deg, rgb(44,55,73), rgb(26,35,49));
    border-bottom: 1px solid rgba(201,162,75,.18);
    padding: .3rem 0 .45rem; margin-bottom: .8rem; }
/* #137/C28: the docked CTA is a crisp OPAQUE footer — the old fade-in gradient let option tiles
   ghost through its top band (owner report dc093be0's screen). */
/* #614: the dock's own box, declared ONCE. Every term below is consumed BOTH by the dock's rule and
   by the page's reservation for it, so the reservation cannot drift from the box it reserves for -
   a wider dock padding or a deeper pull moves both in one edit. --cta-dock-control is the plate at
   the size app.css already designs it to be ("Two lines inside the plate is the only reading that
   stays true for the next longer translation"), not a pixel somebody measured once. The CONTENT
   term is the one CSS cannot derive, so it is not left to arithmetic alone:
   Application\test-invite-dock-clearance.ps1 measures the dock's REAL height in the browser, in
   every locale this repo ships, and reddens the gate if the declared depth stops covering it. */
:root {
    --cta-dock-pad-top: .6rem;
    --cta-dock-pad-bottom: .8rem;
    --cta-dock-pull: 1rem;                                            /* the negative bottom margin that seats the dock on the panel's edge */
    --cta-dock-line: 1.2rem;                                          /* a 1.1rem .cta line box */
    --cta-dock-control: calc(.95rem * 2 + var(--cta-dock-line) * 2);  /* the plate: its own padding block + the two lines it is designed to hold */
    --cta-dock-depth: calc(var(--cta-dock-pad-top) + var(--cta-dock-pad-bottom) + var(--cta-dock-pull) + var(--cta-dock-control));
}
.panel .cta-row { position: sticky; bottom: 0; z-index: 40;
    margin: .4rem -1.1rem calc(-1 * var(--cta-dock-pull));
    padding: var(--cta-dock-pad-top) 1.1rem var(--cta-dock-pad-bottom);
    background: rgb(24,32,45);
    border-top: 1px solid rgba(201,162,75,.25);
    box-shadow: 0 -6px 18px rgba(0,0,0,.35);
    border-bottom-left-radius: 16px; border-bottom-right-radius: 16px; }
/* #614 (class C28, the sibling half): the dock above is OPAQUE by #137, and it is the LAST thing in
   the panel - so whatever the panel renders immediately before it is what the dock paints over, and
   NOTHING reserved the dock's depth. A scroll-into-view - which is what a click, a focus, an anchor
   jump and a regression's own "scroll to it" all perform - moves its target to the raw bottom edge
   of the scrollport, i.e. exactly the dock's height UNDERNEATH the dock: measured -71.0px at
   844x390 and -71.8px at 1280x900 against a 71.4px dock, in ru / ko / en alike, which is the whole
   of the 2.18 review's finding on the #496 empty-friends helper. scroll-padding on the scrollport
   is what a docked bar is reserved with; the viewport's scrollport is the ROOT element, so the
   declaration has to sit there, and `:has` scopes it to the pages that actually dock a CTA.
   @supports for the same reason the --page-col block below has one: a browser without :has() keeps
   the shipped rendering rather than a half-applied one. Nothing moves on screen - scroll-padding
   costs no layout; what changes is where a scroll STOPS. */
@supports selector(:has(*)) {
    html:has(.panel .cta-row) { scroll-padding-bottom: var(--cta-dock-depth); }
}

/* #46: global matchmaking-search indicator — a slim full-width row INSIDE the sticky header (so it
   rides above the profile row on every page). Rendered only while a search is active: when absent
   it leaves no gap. grid-column spans the header's 3-column grid; flex-basis covers the legacy
   flex layout of .topnav. The time value is LTR-isolated so RTL locales never reorder "1:23". */
.search-indicator {
    grid-column: 1 / -1; flex-basis: 100%;
    display: flex; align-items: center; justify-content: center; gap: .45rem;
    margin: .25rem .2rem 0; padding: .28rem .8rem; min-height: 30px;
    border-radius: 999px; text-decoration: none; font-weight: 600; font-size: .92rem;
    color: #f2e7c9; background: rgba(201,162,75,.16); border: 1px solid rgba(201,162,75,.4);
}
/* Hover feedback via the border only — the hover-background gate keeps washes off controls. */
.search-indicator:hover { border-color: rgba(201,162,75,.75); }
.invitation-indicator { color: #dff6e5; background: rgba(46,160,84,.18); border-color: rgba(46,160,84,.5); }
.invitation-indicator .search-indicator-dot { background: #63d982; }
.search-indicator-time {
    font-variant-numeric: tabular-nums; direction: ltr; unicode-bidi: isolate; color: #ffd97a;
}
.search-indicator-dot {
    width: 9px; height: 9px; border-radius: 50%; background: #ffd97a;
    animation: search-indicator-pulse 1.2s ease-in-out infinite;
}
@keyframes search-indicator-pulse {
    0%, 100% { opacity: .35; transform: scale(.8); }
    50% { opacity: 1; transform: scale(1.15); }
}
/* #73: focus mode hides .topnav (and the in-header indicator with it) — the floating copy keeps
   the search visible on the fullscreen game view. Hidden everywhere else; solid backdrop so it
   reads over the board. Top-center, clear of the corner focus-exit button. */
.search-indicator-floating { display: none; }
.app.focus-mode .search-indicator-floating {
    display: flex; position: fixed; top: 8px; left: 50%; transform: translateX(-50%);
    z-index: 300; max-width: min(70vw, 340px); margin: 0; padding: .3rem .9rem;
    background: rgba(20,26,38,.86);
}
.app.focus-mode .search-indicator-floating.invitation-indicator { top: 48px; }

/* ── Queue compactness (report 248f8515: "the whole page should fit a regular screen") ───────── */
.panel { padding: 1rem 1.1rem; }
.picker-label { margin: .5rem 0 .28rem; }
.tile-row { gap: .45rem; }
.tile { padding: .5rem .95rem; min-height: 40px; }
.cta-row .cta { min-height: 42px; }

/* Review step buttons: «‹›» glyph over a tiny localized caption. Equal width, tall enough that
   the caption never spills past the pill's bottom edge. The ROW wraps (Autoplay takes its own line
   when tight) while the step group stays together and fits a 360px viewport (issue #11). */
.review-row { justify-content: center; align-items: center; gap: .4rem .5rem; flex-wrap: wrap; }
/* CONTAINER-relative sizing, not viewport media queries: in landscape the game side column is
   ~260px wide while the viewport is 800px+, so a viewport breakpoint never fires and fixed-width
   buttons overflowed the page sideways (the standing horizontal-view issue). The four steps
   share the row equally and shrink with their container; the counter shrinks last. */
.rev-steps { display: flex; align-items: center; gap: .3rem; flex-wrap: nowrap; width: 100%; max-width: 340px; justify-content: center; }
/* #201: in the tightest side column (focus mode's 240px clamp floor) each caption box is ~35px,
   so EN "Forward" (36px at .6rem) ellipsised. Slim horizontal padding + a .55rem caption keep
   every locale's caption whole WITHOUT touching the glyph size — the ellipsis exemption in the
   UxAudit clipped-text probe is also lifted for .rev-cap, so this class now fails the gate. */
/* #364: 52px, not 48px. This pill stacks a 1.15rem glyph over a .55rem caption, so its content is
   two lines by construction - 27.8px of it - and the plate art only draws over ~64% of the box, i.e.
   30.8px of a 48px button. The glyph's line box sat exactly ON the drawn edge. Padding is not the
   lever here (the content is centred and smaller than the content box), the BOX height is. */
/* #385: the one plate control whose box is fixed by design - four of them share a 240-260px column,
   so it is always 64px wide - which makes its cap the one number that can be pinned without picking
   anything: 46/252 of 64px is 11.7, i.e. 12px is the cap the stretched art was ALREADY drawing here.
   Nothing moves on screen; what changes is that it stops being a coincidence of the box. It cannot
   be the art's aspect cap (46 * 55/92 = 27.5): two of those leave 9px of straight side in a 64px
   pill, and the caption - which is centred, not padding-bound, so its own room is (64 - textWidth)/2
   and not the .1rem padding - would sit on the curve at every locale. */
.rev-btn { flex: 1 1 0; flex-direction: column; gap: .05rem; min-width: 0; max-width: 64px; min-height: 55px;
    padding: .3rem .1rem .35rem; line-height: 1; box-sizing: border-box; border-image-width: 48.91% 12px; }
.rev-glyph { font-size: 1.15rem; font-weight: 800; }
/* Issue 499 (client feedback e711dd19, owner: the Try board's actions "are words... Replace with
   proper action symbols buttons"): the CAPTION-LESS rev-btn. The rule above sizes a step control
   for a glyph stacked over a .rev-cap - two lines, 55px, and `flex: 1 1 0` so four of them share
   the review row equally - and that box is wrong for a control that carries no caption at all.
   The editor already answered this twice by CONTAINER (.pz-history and .pz-replay-nav below), but
   the hot-seat and solve bars put their step controls in a row they SHARE with worded flow chips
   (Full screen, Abort, Restart, the move counter), so a container rule would resize those too.
   Hence the variant on the control: the same 52x40 box those two bars already ship, an equal cap
   for both axes, and a shrink-free box - the point of the change is that the control stops being
   sized by the length of a translation, so `flex: 1 1 0` must not size it by its ROW either. */
.rev-icon { flex: 0 0 auto; min-width: 52px; max-width: none; min-height: 40px;
    flex-direction: row; display: inline-flex; align-items: center; justify-content: center; padding: .3rem .55rem; }
.rev-icon .rev-glyph { font-size: 1.2rem; line-height: 1; }
.rev-cap { font-size: .55rem; color: var(--ink-dim); white-space: nowrap; overflow: hidden; max-width: 100%; text-overflow: ellipsis; }
.review-label { flex: 0 0 auto; min-width: 34px; text-align: center; font-variant-numeric: tabular-nums; }
/* #174: "Save as starting position" sits under the step controls; full-width, its own row (wraps in
   the flex review-row). No background swap on hover (check-css touch-stuck guard) - border + lift. */
/* #236: a chip that IS its own row has no row left to wrap. .chip's #200 `white-space: nowrap` is
   right for a MULTI-chip row (the chips stay one line and the ROW wraps instead) — but this one is
   `flex: 1 1 100%`, so there is no row to give. RU "Сохранить как стартовую позицию" needs ~300px
   and the wide desktop side rail is ~250px, so the label rendered straight past the pill's rounded
   ends: text sitting outside its own border. It fits at mobile portrait only because the pill is
   viewport-wide there, and en/ja/ko/zh/he all fit at desktop only because Russian is the longest
   string in the set — i.e. the bug is the fixed width, not the translation. Let this pill wrap: it
   grows a line instead of letting the text leave it, at every width and in every language. */
/* #364: and because it is the one chip that WRAPS (the rule above), it is the one chip that needs a
   two-line padding budget - the same shape as #306's /friends buttons. ru "Sohranit kak startovuyu
   poziciyu" takes two lines in the ~250px side rail and the second one landed outside the artwork. */
.rev-save-pos { flex: 1 1 100%; text-align: center; white-space: normal; line-height: 1.2; padding-block: .95rem; }
.rev-save-pos:hover { border-color: rgba(201, 162, 75, .55); transform: translateY(-1px); }

/* Issue 510: the EXPLORABLE board - the chip that arms it, and the strip that explains where you
   are while it is up.

   THE BOX IS WHAT GIVES, NEVER THE STRING (the standing directive from feedback f7d77400 / #257,
   and the #236/#364 shape one line above). Both pills here are chips that are their OWN row, which
   is the exact geometry that let "Save as starting position" run past its rounded ends in ru: a
   `flex: 1 1 100%` pill has no row left to wrap, so .chip's #200 `white-space: nowrap` has to be
   lifted on it and the padding budget has to be the two-line one. The longest labels in this set
   are ru "Закрыть изучение" and he "חזרה למהלכי המשחק"; at 360px they fit on one line and at the
   ~240px focus-mode side rail they take two, which is what these rules are sized for. No length
   assumption is made about any of them - shortening a translation only moves the problem to the
   next language.

   The strip's PROSE is the other half: .ex-hint is a sentence in six languages inside a column that
   can be 240px, so it wraps at `overflow-wrap: anywhere` rather than being trimmed, and its
   container is `min-width: 0` so a long unbroken run cannot push the game side column wider than
   the viewport (the standing horizontal-view rule the .rev-steps comment above states). */
.rev-explore { flex: 1 1 100%; text-align: center; white-space: normal; overflow-wrap: anywhere;
    line-height: 1.2; padding-block: .95rem; }
.coach-entry-refusal { flex: 1 1 100%; min-width: 0; margin: 0; text-align: center;
    overflow-wrap: anywhere; }
/* The armed look reuses the sheet's own "this toggle is on" idiom - the primary plate plus weight,
   as `.tile.selected` wears it. NOT `.armed`: that is the red pulsing ring of an arm-then-confirm
   DESTRUCTIVE control, and exploring a finished game destroys nothing. */
.rev-explore.selected { border-image-source: var(--inline-pill-primary); font-weight: 700; }
.explore-note { display: flex; flex-direction: column; align-items: stretch; gap: .4rem;
    min-width: 0; margin: .1rem 0 .5rem; padding-inline: .5rem; }
.ex-hint { margin: 0; font-size: .85rem; line-height: 1.35; color: var(--ink-dim); overflow-wrap: anywhere; }
.ex-variation { margin: 0; font-size: .85rem; line-height: 1.35; font-weight: 700; overflow-wrap: anywhere; }
.ex-return { align-self: stretch; text-align: center; white-space: normal; overflow-wrap: anywhere;
    line-height: 1.2; padding-block: .95rem; }

/* Players strip above the board: first mover (First) left, Second right; glow = side to act (review-aware) */
.players-bar { display: flex; justify-content: space-between; gap: .6rem; margin: 0 0 .5rem; }
.p-card { flex: 1 1 0; min-width: 0; display: flex; align-items: center; gap: .45rem;
    padding: .35rem .65rem; border: 2px solid rgba(255,255,255,.14); border-radius: 12px;
    background: rgba(28,37,51,.5); transition: border-color .15s, box-shadow .15s; }
.p-card.p-right { justify-content: flex-end; }
.p-card.acting { border-color: #6ed88a; box-shadow: 0 0 10px rgba(110,216,138,.28); }
.p-card .p-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* #224: ONLY a real user account looks clickable. The link affordance (link blue, hover underline,
   pointer) is scoped to the anchor; a vs-AI opponent and a local game's side labels render as a
   plain <span> in body ink with the default cursor — no hover reaction at all. */
.p-card a.p-name { color: #86b7ff; text-decoration: none; }
.p-card a.p-name:hover { text-decoration: underline; }
.p-card span.p-name { color: var(--ink); cursor: default; }
.p-card .p-rating { color: var(--ink-dim); font-size: .82rem; white-space: nowrap; }
/* #188: an AI seat says WHAT it is next to its name (the engine it thinks with). Sized and dimmed
   like the rating it sits beside, so the strip keeps one visual rhythm whoever is playing; it
   truncates rather than pushing the name out of a narrow card. */
.p-card .p-ai { color: var(--ink-dim); font-size: .82rem; white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis; min-width: 0; }
/* #278: the side's game progress beside its name - "12(+3)": opposing Suns destroyed (grows only,
   whoever engulfed them) + the opponent's unused creation cells this side's Suns hold right now.
   A small pill in body ink so it reads as a LIVE counter, not a second rating; tabular digits so it
   does not jitter as it counts; nowrap and no-shrink so the NAME truncates, never the numbers. */
.p-card .p-progress { color: var(--ink); font-size: .82rem; line-height: 1.45; white-space: nowrap;
    flex: 0 0 auto; font-variant-numeric: tabular-nums; padding: 0 .45rem; border-radius: 999px;
    background: rgba(255,255,255,.09); }
/* #278: with the readout beside it, a one-line card has no room left for the NAME at a phone width
   (360px: two 155px cards, each holding dot + name + rating + readout - measured, the name was left
   3px) nor in the 200px rail. So the GAME page's cards wrap: the dot and the name on the first line,
   the readout, engine label and rating on a second. Scoped to the play screen's strip - the Theater
   strip shares .p-card, carries no readout and keeps its one line. The name claims at least 60% of
   the card (what pushes the rest off its line) and never more than the card minus the dot (what
   keeps a long name ON the dot's line instead of orphaning the dot above it); the right-hand card
   wraps the other way round so its name line is on top too, its text hugging its dot. The engine
   label keeps a zero flex basis capped at its own width, so it truncates on the readout's line
   (#188's rule) instead of wrapping the rating onto a third. A card wide enough for all of it (a
   tablet strip) stays on one line. */
/* Issue 393: ...and the Theater strip needed exactly the same thing, which is why the scope above
   is now the STRIP rather than the play screen's copy of it. `.p-card .p-name` is the only
   shrinkable item in that row, beside three `white-space: nowrap` siblings (the dot, the rating,
   the side caption), so the caption and the rating took their full width first and the name got
   what was left: measured at 360px, a 30px box on the human seat and 12px on the bot seat, i.e.
   "Ada Lovelace" as "A." and "Nova" as a bare "N" with no room even for the ellipsis. The element
   type is irrelevant - an <a> and a <span> were crushed identically, because the flex rule decides.
   The string is not the fix (a shorter name only moves the threshold); the BOX is. So the theater
   card wraps too and its name column claims the same 60% floor, which pushes the rating and the
   caption onto the card's second line at a phone width and leaves the name ~102px there: both
   reported names now render whole. A card wide enough for all four (a tablet or desktop strip)
   still keeps them on one line, because flex-wrap only wraps what does not fit.
   test-participant-name-fit.ps1 measures this over (surface) x (name length) x (viewport). */
.players-bar .p-card { flex-wrap: wrap; gap: .15rem .45rem; }
.players-bar .p-card.p-right { flex-wrap: wrap-reverse; }
.players-bar .p-card .p-name { min-width: 60%; max-width: calc(100% - 1.75rem); }
/* Written one class lighter than the rail's counterpart in the wide media block above, so the rail
   rule wins there by specificity - by source order alone this later line would override it. */
.players-bar .p-right .p-name { text-align: end; }
.game-board-wrap .players-bar .p-card .p-ai { flex: 1 1 0; max-width: max-content; }
.side-dot { width: 18px; height: 18px; border-radius: 50%; flex: 0 0 auto; box-shadow: 0 0 6px rgba(0,0,0,.4); }
.side-dot.odd { background: radial-gradient(circle at 35% 30%, #a8d4ff, #1e78e0 60%, #0b3f86); }
.side-dot.even { background: radial-gradient(circle at 35% 30%, #ffe9a8, #f4b91c 60%, #8a5f00); }

/* Collapsible game-history filters (player page + My games): a small toggle chip, panel hidden by default */
.filter-toggle { font-size: .85rem; padding: .35rem .8rem; min-height: 34px; margin: 0 0 .5rem; }

/* == ISSUE 533 - A SECTION'S CONTROL SHARES ITS HEADING'S LINE ================================
   2.17 screenshot review, `my-games-finished` at 844x390, two locales, both at scroll 0,0: the
   frame showed the Finished heading and the Filters chip and nothing else. Measured on the sheet
   this replaces, at 844x390: heading bottom 321, chip 352..386, first .card-list row top 404 -
   14px past the fold, so a user with five finished games saw a header and a filter control and
   no evidence any game existed. Every gate assertion passed: the row is in the DOM, and
   `ToBeVisibleAsync` is true of an element scrolled off the user's screen. That is issue 491's
   class one turn on, and this is the structural half of the answer rather than a height threshold:
   a filter control MODIFIES the heading's line, it is not a row between the heading and the list
   it titles, so the two share one row at every width and the list starts 67px higher at every
   height. Held by Application\test-section-fold-layout.ps1 (the real sheet, the real markup, real
   844x390 frames) and by the in-run subject-fold audit (UxAudit) on every gate step that declares
   what it is about.
   The wrapper is a PAGE-LEVEL block, so it joins the #447 --page-col family below: its heading
   has to start on the first character of the .card-list it titles exactly as a bare <h2> did. */
.section-head { display: flex; flex-wrap: wrap; align-items: center; gap: .2rem .7rem; margin: 1.1rem auto .35rem; }
.section-head > h2 { margin: 0; }
.section-head > .chip { margin: 0; }

/* The other half of the same answer, and it is the one that generalises past /games: on a SHORT
   window the page's heading rhythm gives. A landscape phone has 390px of height and the browser
   default .83em heading margins spend ~40px of it per section on white space above content the
   user cannot otherwise reach. Margins only - no type scale moves, so nothing gets smaller and
   nothing re-wraps; the headings simply sit closer to what they title. Same idiom and same
   justification as the #314 in-play chrome block, one surface out: there the chrome gives on a
   portrait phone, here the page column's rhythm gives on a short one. */
@media (max-height: 480px) {
    /* The page title is the one heading that is 2em, and on a 360-390px window it spends ~37px of
       line plus its margins before the page has said anything. #314 removes it outright on a
       portrait phone in play; a short LANDSCAPE window is not in a game, so it keeps its title and
       the title takes h1-of-a-panel size instead of page-banner size. */
    main > h1 { font-size: 1.5rem; margin-block: .15rem .35rem; }
    main > h2, main > h3, main > .section-head { margin-block: .5rem .2rem; }
    main > .empty-note { margin-block: .35rem; }
}

/* Player page game-history filters: labelled controls + applied-filter chips */
.pg-filters { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; align-items: flex-end; }
.pg-filters select, .pg-filters input { background: #141b28; color: var(--ink);
    border: 1px solid rgba(255,255,255,.16); border-radius: 8px; padding: .38rem .5rem; font-size: .9rem; }
.pg-f { display: flex; flex-direction: column; gap: .15rem; font-size: .78rem; color: var(--ink-dim); }
.pg-opp { flex: 1 1 auto; min-width: 140px; }
.pg-chips { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 .6rem; }
.pg-chip { font-size: .8rem; padding: .25rem .65rem; min-height: 30px; }
.pg-chip .x { margin-inline-start: .4rem; opacity: .8; font-weight: 700; }

/* No tap flash on the custom controls — reads as a stray "animation" on touch */
.tile, .chip, .navpill, .menu-tile, main button, .row-card { -webkit-tap-highlight-color: transparent; }

/* Touch devices: hover is now only a brightness lift (never a skin swap), and on hoverless/coarse
   pointers even that is off — a tap can never leave any lingering visual state. */
/* #410: the block itself now lives at the END of this file, not here. It is a NEUTRALISER, and a
   neutraliser only works while nothing overrides it - source order is the whole of that. Sitting
   here at ~line 2143 it was silently out-ordered by `.row-card.rc-click:hover` (#161, ~line 2300),
   equal specificity and later, so a tap on a board-picker row kept its 1px lift and that lift cut
   the card's top border off inside .custom-board-list's clip box (feedback 04595a1f, SamsungBrowser
   360x531). Last in the sheet is the only position where the guard cannot be out-ordered again;
   check-css.ps1 rule 1 now holds it there and holds every moving/repainting :hover rule inside it. */

/* Inline share chip next to the game back link (any online game is link-shareable). RTL-safe gap. */
.share-inline { margin-inline-start: .75rem; }

/* Download artifacts are plain links, not chips (owner report b119368f). */
.dl-link { text-decoration: underline; margin-inline-end: 1.1rem; }
/* Paused board placeholder + pause countdown (issue #22 part 2). */
.board-paused { text-align: center; padding: 2.5rem 1rem; }
.pause-countdown { text-align: center; font-weight: 600; }
/* #286: first-move window countdown — sits over the clocks in the clocks' own box (same max-width), so a
   long localized sentence wraps INSIDE the column instead of running through the edges at 360px. */
.first-move-countdown { text-align: center; font-weight: 600; color: var(--gold); max-width: 460px; margin: 0.4rem auto 0; padding: 0 0.5rem; overflow-wrap: anywhere; }  /* #311 */

.paused-title { font-weight: 700; font-size: 1.2rem; }

/* ── Puzzles & custom-board editor (#97) ─────────────────────────────────────────── */
.pz-menu { margin-bottom: 14px; }
.pz-editor .pz-toolbar { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-end; }
.pz-dim { display: flex; align-items: center; gap: 8px; }  /* #115: one Size: [N] x [Y] line */
.pz-x { opacity: 0.7; }
/* #116/#124: compact VERTICAL spinners (arrow above/below the number); touch-sized targets. */
.pz-spin { display: inline-flex; flex-direction: column; align-items: center; gap: 2px; }
.pz-spinbtn { width: 46px; height: 26px; border-radius: 7px; border: 1px solid rgba(255,255,255,0.18);
  background: rgba(255,255,255,0.06); border-image: none; color: inherit; font-size: 12px; line-height: 1; cursor: pointer; }
.pz-stepval { width: 46px; height: 34px; text-align: center; }
.pz-palette { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
.pz-tool { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 8px;
  border: 1px solid rgba(255,255,255,0.18); border-image: none; background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; }
.pz-tool.active { border-color: #c9a24b; box-shadow: 0 0 0 1px #c9a24b inset; }
.pz-tool:disabled { opacity: .45; cursor: default; }
.pz-swatch { width: 14px; height: 14px; border-radius: 3px; display: inline-block; border: 1px solid rgba(255,255,255,0.25); }
/* #100: three rotating tools — the swatches hint at each tool's cycle. (pz-t- prefix: the tool
   key "cell" must not collide with the grid's .pz-cell class.) */
.pz-t-cell .pz-swatch { background: transparent; border-style: dashed; }
.pz-t-star .pz-swatch { background: radial-gradient(circle, var(--piece-blue) 40%, var(--piece-yellow) 60%); }
.pz-t-state .pz-swatch { background: conic-gradient(var(--cell-first) 0 25%, var(--cell-second) 0 50%, var(--cell-both) 0 75%, var(--cell-spent) 0); }
/* #106: the editing surface is the REAL renderer — a square board canvas (board.js renderStatic,
   active skin incl. colorblind shape-coding) with a transparent hit-grid overlaid on the drawn
   board area (inline left/top/width/height mirror board.js layout()). The overlay carries the
   tool cursor + affordances ONLY — never color; the canvas paints everything. */
/* Sized like the live game board (#110: "too small") — full width up to the game's cap. */
.pz-board { position: relative; aspect-ratio: 1 / 1; margin: 12px auto; width: 100%; max-width: min(680px, 78vh); }
.pz-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
.pz-grid { position: absolute; display: grid; gap: 0; direction: ltr; }  /* #126: the board is GEOMETRY - RTL locales must not mirror the cell order vs the canvas */
/* #113 (owner): NO touch/hover/focus square on cells — the painted cell change IS the feedback.
   Keyboard users keep a :focus-visible ring (never triggered by touch/mouse). */
/* #389 put `box-sizing: border-box` on the global button rule so an <a> and a <button> carrying
   the same class resolve min-height to the same box. That is a GEOMETRY property, and #360's rule
   is that the hit grid answers every one of them itself rather than inheriting it: the value here
   is the one a bare <button> already had from the UA stylesheet, so the drawn cell does not move,
   but the overlay no longer depends on what the page-button rule happens to say. */
.pz-cell { position: relative; box-sizing: border-box; border: none; border-radius: 0; outline: none; min-height: 0; min-width: 0; margin: 0;  /* #360: "main button" leaks into the hit grid twice over - its 44px tap-target min OVERFLOWED the 1fr grid on mobile (gate 20260802-092913), and its 0.25rem block margin sat every cell 4px BELOW the cell board.js draws there, on every board shape */
  background: transparent; border-image: none; cursor: pointer; padding: 0; width: 100%; height: 100%;
  -webkit-tap-highlight-color: transparent; }
.pz-cell:focus { outline: none; }
.pz-cell:focus-visible { outline: 2px solid rgba(255,255,255,0.4); outline-offset: -2px; }
.pz-cell.gone { cursor: default; }
/* #130 (owner): deleted cells stay PURE holes - no ghost square. #330: a hole is acted on by the
   SHAPE tool alone (its hammer half restores it), so tool-shape is the only class that earns the
   pointer cursor - the paint tools leave existence alone and must not advertise otherwise. #295's
   trap runs both ways: a cursor with no behaviour reads as broken, a behaviour with no cursor is
   never found, so this set is derived from the tools' real outcomes in PuzzleEditorToolAuthorityTests. */
.pz-grid.tool-shape .pz-cell.gone { cursor: pointer; }
.pz-actions { flex-wrap: wrap; }
/* #131: shown while a 3D skin is active — authoring stays top-down, Try/play render 3D. */
.pz-3d-note { text-align: center; font-size: 13px; color: rgba(255,255,255,0.55); margin: 2px 0 6px; }
.pz-objective { white-space: pre-wrap; }
.pz-movecount { margin-inline-start: 10px; }
/* #438: the auto-skip control moved off the play panel's board-nav row (where it needed the inline
   gap) onto the editing screen's task panel, beside the objective and its language — it is a
   property of the PUZZLE now, so it sits with the rest of what the author writes. */
.pz-brief .pz-autoskip { margin-top: 8px; }

/* #97 Phase 2: puzzle stats + star voting + search chips. */
.pz-stats { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 4px 0 8px; }
.pz-starline { display: inline-flex; gap: 2px; }
.pz-star { background: none; border-image: none; border: none; padding: 0 2px; cursor: pointer; font-size: 20px; color: rgba(255,255,255,0.25); }
.pz-star.avg { color: #8d7a45; }
.pz-star.mine { color: #f6d63f; }
.badge.pz-solved { margin-inline-start: 8px; background: #1d3a26; color: #7fd79a; }
.pz-searchbar { flex-wrap: wrap; margin-bottom: 10px; }
/* #140: icon-only editor chrome — Save/Save As live in the HEADER (SS1); the bottom savebar is
   gone. The three tool buttons hold slash-separated state chips painted from the live skin (SS3). */
.pz-editor-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.pz-item-id { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.pz-item-type { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: #c9a24b;
  border: 1px solid rgba(201,162,75,.4); border-radius: 6px; padding: 2px 7px; white-space: nowrap; }
.pz-item-name { font-size: 1.25rem; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pz-head-actions { display: flex; align-items: center; gap: 8px; }
.pz-icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px;
  border-radius: 10px; border: 1px solid rgba(201,162,75,.35); border-image: none; background: #18202e; color: #e8edf5;
  cursor: pointer; padding: 0; }
.pz-icon-btn:disabled { opacity: .45; cursor: default; }
/* SS3: one non-wrapping tool row; chips shrink before the row ever wraps. */
.pz-palette { display: flex; flex-wrap: nowrap; gap: 8px; overflow: hidden; }
.pz-tool2 { display: inline-flex; align-items: center; gap: 2px; padding: 6px 8px; min-height: 0; min-width: 0;
  border: 1px solid rgba(255,255,255,.18); border-image: none; border-radius: 10px; background: #131a27; color: #cfd8e6;
  cursor: pointer; flex-shrink: 1; }
.pz-tool2.active { border-color: #c9a24b; box-shadow: 0 0 0 1px rgba(201,162,75,.5); }
.pz-chip { width: clamp(14px, 4.5vw, 22px); height: clamp(14px, 4.5vw, 22px); display: block; border-radius: 3px; }
.pz-sep { color: rgba(255,255,255,.4); font-size: 12px; padding: 0 1px; }
/* #143: the hammer/eraser halves are separate buttons inside the shape tool (direct targets). */
.pz-shape-half { display: inline-flex; align-items: center; justify-content: center; opacity: .4;
  background: transparent; border-image: none; border: none; padding: 2px 3px; min-height: 0; min-width: 0;
  color: #cfd8e6; cursor: pointer; border-radius: 6px; }
.pz-shape-half.on { opacity: 1; color: #ffd76a; box-shadow: 0 0 0 1px rgba(201,162,75,.55); }
/* SS5: drag painting — the grid owns the gesture, so touches must not scroll the page. */
.pz-grid { touch-action: none; }
/* SS6: session-history row under the field (icon-only rev buttons need their own width — the
   viewer's rev-btn sizing assumes a glyph + caption stack). */
.pz-history { justify-content: center; gap: 8px; }
.pz-history .rev-btn { min-width: 52px; min-height: 40px; display: inline-flex; align-items: center; justify-content: center; }
.pz-history .rev-glyph { font-size: 1.2rem; line-height: 1; }
/* SS2: the type-choice step. */
.pz-type-row { display: flex; gap: 10px; justify-content: center; margin: 12px 0; flex-wrap: wrap; }
.pz-type-tile { font-size: 1.02rem; padding: 12px 18px; }
/* SS8-SS13: the Starting Position profile — full-skin preview + one grouped Play region. */
.bp-author { margin: 0 0 10px; }
.bp-preview-wrap { display: flex; justify-content: center; margin: 6px 0 14px; }
.bp-preview { width: 100%; max-width: min(420px, 72vh); }
.bp-play { border: 1px solid rgba(201,162,75,.3); border-radius: 12px; padding: 10px 12px 12px; }
.bp-play-row { flex-wrap: wrap; margin-top: 4px; }
.bp-visitor-note { margin: 8px 0 0; font-size: 0.9rem; }
/* #332: the puzzle's three screens — profile (view), solve / replay, and the editor's task panel.
   Every row wraps and centres; at 360px the pills stack rather than spill (C1). */
.pz-profile .pz-objective { margin: 4px 0 8px; }
.pz-lang-note { margin: 0 0 8px; font-size: 0.9rem; }
.pz-review-how { margin: -4px 0 8px; font-size: 0.9rem; }
/* #337: the solution request's own action row — the door to the solve screen, where a solution is
   recorded and attached. Wraps like every other pill row (C1 at 360px). */
.pz-review-actions { flex-wrap: wrap; margin: 0 0 10px; }
.pz-solve-region { margin-top: 4px; }
.pz-owner-row { flex-wrap: wrap; margin-top: 12px; }
.pz-solve-actions { flex-wrap: wrap; justify-content: center; margin-top: 10px; }
.pz-solve .pz-objective { margin: 0 0 10px; }
.pz-solution-note, .pz-solution-missing { text-align: center; font-size: 0.9rem; margin: 6px 0 0; }
.pz-replay-by { margin: 0 0 8px; }
.pz-replay-nav { justify-content: center; gap: 8px; flex-wrap: wrap; }
.pz-replay-nav .rev-btn { min-width: 52px; min-height: 40px; display: inline-flex; align-items: center; justify-content: center; }
.pz-replay-nav .rev-glyph { font-size: 1.2rem; line-height: 1; }
.pz-brief { margin-top: 14px; border-top: 1px solid rgba(255,255,255,.1); padding-top: 10px; }
.pz-brief textarea { width: 100%; box-sizing: border-box; min-width: 0; }
.pz-lang-field { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.pz-lang-field label { margin: 0; }
.pz-lang-select { max-width: 100%; min-width: 0; }
.pz-lang-hint { margin: 4px 0 0; font-size: 0.85rem; }
/* SS20: ~5 board rows visible; the rest scroll inside the list, never growing the screen. */
/* #410: this box CLIPS, and its first card's top border sat exactly on the clip edge (measured
   gapTop 0 at 360px), so any transform that moves a card up by a pixel takes that border with it.
   The touch guard above stops the STUCK case; this stops the case at all - a scroll container owes
   its content as much room as the largest lift its children can apply (1px today), so the padding
   is what makes a future hover/focus/active nudge unable to reopen this. max-height is a
   CONTENT-box limit here (no universal border-box in this sheet), so the visible row count is
   unchanged; only the box itself is 4px taller. */
.custom-board-list { max-height: 235px; overflow-y: auto; align-content: flex-start; padding-block: 2px; }
.custom-required-note { margin: 6px 0 0; }
/* #140 follow-up: clickable-only list rows + filters + 2-line clamped descriptions + lock mark. */
a.rc-click { text-decoration: none; color: inherit; cursor: pointer; }
a.rc-click .rc-title { color: inherit; }
/* #161: the create-page custom-board picker uses shared card-list ROWS (not floating tiles). Those
   rows are clickable DIVs (pick a board), so mirror the anchor affordances: pointer + a hover lift +
   a selected ring. No background swap on hover (check-css touch-stuck guard). */
.row-card.rc-click { cursor: pointer; }
.row-card.rc-click:hover { border-color: rgba(201, 162, 75, .55); transform: translateY(-1px); }
.row-card.rc-click.selected { border-color: rgba(201, 162, 75, .9); box-shadow: 0 0 0 1px rgba(201, 162, 75, .5) inset; }
.rc-2lines { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.pz-lock { margin-inline-start: 6px; font-size: 0.85em; opacity: 0.8; }
/* #337: the publication request's state beside the lock on the author's own row. Sent-back states
   (a solution asked for / not approved) are the ones the author owes a move on, so they read louder
   than "awaiting review". Never wraps mid-word - a row title already ellipsises. */
.pz-row-review { margin-inline-start: 6px; padding: .1rem .5rem; border-radius: 999px; font-size: .72rem;
  font-weight: 700; white-space: nowrap; background: rgba(201,162,75,.18); color: #e9c877; }
.pz-row-review.sent-back { background: rgba(220,70,70,.22); color: #ffb3b3; }
.pz-filter-row { display: flex; gap: 8px; margin: 4px 0 10px; flex-wrap: wrap; }
/* #150: when the focused filter scrolls to the top (mobile keyboard), stop short of the sticky topnav. */
.pz-filter-row input { flex: 1 1 180px; min-width: 0; scroll-margin-top: 70px; }
.pz-filter-row select { flex: 0 0 auto; }
.chip.cta.disabled { opacity: .45; pointer-events: none; }
.pz-matches { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 2px; }
/* #129: the Custom board picker under the board tiles. */
.custom-board-pick { margin: 6px 0 4px; }
/* #161: the filter now lives in a shared .pz-filter-row (name input + size select); let that govern
   layout so it matches the other places (no bespoke width/margin on the input). */

/* #128 follow-up (UX audit 20260802-152048): the board rows carry three actions - wrap on narrow. */
.pz-board-list .row-card { flex-wrap: wrap; row-gap: 6px; }

/* #134: the 2D-authored teaching arrows do not follow the 3D projection - hide them there. */
.rules-3d .rules-static-ov { display: none; }

/* ── #216 Game Theater ─────────────────────────────────────────────────────────────────────────
   The category menu, then the theater surface. Both reuse the existing panel/chip/players-bar
   vocabulary; only what is genuinely new to this screen is defined here. */
.theater-pick .pick-title { font-weight: 700; margin: 0 0 .6rem; }
.theater-pick .pick-row { display: flex; align-items: flex-start; gap: .6rem; margin: .55rem 0; cursor: pointer; }
.theater-pick .pick-row input { width: auto; flex: 0 0 auto; margin-top: .2rem; }
.theater-pick .pick-text { display: flex; flex-direction: column; min-width: 0; }
.theater-pick .pick-name { font-weight: 600; }
.theater-pick .pick-sub { font-size: .85rem; color: var(--ink-dim); }

.theater-bar { display: flex; align-items: center; gap: .6rem; margin: 0 0 .6rem; }
.theater-spacer { flex: 1 1 auto; }
.theater .moves { margin: .6rem 0 0; color: var(--ink-dim); }
/* Which side each player is on, next to their name (owner spec) - the colour dot alone does not
   say "First"/"Second", and the theater viewer has no other context for the pairing. */
.theater .p-card .p-side { font-size: .82rem; color: var(--ink-dim); white-space: nowrap; }
/* The controls wrap rather than shrink: five actions do not fit a 360px viewport in one row. */
.theater-controls { flex-wrap: wrap; row-gap: .5rem; }
.theater-empty { text-align: center; }

/* == #470: the tournament surface (/tournaments, /tournament/{publicId}, /tournaments/create) ==
   The server side of tournaments shipped at #458 and #467 and rendered NOWHERE - sixteen files, all
   of them under Server/. This is the first stylesheet it has ever had, and almost nothing in it is
   new geometry. That is the point.

   A tournament row carries a NAME, a state, a board, a timer, a format and an entrant count - more
   competing columns than the /rankings row that already needed every measurement this family has:
   .rc-title wraps inside the card (#275), the single trailing element is rigid so the NAME is the
   item that yields (#309), and below 360px the row takes a second line rather than cutting the name
   to six characters (issue 403). So the rows ARE .card-list/.row-card and inherit all of it; a table
   here would crush its own cells at phone width exactly as the admin tables do (#376). What is added
   is the two boxes that family does not have, and both obey the owner's standing rule that THE BOX
   GIVES, NOT THE STRING (feedback f7d77400 / #257) - shorten a label and the next longer translation
   reopens it.

   .tn-meta is the answer to "six facts and one phone": a WRAPPING line of nowrap facts, so at 360px
   it takes the lines it needs and no single fact is ever split mid-term. It is deliberately not a
   column layout - columns are what crush each other at phone width.

   .tn-seed is #459's rank badge one page over: a min-width keeps the column steady while entry
   orders are single digits, and horizontal padding - never a fixed width - is what lets a
   three-figure seed render whole instead of clipping inside its own box.

   No :hover rule is added here on purpose. Every interactive element on these pages is an existing
   .row-card, .navpill, .chip or `main button`, each already neutralised by the touch guard at the end
   of this sheet (#410) - a new hover rule would have to be declared above it and named inside it. */
.tn-meta, .cl-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .15rem .55rem; }
.tn-fact, .cl-fact { white-space: nowrap; }
/* The trailing state pill is one of .row-card's rigid items (`.row-card > .status-pill`, #309), so
   these two only TINT it: green while a tournament is worth entering, gold while it is under way.
   Everything terminal keeps this sheet's existing neutral `.done`. */
.status-pill.tn-open { background: rgba(76,175,80,.22); color: #a5e0ae; }
.status-pill.tn-live { background: rgba(201,162,75,.20); color: var(--gold); }
/* The name filter takes the filter row's spare width the way the /player opponent filter does; the
   min-width is what stops it collapsing to a few characters when the row wraps at 360px. */
.pg-filters .tn-q, .pg-filters .cl-q { flex: 1 1 auto; min-width: 140px; }
/* The tournament page's own boxes. .tn-head is a .panel.panel-wide, so it caps and centres itself
   (#311) and shares the 880px column of the entrant .card-list beneath it (#447); the rules here
   only lay out INSIDE it. */
.tn-head .tn-meta, .cl-head .cl-meta { margin-bottom: .6rem; }
.tn-byline, .cl-byline { display: flex; flex-wrap: wrap; align-items: center; gap: .1rem .5rem; margin: 0 0 .5rem; }
.tn-byline-label, .cl-byline-label { color: var(--ink-dim); font-size: .85rem; }
/* An organizer's own words, at 360px: `anywhere` is #275's rule - a long unbroken run wraps inside
   the panel instead of pushing past its border. */
.tn-desc, .cl-desc { overflow-wrap: anywhere; margin: 0 0 .8rem; }
/* The entry doors. A wrapping row, so a long localized button label takes its own line at its
   natural size rather than being squeezed inside a plate whose drawn cap does not move (#306/#380). */
.tn-entry, .cl-doors { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.tn-hint, .cl-hint { color: var(--ink-dim); font-size: .82rem; margin: .35rem 0 0; }
.tn-seed { flex: 0 0 auto; min-width: 34px; height: 34px; padding: 0 .4rem; box-sizing: border-box;
    display: inline-flex; align-items: center; justify-content: center; border-radius: 8px;
    background: rgba(255,255,255,.07); color: var(--ink-dim);
    font-weight: 800; font-size: .95rem; font-variant-numeric: tabular-nums; }
/* An entry that was given up STAYS on the list and is marked, matching the server, which keeps the
   row and stops counting it. Dimmed rather than removed: a reader has to be able to see that a seat
   was taken and given up. */
.tn-entrant.tn-out, .cl-seat.cl-out { opacity: .65; }
/* The standing sits UNDER the name rather than in a trailing box. Measured at 360x531 with both:
   a rigid seed badge and a rigid trailing pill leave `.rc-main` ~155px, and `.rc-player`'s cap
   (max-width: min(260px, 100%); text-overflow: ellipsis) then cut a 44-character display name to
   "A player with a ...". That cap is correct and measured - a user-controlled name must never
   widen the page - so the fix is the BOX it is applied inside, never the string: with the standing
   on its own line the name column is ~248px on the same 360px card. This is the only row in the
   family that carries TWO rigid boxes, which is why it is the only one that needed it. */
.tn-entrant .tn-standing { color: var(--ink-dim); }
/* The pager's position line shares the wrapping chip row with its two buttons, so at phone width it
   takes a line of its own instead of squeezing them (the /rankings pager's rule, same shape). */
.tn-range, .cl-range { align-self: center; color: var(--ink-dim); font-size: .85rem; font-variant-numeric: tabular-nums; }

/* == #475 (roadmap step 3b): the start control, the standings and the record of play =============
   THE ORGANIZER'S CONTROL is its own wrapping row under the entry doors rather than a fifth item
   inside them. The doors answer "where do I stand in this"; this answers "may I run it", and an
   organizer who has also entered is being asked both at once - two questions on one line read as one
   row of four buttons at 360px, and the hint under it belongs to this control alone.

   THE TWO TABLES are the only real <table>s on a player-facing screen in this sheet, and the #470
   block above is where the general rule lives: a row of competing columns crushes its own cells at
   phone width, so browse rows, entrant rows and the /rankings board are all .card-list/.row-card.
   These two are the shape that family cannot serve. Their columns are a RANK, a SCORE, an OUTCOME
   word and a link - four short rigid things - and the whole point of a standings table is that the
   numbers line up UNDER each other, which a stack of cards cannot do however wide the phone is.

   So the rule is applied rather than waived, and it is applied to the BOX (owner directive, feedback
   f7d77400 / #257 - shorten a label and the next longer translation reopens it):
     - the short cells never wrap, so "No contest" is never two lines beside a one-line name;
     - the PROSE cell is the one that gives, and it gives in LINES: a display name wraps inside its
       column instead of holding a fixed ribbon open;
     - the BOX around the table scrolls, so if a language ever makes even the wrapped minimum too
       wide, the table moves sideways inside its own column and the PAGE never does (UX-audit C1).

   THE ONE MEASURED CORRECTION, and it is why this block is longer than the markup it styles. Built
   first with the admin tables' shape - the table itself as the scroller (display:block +
   overflow-x:auto, head and body as row groups per #350) - and measured in a REAL 360px column (an
   iframe; a headless window clamps to ~484px, so --window-size alone measures a lie). The page did
   not scroll, but BOTH tables scrolled their own box by 90px and 133px, on every render, with no
   long name in sight: `.rc-player` is `max-width: min(260px, 100%); white-space: nowrap` (#309), and
   inside a block box the anonymous table has no definite width for that 100% to resolve against, so
   the 260px arm won and nowrap made the name column's MINIMUM 308px. The Score column - the whole
   reason a standings table is a table - sat off-screen until the reader dragged it.
   The fix is the box in both halves: the WRAPPER scrolls and the table is a plain
   `display: table; width: 100%`, so auto layout fits it to the 320px column; and inside these two
   tables only, a name WRAPS rather than holding a 260px nowrap ribbon. The #309 invariant it is
   overriding - a user-controlled name must never widen the page - is kept by the other mechanism
   available here: `overflow-wrap: anywhere` means a 44-character unbroken name takes more LINES, not
   more pixels. Re-measured after: 360px page, 0px of table scroll, both tables whole. */
.tn-organizer { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-top: .7rem; }
.tn-organizer .tn-hint { margin: 0; }
/* The tables share the entrant list's 880px column (#311/#447), so a heading and the block it titles
   start at the same left edge - and this box, not the table, is the scroller. */
.tn-tablebox { max-width: 880px; margin: .6rem auto 1rem; overflow-x: auto; }
.tn-table { width: 100%; border-collapse: collapse; font-size: .9rem; }
.tn-table th, .tn-table td { text-align: start; padding: .45rem .5rem; vertical-align: top;
    border-bottom: 1px solid rgba(255,255,255,.07); }
.tn-table thead th { color: var(--ink-dim); font-weight: 600; font-size: .72rem;
    text-transform: uppercase; letter-spacing: .4px; }
/* The rigid columns. A rank, a score, one outcome word and one link: none of them is a sentence, so
   none of them is allowed to become two lines, and none of them is what a narrow box takes from. */
.tn-table .tn-num, .tn-table .tn-outcome, .tn-table .tn-open, .tn-table thead th { white-space: nowrap; }
.tn-table .tn-num { font-variant-numeric: tabular-nums; }
/* The Score is the format's own number and is only ever SHOWN - the weight makes it findable down a
   column, and nothing here formats, rounds or re-scales it. */
.tn-table .tn-score { font-weight: 700; color: var(--ink); }
/* The prose column: a display name is user-controlled, and here it is the item that yields. The
   min-width keeps the column from collapsing to a couple of characters when the box is wide enough
   for it not to have to; a meeting holds two names, so it asks for more. */
.tn-table .tn-who { min-width: 128px; }
.tn-table .tn-meeting .tn-who { min-width: 150px; }
/* ...and the override the measurement above bought: inside these two tables a name takes lines
   rather than a fixed 260px ribbon. `anywhere` is what keeps #309's real promise - an unbroken
   44-character name still cannot widen anything. */
.tn-table .rc-player { max-width: none; white-space: normal; overflow: visible;
    text-overflow: clip; overflow-wrap: anywhere; }
.tn-vs { color: var(--ink-dim); margin: 0 .3rem; }
.tn-bye { color: var(--ink-dim); }

/* == #493 (roadmap track A, step 6b): the venue, and the organizer's result control ==============
   THE VENUE is a `.tn-fact` and needs no geometry of its own - it joins the wrapping line of nowrap
   facts the #470 block above measured, on the browse row and on the tournament page alike. The rule
   `.tn-fact { white-space: nowrap }` already covers it, so `.tn-venue` exists as a HOOK for the
   narrow probes and for anything later that wants to find it, not as a second opinion on its layout.

   THE RESULT CONTROL is the part that had to be measured, and #493 decision 6 states the hazard in
   its own words: "a third control in the outcome cell is exactly what crushes a table at phone
   width". Three buttons laid out BESIDE the outcome word would make the outcome column as wide as
   three localized outcome labels in a row - ~330px in English before a single one of them is armed,
   and the armed label is longer than the word it replaces - and the `.tn-who` column holding two
   display names would be squeezed against it inside a 320px box until the wrapper scrolled on every
   render. That is the exact failure the #475 block above records having measured and fixed once.

   So the cell keeps its SHAPE and grows DOWNWARD: the word stays on its own line where it always
   was, and the controls are a wrapping flex row under it. The column's width is then the widest
   single button rather than the sum of three, and at 360px they stack. Two consequences are
   deliberate:
     - `white-space: normal` on the actions box and on the buttons. `.tn-table .tn-outcome` is nowrap
       (it is a short cell, and that rule is right for the WORD), but the armed label is a sentence -
       "Confirm Second won" - and a sentence that may not wrap is a column that widens instead. The
       BOX gives in lines, never the string (owner directive, feedback f7d77400 / #257): shortening
       the armed copy would only move the problem to whichever language spells it longest.
     - the buttons are `main button.sm`, which is #401's 40px minimum tap target - three of them in
       one cell is three things a thumb has to hit without hitting its neighbour.
   The `max-width` is what keeps the flex row from opening the column on a WIDE screen: without it
   the three buttons sit in one line and the table is back to a 330px outcome column on the desktop
   it was not measured at.

   THE ARMED STATE reuses `.armed` (#157) unchanged - a red ring and a pulse, no background swap, so
   a touch tap never leaves a button stuck looking armed. Nothing here is `.chip-danger`: this is a
   destructive-SHAPED control (a mis-press re-advances the tournament) rather than a destructive one,
   and painting all three results red would say that recording a win is a thing to be afraid of. */
.tn-table .tn-outcome .tn-record-actions {
    display: flex; flex-wrap: wrap; gap: .3rem; max-width: 12rem;
    margin-top: .4rem; white-space: normal; }
.tn-table .tn-outcome .tn-result { white-space: normal; text-align: center; }
/* The hint and the refusal sit above the table and take their CAP from the #447 page column, which
   already claims every `main > p` - so neither declares a max-width of its own. What both DO have
   to declare is `auto` in the inline slot: `.tn-hint`'s `margin: .35rem 0 0` is (0,1,0) and the
   family's `main > p` is (0,0,2), so the shorthand WINS and would hold a capped block against the
   shell's inline-start edge while looking perfectly ordinary - the trap #447 records as the one that
   swallowed its first attempt, and PageColumnAlignmentTests is what says so out loud. */
.tn-record-hint { margin: .3rem auto 0; }
.tn-record-error { margin: .4rem auto 0; }

/* == #613: club-team entry and standings =======================================================
   The picker and lineup stack inside the existing wide panel. At 360px every label and control
   gets the full available line; names wrap and the selection never becomes a squeezed button row.
   Snapshot cards reuse the responsive row family, while the aggregate table reuses the measured
   horizontal wrapper above. */
.tn-team-entry { margin-top: .7rem; }
.tn-team-entry .field { margin-bottom: .65rem; }
.tn-lineup { min-width: 0; margin: .35rem 0; padding: .65rem; border: 1px solid var(--line);
    border-radius: 10px; }
.tn-lineup legend { padding: 0 .25rem; color: var(--ink-dim); font-size: .85rem; }
.tn-lineup-member { display: flex; align-items: center; gap: .5rem; min-height: 40px;
    overflow-wrap: anywhere; }
.tn-team-card.tn-out { opacity: .65; }
.tn-team-lineup { display: flex; flex-wrap: wrap; gap: .2rem .7rem; margin-top: .25rem; }
.tn-team-lineup .rc-player { max-width: 100%; }
.tn-team-actions { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .45rem; }
.tn-team-standing-row { background: rgba(255,255,255,.035); }
.tn-team-member-row .tn-who { padding-inline-start: 1.2rem; }

/* == #479 (roadmap track D, step 1b): the club surface (/clubs, /club/{publicId}, /clubs/create) ==
   ALMOST NOTHING HERE IS NEW GEOMETRY, and that is the point twice over. #474 shipped the whole club
   domain with no stylesheet at all, and what a club browse row, a club page and a roster row need is
   what #470 already measured one surface over at 360px: a wrapping line of nowrap facts rather than
   columns that crush each other, a by-line, an author's own words that wrap inside their panel, a
   wrapping row of doors, and a dim hint. So the club classes JOIN those selector lists above rather
   than re-declaring their values next door - one geometry in one place, which is what stops a club
   row and a tournament row drifting apart while both look measured.

   THE ROSTER IS .card-list/.row-card, NOT A TABLE. Same reason the tournament browse is: a row of
   competing columns crushes its own cells at phone width exactly as the admin tables do (#376), and
   a seat carries a NAME, a role word and a standing word - three things, two of them translated, and
   translated words are the thing that must never be what gives (owner directive, feedback f7d77400 /
   #257: shorten a label and the next longer translation reopens it).

   NO BADGE HOLDS A WORD IN A RIGID FRAME. #459's rank badge and #470's seed badge are rigid boxes
   because what is in them is a NUMBER. A role and a standing are strings from a locale family, so
   they go under the name as a wrapping .rc-sub - which is also #475's own measurement applied before
   it could bite: a SECOND rigid box beside a rigid trailing pill left .rc-main ~155px at 360x531 and
   .rc-player's cap then cut a 44-character display name to "A player with a ...". The row's only
   trailing element on the browse is the visibility pill, and on a roster row it is .rc-actions, which
   #309 deliberately leaves OUT of the rigid set because it is a wrapping button row whose second line
   is intended (#306). The box gives; the string never does.

   No :hover rule is added here on purpose, for the reason the #470 block states: every interactive
   element on these pages is an existing .row-card, .navpill, .chip or `main button`, each already
   neutralised by the touch guard at the end of this sheet (#410). */
/* The ONE tint this surface adds. A club a reader can see AND that is not listed is one they are
   already in - the browse shows a Private club to its own members only - so the pill says why it is
   there. Public keeps the sheet's neutral pill: on an open browse every row would carry the tint,
   which is a colour that means nothing. */
.status-pill.cl-private { background: rgba(201,162,75,.20); color: var(--gold); }
/* Where THIS reader stands, wherever it is shown: on the browse row it is one of the .rc-sub facts
   and on the club page it is one of the .cl-meta facts. Dim in both, because it is a note about the
   reader beside facts about the club, and it is the same note in both places. */
.cl-standing { color: var(--ink-dim); }
/* #610: team controls reuse the club's wrapping geometry; only their local spacing and archived
   state need names. At 360px the heading action and roster actions wrap instead of squeezing copy. */
.cl-teams-head { justify-content: space-between; }
.cl-team-row .rc-sub { overflow-wrap: anywhere; }

/* Issue 625 (roadmap track D, step 2b): the follow control and the follow surface's own rows.
   ONE wrapping row geometry, reused by all four places this feature needs one - the control on
   /player and /club, the stream's "show more", and the two list pagers - for the reason .cl-doors
   above states: at 360px a long localized label takes its own line at its natural size rather than
   being squeezed inside a plate whose drawn cap does not move (#306/#380).

   .follow-note is the sentence a refusal is. It claims the WHOLE row (flex-basis 100%), so the
   explanation sits under the control it is about instead of being wedged beside it and cut - the
   #403 measurement, one surface over: the box gives, the string never does. */
.follow-box { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: .5rem 0 0; }
.follow-note { flex: 1 1 100%; margin: .15rem 0 0; font-size: .85rem; overflow-wrap: anywhere; }
/* The stream row's sentence is the only `.rc-sub` in this sheet that carries USER-CONTROLLED text:
   a finished game's OPPONENT NAME, and a club entry's tournament and team names, none of which the
   server caps a length on. `.rc-sub` has no overflow rule of its own and `overflow-wrap` defaults to
   `normal`, so an unbroken name is a single unbreakable run and the box it is in is `overflow:
   visible` - measured in the #309 matrix (Application\test-row-name-fit.ps1, a 64-character name at
   360x531 in every shipped locale) the line laid out 447px wide inside a 149px column and painted
   the remaining 298px straight UNDER the row's "Open game" pill and 161px past the card's own right
   edge. That is #309's reported shape exactly, one element down the same column: the name that ran
   under the trailing pill there was the title's, here it is the sub-line's, and the reason is the
   same - a text box with no rule for a string longer than its container. THE BOX GIVES, NOT THE
   STRING (owner directive, feedback f7d77400 / #257), so the line wraps inside its column rather
   than the name being cut or the column being widened; the identical one line `.cl-team-row .rc-sub`
   above already is for a team's name. Scoped to the STREAM row, which is the only one of the four
   /feed ships whose sub-line is not just a timestamp. */
.fd-entry .rc-sub { overflow-wrap: anywhere; }
.team-manage .team-add { margin-top: 1rem; }
.status-pill.team-archived { background: rgba(255,255,255,.08); color: var(--ink-dim); }

/* == #447 THE PAGE COLUMN - a heading is aligned to the block it titles ========================
   Client feedbacks b0b4988e + 1428323a, three minutes apart, one viewport (1295x724): "everything
   is centred, but THIS has moved left". Measured on the shipped stylesheet, shell content box
   127..1167: on /queue the <h1> and the .live-stats line under it began at 127 while the .panel
   they title began at 398 - 271px off. On /games the <h1>, the <h2>s, the .games-group headers and
   the .empty-note lines began at 127 while the .card-list rows they title began at 207 - 80px off.

   THIS IS #311'S MIRROR, not a second bug. #311 centred every block that CAPS itself (.menu,
   .panel, .card-list, ...): .app is 1040px and centred, so a 460px block with `margin-inline: 0`
   sat against its inline-start edge. The rules here declare NO cap at all, so they fill the whole
   1040px shell and their text starts at the shell edge while every capped neighbour is centred
   inside it - the identical symptom, and invisible on a phone for the identical reason (the shell
   is narrower than every cap there, so everything fills its column). It is outside
   Application\test-shell-block-centring.ps1's input set BY CONSTRUCTION: that sweep is over fixed
   caps, and an uncapped rule declares none.

   ONE COLUMN PER PAGE, not one cap for every heading. /queue titles a 460px .panel and /games an
   880px .card-list, so a single global cap would land on one and miss the other by 210px.
   --page-col is the CONTENT width of the page's own content column and the page-level text takes
   it, so a heading's first character lands on the first character of the block below it (a
   .panel's 460px cap is its content box; its 1.1rem padding is exactly what the heading clears).

   Held by Caracri.Web.Tests/PageColumnAlignmentTests.cs - every page's top-level blocks, derived
   from the razor and from this sheet, so the next uncapped page-level block fails until it is
   either in the column or classified.

   @supports: a browser with no :has() cannot resolve the wide arm, and half a column is worse than
   none - it would take /games from 80px off to 210px off the other way. There it keeps the shipped
   rendering instead of a new misalignment.
   ============================================================================================ */
@supports selector(:has(*)) {
    main { --page-col: 460px; }
    /* The wide arm: where the page's own content column is the 880px list/panel, that is the
       column the headings above it belong to. */
    main:has(> .card-list), main:has(> .panel-wide) { --page-col: 880px; }

    /* The page-level text: every heading and every note that is a direct child of <main>. A block
       INSIDE a capped box is already in its column (the cap cannot bind there), so this changes
       nothing for it. */
    main > h1, main > h2, main > h3, main > p,
    /* issue 533: the heading + its own filter control, now one row instead of two. It is the same
       page-level text it was as a bare <h2>, so it takes the same column. */
    main > .section-head,
    /* ...and the page-level blocks that are not text: the hot-seat tab strip (the .tab-strip next
       door has carried its own 460px cap since #311), the /games filter row and its applied-filter
       chips, and the puzzle/board head row that sits above a .panel. */
    /* ...and, since issue 625, the follow surface's own wrapping row: the stream's "show more" and
       the two list pagers sit directly under a .card-list, so an uncapped one would run the whole
       1040px shell while the list it belongs to is centred inside 880px of it. */
    main > .tabs, main > .pg-filters, main > .pg-chips, main > .pz-editor-head, main > .follow-box {
        max-width: var(--page-col); margin-inline: auto;
    }
    /* A .chip is inline-flex, so auto margins cannot centre it - it is offset to the column's
       inline-start instead. max(0px, ...) because below the cap the column IS the shell, and a
       negative margin there would pull the chip out of the page. */
    main > .chip { margin-inline-start: max(0px, calc((100% - var(--page-col)) / 2)); }
}


/* Release Notes (#572): one permanent, shareable page per version. */
.rn-page { width: 100%; max-width: 880px; margin-inline: auto; }
.rn-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem;
    margin-bottom: 1.25rem; }
.rn-header h1 { margin-bottom: .35rem; }
.rn-version { display: flex; align-items: center; gap: .55rem; margin: 0; font-size: 1.1rem;
    font-weight: 700; color: var(--gold, #c9a24b); }
.rn-date { margin: .25rem 0 0; opacity: .72; }
.rn-share { display: inline-flex; align-items: center; gap: .4rem; flex: none; }
.rn-share .share-ico { width: 1rem; height: 1rem; }
.rn-section { margin: 1.25rem 0; }
.rn-section > h2, .rn-history > h2 { font-size: 1rem; text-transform: uppercase;
    letter-spacing: .06em; opacity: .76; }
.rn-entry-list { display: grid; gap: .75rem; }
.rn-entry { padding: 1rem 1.1rem; border: 1px solid rgba(255,255,255,.10); border-radius: 14px;
    background: rgba(255,255,255,.035); }
.rn-entry h3 { margin: 0; font-size: 1.08rem; }
.rn-entry p { margin: .5rem 0 0; line-height: 1.55; }
.rn-entry-link { display: inline-flex; align-items: center; gap: .4rem; color: inherit;
    text-decoration: none; }
.rn-entry-link:hover { color: var(--gold, #c9a24b); }
.rn-entry-link.locked { opacity: .78; }
.rn-lock { font-size: .8em; }
.rn-open { font-size: 1.3em; line-height: .8; }
.rn-history { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid rgba(255,255,255,.10); }
.rn-history-links { display: flex; flex-wrap: wrap; gap: .5rem; }
.rn-contributors { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; margin-top: .8rem;
    font-size: .9rem; opacity: .9; }
.rn-contributor { display: inline-flex; align-items: center; gap: .35rem; }
a.rn-contributor { color: inherit; text-decoration: none; }
a.rn-contributor:hover { color: var(--gold, #c9a24b); }
.rn-contributor-avatar { width: 1.7rem; height: 1.7rem; border-radius: 50%; object-fit: cover;
    font-size: .62rem; }
.rn-contributor-loading { width: 5rem; height: 1.4rem; border-radius: .7rem;
    background: rgba(255,255,255,.08); }
.rn-contributor-deleted { opacity: .72; }
.rn-not-found { margin-inline: 0; }
/* #729: pre-history community acknowledgements - deliberately NOT .rn-entry/.rn-section, so it
   cannot be mistaken for a version change entry at a glance. */
.rn-prehistory { margin-top: 1.75rem; padding: 1rem 1.1rem; border: 1px dashed rgba(201,162,75,.45);
    border-radius: 14px; background: rgba(201,162,75,.06); }
.rn-prehistory h2 { margin: 0 0 .35rem; font-size: 1rem; text-transform: uppercase;
    letter-spacing: .06em; opacity: .76; }
.rn-prehistory-intro { margin: 0 0 .85rem; opacity: .8; font-size: .92rem; }
.rn-prehistory-list { display: grid; gap: .75rem; }
.rn-prehistory-entry h3 { margin: 0; font-size: 1.02rem; }
.rn-prehistory-entry p { margin: .4rem 0 0; line-height: 1.5; }
.about-release-notes { font-weight: 700; }
@media (max-width: 520px) { .rn-header { align-items: stretch; flex-direction: column; }
    .rn-share { align-self: flex-start; } }

/* == THE TOUCH GUARD IS THE LAST WORD IN THIS SHEET (#410) ====================================
   Touch devices: hover is only a brightness lift (never a skin swap), and on hoverless/coarse
   pointers even that is off - a tap can never leave any lingering visual state.

   This block is a NEUTRALISER: it wins only while nothing overrides it, and at equal specificity
   that is purely source order. It used to sit mid-sheet (~line 2143) and `.row-card.rc-click:hover`
   (#161) was declared 160 lines LATER at the same (0,3,0), so the guard silently lost - a tap on
   a /hotseat or /create board-picker row kept its 1px lift, and inside .custom-board-list's clip
   box that lift carried the card's own top border out of view (feedback 04595a1f, SamsungBrowser
   360x531; measured gapTop 0 -> -1). Nothing may be appended after this block: any :hover rule
   that MOVES (transform) or REPAINTS (filter/background) its element must be declared above it
   and neutralised inside it. Application\check-css.ps1 rule 1 enforces both halves.
   ============================================================================================ */
@media (hover: none), (pointer: coarse) {
    .chip:hover, .chip-danger:hover, .tile:hover, .navpill:hover,
    main button:hover, .btn:hover, .provider:hover, .report-button:hover, main button[type=submit]:hover,
    .menu-tile:hover, .menu-tile.primary:hover { filter: none; }
    .menu-tile:hover { transform: none; }
    a.row-card:hover { border-color: rgba(255,255,255,.10); transform: none; }
    /* #410. Naming `.row-card.rc-click` is required, not cosmetic: adding `a` to the line above
       would still lose, because `.row-card.rc-click:hover` is (0,3,0) against `a.row-card:hover`'s
       (0,2,1). All four rc-click sites are covered (VsAiPanel, BoardPicker, both Puzzles lists). */
    .row-card.rc-click:hover { border-color: rgba(255,255,255,.10); transform: none; }
    /* ...and because this block is now LAST, that border-color reset also out-orders
       `.row-card.rc-click.selected` (equal (0,3,0), declared ~70 lines earlier), which the mid-sheet
       position had hidden. Measured at 360x531 coarse: the selected card's border went gold .9 ->
       white .10 under the stuck hover - the picked board stopped looking picked. So the selected
       ring is re-asserted one specificity step up. A guard that erases a REAL state is not a guard. */
    .row-card.rc-click.selected:hover { border-color: rgba(201, 162, 75, .9); }
    /* #410, same class, surfaced by the check-css sweep this fix generalised - every other :hover
       rule in the sheet that moves or repaints its element. .rev-save-pos is a .chip (brightness
       already covered above) but carries its own 1px lift; .cotw-card and .board-topbar.can are
       whole tappable surfaces that stayed lifted/brightened after a tap. */
    .rev-save-pos:hover { transform: none; }
    .cotw-card:hover { filter: none; transform: none; }
    .board-topbar.can:hover { filter: none; }
}
