/* GENERATED by tools/gen-css.mjs from the files listed in it -
   do not edit this file. Edit src/<id>/style.css (or
   src/shared/styles/platform.css for the platform chrome) and run npm run gen. */
/*
 * PLATFORM CHROME: what every game page wears - the card sprite, panels,
 * buttons, the log, the setup screen, the review bar, the P2P status line, the
 * coach panel, the game picker.
 *
 * IT ALSO STILL HOLDS PER-GAME RULES, and that is not by design. The sheet grew
 * by appending, so the early games' board rules (ring poker at "ring poker
 * board", exchange poker's board, locked cards and swap markers, duel poker's
 * board and its Play-4 counter, grid poker's hand centering) sit in here among
 * the shared ones, and a few later games left a stray rule too.
 *
 * They were LEFT WHERE THEY ARE when this file was split out, deliberately.
 * Moving a rule changes which other rules it beats, and no static check can see
 * that: two rules of equal specificity are decided by order alone. Gathering
 * them into their games' own files is a real change with a visual risk, and it
 * wants a browser and a person, not a codemod. Doing it here would have meant
 * claiming the split changed nothing while it changed the cascade.
 *
 * So: a new game's rules go in src/<id>/style.css. Nothing new belongs here
 * unless every game wears it.
 */

/* Grid Poker web app styles. Card faces come from the 14x4 sprite atlas
   (assets/cards.png, 64:89 cells); .card elements size via --cw and position
   the sprite with percentages, so cards can render at any size. */

:root {
    /* Card width, fluid with the viewport.
     *
     * This used to be a flat 72px with a single step down to 56px at the 560px
     * breakpoint, which meant the sheet could only ever SHRINK from its 1200px
     * design: cards were the same size on a 2560px display as in a 1000px
     * window ("too small on my laptop"), and a 360px phone got the same 56px
     * card as a 560px one ("too wide on my phone").
     *
     * The curve deliberately leaves the middle alone. Below 1200px this clamps
     * to exactly the old 72px, so every board tuned against that number - and
     * the ~30 container rules that derive their own card size from it - behave
     * as before. Growth only happens above 1200px, where the page cap now
     * releases width for it (see #screen-game), and it is capped at 96px: the
     * full-art atlas has 256px cells, so 96px is still a downscale at
     * devicePixelRatio 2-3 and never asks the sprite to upscale.
     *
     * 32px + 3.33vw passes through (1200, 72) and (1920, 96). The page cap
     * grows FASTER than the cards over that span (480px of page for 24px of
     * card), so a nowrap row's capacity in cards goes up, never down - which is
     * what keeps this from breaking a board that exactly fitted at 72px.
     * Downward continuity below 560px is handled in that breakpoint's block.
     * Rounding to whole pixels happens in the @supports block below. */
    --cw: clamp(72px, calc(32px + 3.33vw), 96px);
    --felt: #1e5c38;
    --felt-dark: #17492d;
    --ink: #f2efe9;
    --panel: rgba(255, 255, 255, 0.07);
    --panel-border: rgba(255, 255, 255, 0.14);
    /* The speech-balloon surface: OPAQUE, and light against the dark felt.
       Every other panel here is white at 7% over the felt, which is right for
       something that IS the page. A balloon sits in front of it and has to read
       as a separate object, so it inverts instead: this is the classic comic
       speech bubble, it gives 12.5px text the best contrast on the site, and it
       is what makes an 8px tail visible at all. A near-felt opaque green was
       tried first and measured 15 per channel off the felt, so the tail did not
       read. --balloon-edge is the border and the tail's rim. */
    --balloon: #f5f2ec;
    --balloon-ink: #23261f;
    --balloon-edge: rgba(0, 0, 0, 0.22);
    --accent: #ffd166;
    --accent-ink: #3a2c00;
    --red: #ff9c9c;
    /* Suit text inks, mirroring in text the distinction the card sprites draw
       in ink (tools/card-palettes.mjs, palette `hue`): hearts and spades stay
       conventional, diamonds warms toward orange, clubs cools toward blue. They
       are LIGHTENED relatives of the sprite inks, not the same values - suit
       text sits on the dark felt, where #d94a2f and #26384a are unreadable - so
       the hue angles are carried over (~16 deg for diamonds, 210 deg for clubs)
       at a lightness that reads. Hearts keeps the existing --red and spades
       keeps --ink, so only the SECOND suit of each family is new, which is what
       makes the change legible rather than loud.
       --red is deliberately NOT renamed: it doubles as the platform's warning
       color (.em-warn, .dos-danger-text), so those keep their meaning.
       Four games (Claim the Crown, Liar, Deck of Spies, Club Fight) picked
       their own, more saturated hearts red inline rather than --red. Those keep
       it, and their diamond counterpart is derived the same way this one is:
       same saturation and lightness, hue rotated ~16 deg toward orange
       (#ff7b72 -> #ffa172, #ff6a6a -> #ff926a). Derive any future one that way
       so every deck reads as the same idea at a different intensity.
       --blue-club is family-neutral, so all four reuse it for clubs.
       --red-diamond and --blue-club themselves are set in the deck block
       below, because the four-color deck redefines them. */
    /* The four-color deck's text inks (body.deck-fourcolor below), lightened
       from its sprite inks the same way: the `fourcolor` palette paints
       diamonds #1976d2 and clubs #388e3c, and these are the 200 tints of the
       same two hues, about 6:1 on the felt like --red. The `-ink` pair is for
       suit text on a LIGHT surface (a balloon, a cream panel, the accent
       button), where a game already keeps its own dark inks. */
    --fc-diamond: #90caf9;
    --fc-club: #a5d6a7;
    --fc-diamond-ink: #1565c0;
    --fc-club-ink: #2e7d32;
}

/* ---------- deck colors ----------
   The card atlases and the two suit-text inks that change with the deck, set
   per DECK rather than per rule (src/shared/ui/deckcolors.ts). Every .card
   rule below reads its atlas from --atlas-*, and every suit-text rule on the
   site colors diamonds with --red-diamond and clubs with --blue-club, so one
   class redefines all of them at once and no rule's specificity changes.

   `.deck-fourcolor` is the class the boot script puts on <body> when the
   setting is Four colors (never on a color-only game's pages, which carry
   data-deck="two"). `.deck-twocolor` restores the default under a four-color
   ancestor, for the Settings_Page's side-by-side samples. Either works on any
   element, because custom properties inherit: a flight ghost appended to
   <body> picks the body's deck up like every card in the page.

   Relative url()s in a custom property resolve against the stylesheet whose
   var() uses them, and the .card rules sit in this same file, so `assets/`
   means site/assets/ from every page depth. */
:root, .deck-twocolor {
    --atlas-full: url('assets/cards.png');
    --atlas-mini: url('assets/cards-mini.png');
    --atlas-simple: url('assets/cards-simple.png');
    --red-diamond: #ffb499;
    --blue-club: #d6e0eb;
}
.deck-fourcolor {
    --atlas-full: url('assets/cards-fourcolor.png');
    --atlas-mini: url('assets/cards-mini-fourcolor.png');
    --atlas-simple: url('assets/cards-simple-fourcolor.png');
    --red-diamond: var(--fc-diamond);
    --blue-club: var(--fc-club);
}

/* Snap the fluid --cw to a whole pixel. The atlas paints at background-size
   1400% x 400% of the card box with a percentage background-position, so the
   selected cell's offset is exactly -(col x card width). A fractional width
   (61.777px, say) puts both the box edges and the atlas sampling off the
   device-pixel grid, and the bilinear resampling visibly softens every face.
   Rounding down realigns the two.
   The clamp above stays UNROUNDED as the fallback, and this must stay in
   @supports. A custom property has no parse-time fallback, so an unsupported
   round() would make every var(--cw) use guaranteed-invalid and blank the cards
   on every page. Without round() (Chrome before 125) the cards are fractionally
   sized: soft, but working. Every other rounded card width in the sheet
   (.duel-board, .ring-board) follows this pattern for the same reason. */
@supports (width: round(down, 1.5px, 1px)) {
    :root { --cw: round(down, clamp(72px, 32px + 3.33vw, 96px), 1px); }
}

* { box-sizing: border-box; }

/* The [hidden] attribute must ALWAYS win, even over author display rules
   (display: grid/flex on a class would otherwise override the UA default and
   leave "hidden" elements rendered - e.g. an invisible full-screen overlay
   swallowing every click). */
[hidden] { display: none !important; }

/* The felt-dark canvas behind everything. Setting it on <html> (not just the
   body gradient) means the split-second between pages during a navigation
   shows dark felt, not the browser's default white - no white flash. */
html { background: var(--felt-dark); }
body {
    margin: 0;
    min-height: 100vh;
    background: radial-gradient(ellipse at 50% -20%, var(--felt), var(--felt-dark) 70%);
    color: var(--ink);
    font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif;
    line-height: 1.4;
}

.topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 18px;
    background: rgba(0, 0, 0, 0.25);
}
.topbar h1 { font-size: 20px; margin: 0; font-weight: 600; letter-spacing: 0.4px; }
.homelink { color: var(--ink); text-decoration: none; margin-right: 6px; }
/* Wraps because the three controls (All games / Undo / New game) add up to
   323px, so on a 320px screen the row hung 3px off the right edge and took the
   whole document sideways with it. .topbar already wraps at that breakpoint;
   the inner row has to be allowed to as well or the wrap has nothing to do. */
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; }
/* The compact step above the phone breakpoint: src/shared/ui/topbar-fit.ts
   sets .compact when the labelled row would wrap or overflow, and the row
   takes the phone's shape (icon-only buttons, an ellipsized title) at the
   desktop's type size. The 560px rule further down does the same at phone
   widths for every page, so this only ever applies above it. */
.topbar.compact { gap: 8px; }
.topbar.compact h1 { flex: 1 1 auto; min-width: 0; }
.topbar.compact h1 .pagehome {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.topbar.compact .toolbar { flex: none; flex-wrap: nowrap; gap: 6px; }
.topbar.compact .toolbar .btnlabel { display: none; }
.topbar.compact .toolbar .btn { padding: 9px; }
.topbar.compact .toolbar .btnicon { width: 18px; height: 18px; }

/* The title is a link back to this game's own start screen (and, on the site
   pages, back to the hub). It reads as a heading, not as a link, until you
   reach for it: an underline in the topbar would compete with the buttons
   beside it, and the whole bar is chrome, not content. */
.pagehome { color: inherit; text-decoration: none; }
.pagehome:hover { color: var(--accent); }
.pagehome:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }

/* ---------- the Beta pill ----------
 *
 * Stamped by tools/gen-pages.mjs on every game in BETA (pages/publish.config.mjs):
 * playable, and not play-tested yet. Three places wear it, and one class covers
 * all three because it is the same claim each time: beside the title in the
 * game's own topbar, in the .gamestatus row of its hub tile, and inline in the
 * hub's one-line explanation of what it means.
 *
 * COOL BLUE, NOT THE GOLD ACCENT, and that is the whole color decision. Gold
 * is this sheet's "act on me" ink, and .gamebadge already spends it on the save
 * pill ("Game in progress") that can sit in the very same row. A second gold
 * pill beside it would read as another call to action instead of a caveat, and
 * the two would be hard to tell apart at 12px. Uppercase and letter-spaced so
 * it reads as a label rather than as a word in the sentence.
 *
 * It lives here rather than next to .gamebadge in src/split-court/style.css
 * because it is platform chrome (the topbar wears it), and platform.css is
 * FIRST in the gen-css.mjs cascade: a shared name like .gamebadge.beta declared
 * here would lose an equal-specificity fight with split-court's .gamebadge. A
 * class of its own has no such fight to lose. */
.betaflag {
    flex: none;
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(126, 200, 227, 0.14);
    border: 1px solid rgba(126, 200, 227, 0.45);
    color: #a9dcee;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    white-space: nowrap;
}
/* ---------- the Disabled pill ----------
 *
 * Stamped on every game in DISABLED (pages/publish.config.mjs), with that
 * game's own reason as the tooltip. It can only ever be seen on LOCALHOST,
 * because a disabled game has no page and no hub tile in the public build - and
 * localhost being the only place these games are reachable is precisely why the
 * pill is needed: a game that will never ship looked identical to one that just
 * did.
 *
 * RED, against the Beta pill's cool blue and .gamebadge's gold. The three say
 * different things and a player should not have to read them to tell them
 * apart: gold "act on me", blue "rough edges", red "not going out". Same shape
 * and metrics as .betaflag so a row holding either keeps one rhythm. */
.disabledflag {
    flex: none;
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(255, 106, 106, 0.14);
    border: 1px solid rgba(255, 106, 106, 0.45);
    color: #ff9c9c;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    white-space: nowrap;
}
/* ---------- the Local only pill ----------
 *
 * Stamped on every registered game that is in NO publish list (not APPROVED,
 * BETA, or DISABLED in pages/publish.config.mjs), which is where a new game
 * starts. Like Disabled it can only ever be seen on LOCALHOST, because the
 * public build has no page and no tile for these games. Without it a game
 * nobody has classified yet looked exactly like an approved one.
 *
 * VIOLET, against Beta's cool blue, Disabled's red, and .gamebadge's gold, so
 * the four can be told apart without reading them. Not green, because green
 * would say "approved", the opposite of the claim. Same shape and
 * metrics as .betaflag so a row holding any pill keeps one rhythm.
 *
 * Contrast at the lightest felt (#1e5c38), the worst case, is 5.78 to 1 on a
 * hub tile, 4.53 to 1 on the .flagnote line, and 6.94 to 1 in the topbar. All
 * clear the 4.5 to 1 bar, which applies because 11px bold is not large text. */
.localflag {
    flex: none;
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(190, 150, 255, 0.14);
    border: 1px solid rgba(190, 150, 255, 0.5);
    color: #ebdeff;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    white-space: nowrap;
}
/* ---------- the Family pill ----------
 *
 * Stamped by tools/gen-pages.mjs beside the site name in the hub and site page
 * topbars of the FAMILY build only (build-public.mjs --tier family), so a family
 * member can tell the family site from the public one at a glance. Localhost
 * and the public build never carry it.
 *
 * APRICOT, against Beta's cool blue, Disabled's red, Local only's violet, and
 * .gamebadge's gold, so it reads as none of the four. Same shape and metrics
 * as .betaflag so it hangs off the title the same way.
 *
 * Contrast at the lightest felt (#1e5c38): 5.55 to 1 against the bare felt and
 * 6.13 to 1 in the topbar, where it sits. Even over its own tint on bare felt,
 * a place it never appears, it is 4.68 to 1. All clear the 4.5 to 1 bar for
 * 11px bold text. */
.familyflag {
    flex: none;
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(255, 159, 67, 0.14);
    border: 1px solid rgba(255, 159, 67, 0.5);
    color: #ffcf9f;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    white-space: nowrap;
}
/* ---------- the Family only pill ----------
 *
 * Stamped on every game in FAMILY_ONLY (pages/publish.config.mjs): live, but
 * kept to the family site, with the map's reason as the tooltip. Like Disabled
 * it can only be seen on localhost and the family site, because the public
 * build has no page and no tile for these games. It is the one pill that
 * shares a game with another, so a family-only Beta game wears Beta and then
 * this, in one .flagtitle line or one .gamestatus row.
 *
 * APRICOT, the Family pill's colors, because both say "the family site". The
 * two never meet: the Family pill sits beside the SITE name, and this one
 * beside a GAME's. Same shape and metrics as .betaflag so a row holding both
 * keeps one rhythm.
 *
 * The tint and border are .familyflag's. The text is a lighter apricot,
 * because this pill also sits on the hub's .flagnote line, over the lighter
 * .setup-card panel, where .familyflag's #ffcf9f measures 4.0 to 1 over the
 * pill's own tint. At the lightest felt (#1e5c38), the worst case, #ffe3c6
 * measures 4.66 to 1 on the .flagnote line, 5.96 to 1 on a hub tile, and 7.08
 * to 1 in the topbar, each over the pill's own tint, and 6.45 to 1 against the
 * bare felt. All clear the 4.5 to 1 bar for 11px bold text. */
.familyonlyflag {
    flex: none;
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(255, 159, 67, 0.14);
    border: 1px solid rgba(255, 159, 67, 0.5);
    color: #ffe3c6;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    white-space: nowrap;
}
/* Unlisted: localhost and the family site only, on a game the public site
   ships at a private link (README "Unlisted games"); the tooltip carries the
   path. It comes after every other pill. On a phone it stays off the topbar,
   which already holds two pills at most, and keeps its place on the hub tile. */
.unlistedflag {
    flex: none;
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(80, 200, 220, 0.14);
    border: 1px solid rgba(80, 200, 220, 0.5);
    color: #d4f6fb;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    white-space: nowrap;
}
@media (max-width: 560px) {
    .topbar .unlistedflag { display: none; }
}
/* The h1 holding a pill becomes a flex LINE. Needed for the phone breakpoint,
   where .pagehome is `display: block` to get its ellipsis: two block children
   in a plain h1 stack, which would make the chrome two rows tall and undo the
   one-row topbar. min-width: 0 on both the h1 (set at that breakpoint) and the
   anchor is what lets the title ellipsize instead of shoving the pill out.

   Named for the MECHANISM rather than for Beta: every pill hangs off the title
   the same way and needs the same line, so one class serves them all. */
.topbar h1.flagtitle { display: flex; align-items: center; gap: 8px; min-width: 0; }
.topbar h1.flagtitle .pagehome { min-width: 0; }
/* TWO pills on a phone (a family-only Beta game: Beta, then Family only) stack
   in a column beside the title rather than sitting in the line. Side by side
   they need about 150px, and the phone toolbar keeps its width (flex: none),
   so at 360px and 390px the pair ran 36px and 6px under the toolbar (measured
   October 2026, touch emulation). Stacked, they take the width of the wider
   pill, keep their type, and fit the one-row topbar by giving up only their
   vertical padding: two 16px pills and a 2px gap stay inside the 38px
   toolbar, so the topbar keeps its height. One pill keeps the flex line. */
@media (max-width: 560px) {
    .topbar h1.flagtitle:has(.betaflag + .familyonlyflag) {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        column-gap: 8px;
        row-gap: 2px;
        align-items: center;
    }
    .topbar h1.flagtitle:has(.betaflag + .familyonlyflag) > .pagehome { grid-row: 1 / span 2; }
    .topbar h1.flagtitle:has(.betaflag + .familyonlyflag) > .betaflag,
    .topbar h1.flagtitle:has(.betaflag + .familyonlyflag) > .familyonlyflag {
        grid-column: 2;
        justify-self: start;
        padding-block: 0;
        line-height: 14px;
    }
}
/* Beside the Family pill, a site page's plain-text title in the game-only view
   (.sitetitle, below) ellipsizes the same way, and so does the hub's site name
   (.sitename, which only the family build emits). A bare text node could not:
   on a 390px phone it wrapped the hub title onto two lines. */
.topbar h1.flagtitle .sitetitle { min-width: 0; }
.topbar h1.flagtitle .sitename {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* The hub's one-line gloss per pill, directly under the headline and above the
   list, so the pills are explained before they are seen rather than after. */
.flagnote { margin: 10px 0 0; font-size: 13px; color: rgba(255, 255, 255, 0.72); }
.flagnote .betaflag, .flagnote .familyonlyflag, .flagnote .disabledflag,
.flagnote .localflag, .flagnote .unlistedflag { vertical-align: 1px; }
/* The .flagnote lines ship in one disclosure, closed by default (flagNotes in
   tools/gen-pages.mjs; localhost and the family build only). Its summary is
   one more small note line: .flagnote's size and color, left-aligned with the
   notes around it as prose is, and the browser's own marker. fit-content keeps
   the empty width to its right from toggling it. Open, the body is the
   .flagnote lines as they were. */
.flagnotes { margin: 10px 0 0; font-size: 13px; color: rgba(255, 255, 255, 0.72); }
.flagnotes > summary { width: fit-content; cursor: pointer; }
/* The hub's standing note that every game here is an unofficial implementation.
   Styled like .flagnote but its own class, because .flagnote means "glosses a
   pill" and publish_pills_test counts it that way. */
.hubnote { margin: 10px 0 0; font-size: 13px; color: rgba(255, 255, 255, 0.72); }
/* A designer game's setup line saying it is an unofficial implementation
   (emitted by unofficialNote in tools/gen-pages.mjs). It sits directly under
   the game facts row, so it takes that row's gap, size, and color
   (.gamefacts, .gamefact) and reads as the row's last line. */
.unofficial-note { margin: 6px 0 0; font-size: 13px; color: rgba(255, 255, 255, 0.6); }

/* ---------- buttons ---------- */

.btn {
    font: inherit;
    color: var(--ink);
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    padding: 7px 14px;
    cursor: pointer;
}
.btn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.14); }
.btn:disabled { opacity: 0.4; cursor: default; }
/* Toolbar controls carry an icon beside their label (tools/gen-pages.mjs);
   below 560px the label hides and the icon stands alone. */
.toolbar .btn { display: inline-flex; align-items: center; gap: 7px; }
.btnicon { width: 17px; height: 17px; flex: none; fill: currentColor; }
.btn.primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; font-weight: 600; }
.btn.primary:hover:not(:disabled) { background: #ffdf8e; }
.btn.big { font-size: 17px; padding: 12px 22px; }
button:focus-visible, a.btn:focus-visible, .card.clickable:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}

/* The first-visit tutorial flash: two gold pulses on #setup-play-tutorial for
   somebody who has never played this game (GameShell.nudgeTutorial adds the
   class, once per page load). The button ships looking like the other three, so
   without this it reads as a fourth way to start rather than as the answer to
   "I do not know this game".

   A BOX SHADOW rather than the .hinted / .coach-mark outline recipe, for two
   reasons. The focus ring directly above is an outline, and `.btn.tutnudge`
   outranks `button:focus-visible` on specificity, so an outline here would eat
   the ring of a keyboard user who tabbed to the button mid-flash. And a shadow
   fades, where an outline can only blink between two colors: `hintpulse` is
   built to repeat forever, and a hard on/off ending on the second beat reads as
   a glitch rather than as a finished gesture.

   A shadow does not affect layout, so the setup action row cannot move (see
   .kiro/steering/ui-conventions.md); measured across the whole flash, the row
   height, the button's offsetTop and its width are all constant. rgba of
   --accent (#ffd166) because a shadow's color cannot be animated apart from its
   geometry, and this is the sheet's usual idiom for a translucent accent.

   Two layers, both soft: a low-alpha 4px ring to give the glow an edge to sit
   on, and a wide blurred glow doing most of the work. The weight is deliberately
   low. This is a suggestion to somebody who has just arrived, not a warning, and
   at full strength the ring competed with "Play vs AI" one button along, which
   IS the primary action and wears the accent as a fill. A cue that outshouts the
   button it sits beside has picked the wrong fight.

   The ring stays inside the 12px .setup-actions gap in both axes so it never
   touches a neighbor, even on a phone where that row wraps; the glow does reach
   past it, which is what a glow is for and is why it can be soft.

   Two iterations, and a delay that clears #screen-setup's own 160ms screen-in
   fade so the flash is not competing with the page arriving. `both` holds the
   transparent first frame through the delay. */
.btn.tutnudge {
    animation: tutnudge 850ms ease-in-out 450ms 2 both;
}
@keyframes tutnudge {
    from, to {
        box-shadow: 0 0 0 0 rgba(255, 209, 102, 0), 0 0 0 0 rgba(255, 209, 102, 0);
    }
    45% {
        box-shadow: 0 0 0 4px rgba(255, 209, 102, 0.32),
            0 0 16px 4px rgba(255, 209, 102, 0.22);
    }
}
/* A steady ring still says "start here", without the movement, which is the
   .coach-mark precedent. body.motion-off is "Animations: Off" (shared/ui/motion.ts). */
body.motion-off .btn.tutnudge {
    animation: none;
    box-shadow: 0 0 0 3px rgba(255, 209, 102, 0.32);
}

/* ---------- setup / hub ---------- */

/* The hub's pin row (.hubpinrow is display: flex further down) can hold two
   pins at once, a designer's and the similarity order's (src/shared/hub.ts). */
.hubpinrow { flex-wrap: wrap; gap: 8px; }
/* A `?like=` visit's list, held until the hub script has ordered it by
   similarity, so it never paints in Featured order first (tools/gen-pages.mjs
   FAVORITE_ICONS_BOOT). Its space is kept, so nothing moves when it shows. */
.gamelist.hub-ordering { visibility: hidden; }

.setup-card {
    max-width: 640px;
    margin: 48px auto;
    padding: 28px 32px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
}
.setup-actions { display: flex; gap: 12px; flex-wrap: wrap; margin: 18px 0; }

/* The icon-only stats toggle beside the setup screen's record line: a square
   button about 30px across (above the 24px minimum tap target), quiet until
   hovered, and in the accent color while its panel is open. aria-expanded is
   the state the shell sets, so the look cannot disagree with it. */
.btn.stats-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 6px;
    line-height: 0;
    color: rgba(255, 255, 255, 0.7);
}
.btn.stats-toggle .btnicon { width: 16px; height: 16px; }
/* Hover brightens a CLOSED toggle only: the :not(:disabled) makes a hover rule
   outrank the open one, which would turn an open toggle white under the mouse
   that just opened it. */
.btn.stats-toggle[aria-expanded="false"]:hover:not(:disabled) { color: var(--ink); }
.btn.stats-toggle[aria-expanded="true"] { color: var(--accent); border-color: var(--accent); }

/* The stats panel under the setup screen's record line (STATS_BLOCK in
   tools/gen-pages.mjs) and the site-wide Stats page (src/stats/stats.ts). Both
   paint through src/shared/shell/stats-view.ts, so one set of rules serves both.

   Real tables, never .result-table: that class is a grid whose rows size their
   own tracks, so its columns drift apart. Names left, numbers right in tabular
   figures with their headers, as every score table here reads. */
.stats-detail { margin: 2px 0 14px; }
.stx-table {
    border-collapse: collapse;
    margin: 12px 0 0;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
}
.stx-table caption {
    caption-side: top;
    text-align: left;
    font-weight: 600;
    padding-bottom: 4px;
}
.stx-table th, .stx-table td { padding: 3px 0 3px 16px; }
.stx-table th:first-child, .stx-table td:first-child { padding-left: 0; }
.stx-table thead th {
    font-weight: 600;
    color: rgba(255, 255, 255, 0.62);
    border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}
.stx-table .stx-num { text-align: right; white-space: nowrap; }
/* A row label is a header for its row, but it reads as the row's name, not as
   a heading, so it keeps the body weight. nowrap keeps "With the bots" whole. */
.stx-table .stx-label { text-align: left; font-weight: 400; white-space: nowrap; }
.stx-facts { margin: 10px 0 0; color: rgba(255, 255, 255, 0.62); font-size: 13px; }
/* A rating note: what "?" means, or when a rating will appear. Its own line,
   quieter than the facts, since it explains rather than reports. */
.stx-note { margin: 6px 0 0; color: rgba(255, 255, 255, 0.5); font-size: 12px; }
/* The Stats page: the summary line leads, and each table gets more air. A game
   NAME may wrap there (the link, not the row label around it), since "Deck
   Express: Southern Railway" held on one line would push the numbers off a
   phone. Headers and numbers never wrap: "20h 34m" on two lines reads as two
   values. */
.stx-lead { font-weight: 600; }
.stx-page .stx-table { margin-top: 22px; font-size: 14px; }
.stx-page .stx-label a { white-space: normal; }
/* A sortable table (Your games): each header is a button (sortHeader in
   stats-view.ts) dressed as the header text, so the columns read exactly as
   before. The sorted column's header is full ink and carries an arrow. Every
   header keeps its arrow's space, hidden, so a column does not change width
   when the sort moves to or from it. Phones drop that reserve (below). The
   Game header heads the names, so it sits left over them. */
.stx-table thead .stx-corner { text-align: left; }
.stx-sort {
    font: inherit;
    color: inherit;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    cursor: pointer;
    white-space: nowrap;
}
.stx-sort:hover { color: var(--ink); text-decoration: underline; }
.stx-sort:focus-visible { outline-offset: 1px; }
.stx-sorted .stx-sort { color: var(--ink); }
.stx-sortmark { display: inline-block; font-size: 0.7em; visibility: hidden; }
.stx-sortmark.stx-lead-mark { margin-right: 2px; }
.stx-sortmark.stx-tail-mark { margin-left: 2px; }
.stx-sorted .stx-sortmark { visibility: visible; }
/* Every stats table sits in a scroll box (statsBlock in stats-view.ts). On an
   ordinary record it fits and nothing scrolls; a long one (an 8-seat game's
   player-count table, a 4-digit game count) scrolls inside the box instead of
   pushing the whole page sideways. The negative margin and matching padding
   leave the focus ring and a cell's last pixel room inside the clip. */
.stx-wrap { overflow-x: auto; margin: 0 -4px; padding: 0 4px; }
/* Phones: tighter gaps, and the Stats page at the panel's size, measured to
   fit the five-column Your games table at 360px with the longest game names
   (each breaks at its spaces; "Triangulations" is the longest single word). */
@media (max-width: 560px) {
    .stx-table th, .stx-table td { padding-left: 7px; }
    .stx-table th:first-child, .stx-table td:first-child { padding-left: 0; }
    .stx-page .stx-table { font-size: 13px; }
    /* Only the sorted column has its arrow here. Reserving one on every
       header cost Your games about 16px it does not have at 360 to 390px, so
       the table scrolled sideways. The trade is that the sorted column can
       widen by an arrow when its header is its widest cell. Measured: an
       arrow in the column gap instead overlapped the next header at 360px. */
    .stx-sortmark { display: none; }
    .stx-sorted .stx-sortmark { display: inline-block; }
}
/* 360px phones: one step smaller again, which is what fits Your games there. */
@media (max-width: 380px) {
    .stx-table th, .stx-table td { padding-left: 5px; }
    .stx-table th:first-child, .stx-table td:first-child { padding-left: 0; }
    .stx-page .stx-table { font-size: 12px; }
}
.stats-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 14px 0 0; }
/* Site-wide stats is a link in the button costume, so it drops the underline
   and the link color, as the toolbar's link buttons do. */
.stats-actions a.btn { color: var(--ink); text-decoration: none; }
.rules summary { cursor: pointer; font-weight: 600; }
.rules ol { margin: 10px 0 0; padding-left: 20px; }
.rules li { margin-bottom: 6px; }
/* A nested list inside a rules step (e.g. the Christmas Tree frill types). */
.rules ul { margin: 6px 0 2px; padding-left: 18px; }
.rules ul li { margin-bottom: 3px; }
/* The generated Rules_Summary of a game with a Cheat_Sheet (rulesSummary in
   tools/gen-pages.mjs): one h3 per section, then a link to the game's Rules_Page.
   Scoped to .cheatsheet because no hand-written rules block carries headings, and
   the accent color is .pagedoc's, since an unstyled link is the browser's blue on
   a dark panel. Each section after the first is set off by a hairline in the
   panel's own border color. Two classes and an element outrank .pagedoc h3, so
   the Rules_Page (where the block sits inside .pagedoc) gets the same headings. */
.rules.cheatsheet h3 {
    margin: 16px 0 5px;
    padding-top: 12px;
    border-top: 1px solid var(--panel-border);
    font-size: 16px;
}
.rules.cheatsheet h3:first-of-type { margin-top: 12px; padding-top: 0; border-top: 0; }
.rules.cheatsheet a { color: var(--accent); }
/* A suit glyph in Cheat_Sheet prose, colored per suit by the classes
   suitTextClass() in src/shared/cards.ts would give it (`red`, `red diamond`,
   `club`), the same convention as the .coach and .cardlabel rules. Scoped to
   .rules.cheatsheet so the bare modifiers cannot reach any hand-written rules
   block; `.diamond` one class deeper so it wins over `.red`. */
.rules.cheatsheet .red { color: var(--red); }
.rules.cheatsheet .red.diamond { color: var(--red-diamond); }
.rules.cheatsheet .club { color: var(--blue-club); }

/* ---------- example cards and reference lists ---------- */

/* An example painted into a [data-cards] element (src/shared/ui/card-examples.ts):
   read-only cards from the simplified atlas, which ui-conventions requires at
   34px and below. The size is the container's --cw, so a context resizes every
   example in it with one custom property. In running prose it is small enough
   to sit on the line; a .reflist row gives it more room. */
.cardex {
    --cw: 26px;
    display: inline-flex;
    gap: 2px;
    flex-wrap: nowrap;
    vertical-align: middle;
}
.cardex .card { flex: none; }
/* A card shown for context but not part of the combination (a poker kicker).
   The row's aria-label says "dimmed", so the fade is not the only signal. */
.cardex .card.faded { opacity: 0.38; }
.cardex-gap { flex: none; width: 7px; }

/* A reference list: one row per hand or meld, its name and a one-line
   description on the left and an example on the right. An <ol> numbers its
   rows, for a ranking read strongest first; a <ul> does not. Written by hand in
   a Cheat_Sheet (pages/<id>.mjs) and built by ui/rankings.ts, so every game's
   rankings and meld types look the same. The row stays left-aligned, because
   its columns have to line up down the list (ui-conventions).
   SCOPED TO .cheatsheet, which every copy sits in (the Rules_Summary, the
   Rules_Page, and the Reference_Dialog's body), so the list and row rules
   outrank `.rules ol`, `.rules ul li`, and `.pagedoc ul`/`li` in either order. */
.cheatsheet ol.reflist,
.cheatsheet ul.reflist {
    list-style: none;
    margin: 10px 0 0;
    padding: 0;
    display: grid;
    gap: 5px;
    counter-reset: reflist;
}
.cheatsheet .reflist > li {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: 'name ex' 'what ex';
    align-items: center;
    column-gap: 14px;
    row-gap: 1px;
    margin: 0;
    padding: 6px 10px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.045);
}
.cheatsheet ol.reflist > li {
    counter-increment: reflist;
    grid-template-columns: 1.4em minmax(0, 1fr) auto;
    grid-template-areas: 'num name ex' 'num what ex';
}
.cheatsheet ol.reflist > li::before {
    content: counter(reflist);
    grid-area: num;
    font-weight: 700;
    font-size: 13px;
    opacity: 0.5;
    font-variant-numeric: tabular-nums;
}
.reflist-name { grid-area: name; align-self: end; font-weight: 700; font-size: 14px; }
.reflist-what { grid-area: what; align-self: start; font-size: 12.5px; opacity: 0.82; line-height: 1.4; }
/* A row with no description centers its name against the example. */
.reflist > li:not(:has(.reflist-what)) > .reflist-name { grid-row: 1 / span 2; align-self: center; }
/* The example, alone or as a group of alternatives in .reflist-ex. */
.reflist > li > .cardex,
.reflist-ex { grid-area: ex; justify-self: end; }
.reflist .cardex { --cw: 32px; }
.reflist-ex { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px 12px; }
@media (max-width: 560px) {
    /* Five cards still fit beside their text on a phone, so a poker hand keeps
       its row. A longer example, or a group of them, drops under its text,
       which is the only column that can afford to wrap. The sixth child of a
       .cardex is a sixth card or a gap, so :nth-child(6) means "longer than a
       poker hand". Below 380px nothing fits beside the text, so every row
       stacks. */
    .reflist .cardex { --cw: 26px; }
    .cheatsheet .reflist > li { column-gap: 10px; padding: 6px 8px; }
    .cheatsheet ul.reflist > li:has(> .reflist-ex, > .cardex > :nth-child(6)) {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: 'name' 'what' 'ex';
        row-gap: 3px;
    }
    .cheatsheet ol.reflist > li:has(> .reflist-ex, > .cardex > :nth-child(6)) {
        grid-template-columns: 1.4em minmax(0, 1fr);
        grid-template-areas: 'num name' 'num what' 'num ex';
        row-gap: 3px;
    }
    .reflist > li:has(> .reflist-ex, > .cardex > :nth-child(6)) > .reflist-name { grid-row: auto; }
    .reflist > li:has(> .reflist-ex, > .cardex > :nth-child(6)) > .cardex,
    .reflist > li > .reflist-ex { justify-self: start; justify-content: flex-start; margin-top: 2px; }
}
@media (max-width: 380px) {
    .cheatsheet ul.reflist > li {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: 'name' 'what' 'ex';
        row-gap: 3px;
    }
    .cheatsheet ol.reflist > li {
        grid-template-columns: 1.4em minmax(0, 1fr);
        grid-template-areas: 'num name' 'num what' 'num ex';
        row-gap: 3px;
    }
    .reflist > li:not(:has(.reflist-what)) > .reflist-name { grid-row: auto; }
    .reflist > li > .cardex { justify-self: start; margin-top: 2px; }
}

/* A worked-example table in a Cheat_Sheet's Examples section (tools/cheatsheet.mjs,
   WORKED EXAMPLES). A real <table>, so each column is sized once for every row
   (ui-conventions, "Score tables line up by column"), as wide as the text column
   around it (ui-conventions, "Examples fill the text column"), every cell aligned
   explicitly, and numbers right-aligned in tabular figures. Per-game tweaks go in
   the game's own stylesheet under the game's own class. */
.cheatsheet table.extable { border-collapse: collapse; width: 100%; margin: 10px auto 0; font-size: 13px; }
.cheatsheet .extable th,
.cheatsheet .extable td {
    padding: 4px 6px;
    text-align: left;
    vertical-align: middle;
    border-bottom: 1px solid var(--panel-border);
}
.cheatsheet .extable th { font-weight: 600; }
.cheatsheet .extable .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.cheatsheet .extable .cardcell { text-align: center; }
.cheatsheet .extable .cardex { --cw: 26px; }
.cheatsheet .extable .exnote { display: block; font-size: 11px; opacity: 0.8; white-space: nowrap; }
.cheatsheet .extable .exwhy td { border-bottom-width: 2px; font-size: 12.5px; opacity: 0.9; }
/* One sum in example prose ("24 − 20 = 4"), kept on one line so a phone never
   breaks it between a number and its operator. */
.cheatsheet .exsum { white-space: nowrap; }
@media (max-width: 560px) {
    .cheatsheet table.extable { font-size: 12px; }
    .cheatsheet .extable th,
    .cheatsheet .extable td { padding: 3px 4px; }
    .cheatsheet .extable .cardex { --cw: 22px; }
}

/* ---------- poker hand rankings reference ---------- */

/* Built by src/shared/ui/rankings.ts into every [data-rankings] container, as
   an ol.reflist. The note under it states the Joker rules, beside an example
   of each Joker. */
.rankings-note {
    display: flex; align-items: flex-start; gap: 10px;
    margin: 12px 0 0; font-size: 13px; opacity: 0.85; line-height: 1.5;
}
.rankings-note .cardex { flex: none; --cw: 30px; }

/* The cribbage scoring reference (shared/ui/cribbage-scoring.ts), filling the
   [data-cribbage-scoring] container the cribbage family's pages carry. Same
   shape as the rankings block above and sized to match it: a label column, a
   row of simplified example cards, and what the example pays. */
.cribbage-scoring-lead { margin: 10px 0 0; font-size: 14px; }
.cribbage-scoring-values {
    margin: 8px 0 0; font-size: 13px; opacity: 0.85; line-height: 1.45;
}
.cribbage-scoring-note {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    margin: 10px 0 0; font-size: 13px; opacity: 0.85; line-height: 1.45;
}
.cribbage-scoring-note .cardrow { flex: none; gap: 3px; }
.cribbage-scoring-note .card { --cw: 26px; }
.cribbage-scoring-list {
    list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 2px;
}
/* ONE LINE PER COMBINATION, in three columns: what it is called and pays, the
   rule, and the example. The name and its points share the FIRST column so they
   touch whatever the name's length - they were previously at opposite edges of
   the row (`margin-left: auto`), which put the whole width of the block between a
   label and its own number. The column is em-sized so the rule text still lines
   up down the list. */
.cribbage-scoring-row {
    display: grid;
    grid-template-columns: 11em minmax(0, 1fr) auto;
    align-items: center;
    gap: 4px 12px;
    margin: 0;
    padding: 2px 0;
}
.cribbage-scoring-head {
    display: flex;
    align-items: baseline;
    gap: 7px;
}
.cribbage-scoring-name { font-weight: 600; font-size: 13.5px; }
.cribbage-scoring-how { font-size: 12px; opacity: 0.7; }
.cribbage-scoring-row .card { --cw: 26px; }
.cribbage-scoring-row .cardrow { gap: 2px; flex: none; justify-content: flex-end; }
/* What the example pays, colored like every other points figure in the family
   and right beside the name it belongs to. */
.cribbage-scoring-pts {
    font-weight: 700; font-size: 14px; color: var(--accent);
    white-space: nowrap; font-variant-numeric: tabular-nums;
}
@media (max-width: 560px) {
    /* The rule text drops to its own line under the name and the example, which
       is the only column that can afford to wrap at this width. */
    .cribbage-scoring-row {
        grid-template-columns: minmax(0, 1fr) auto;
        gap: 1px 8px;
        padding: 4px 0;
    }
    .cribbage-scoring-how { grid-column: 1 / -1; }
    .cribbage-scoring-row .card { --cw: 24px; }
}

/* ---------- the score breakdown modal (#score-modal) ----------
   Tap any cribbage count in the family and see every combination that made it,
   one row per reason, each row carrying its own cards as simplified sprites
   (src/shared/ui/score-breakdown.ts fills the page's bare #score-modal anchor).

   SAME SHAPE AS THE GAME PICKER above, deliberately: fixed full-page anchor, a
   translucent ::before backdrop, and a centered card sized by content. z-index
   500 puts it under the handoff gate's 600, so passing the device can never
   leave a breakdown floating over the gate. The card is `position: relative`
   rather than transform-centered, because a fixed ::before backdrop plus a
   transformed card is the combination that traps the backdrop inside the card's
   own containing block. */
.scorebd {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: none;
    padding: 20px;
}
.scorebd.scorebd-shown {
    display: flex;
    align-items: center;
    justify-content: center;
}
.scorebd::before {
    content: '';
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
}
.scorebd-card {
    position: relative;
    display: flex;
    flex-direction: column;
    width: min(560px, 100%);
    max-height: calc(100vh - 40px);
    padding: 16px 18px;
    background: var(--felt-dark);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
    box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
}
/*
 * THE HEADING IS CENTERED over the cards it names, with Close taken out of the
 * flow at the right. Left in the row, the button would push the title off-center
 * by half its own width, and `text-align` cannot fix that on a shrink-to-fit
 * flex item: the same reason `.panel-head > .btn.small` is absolute (see
 * ui-conventions, "Center, do not left-align"). The side padding is room for the
 * button on BOTH sides, so a long title wraps symmetrically instead of running
 * under it.
 */
.scorebd-head {
    position: relative;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: 12px;
    margin-bottom: 10px;
    padding: 0 64px;
}
.scorebd-heading {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 1px;
}
.scorebd-head .scorebd-close { position: absolute; top: 0; right: 0; }
.scorebd-head h2 { margin: 0; font-size: 19px; }
.scorebd-total {
    font-size: 13px;
    color: var(--accent);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
/* The cards being counted, at reading size, so the rows below have something to
   point at. They wrap rather than shrink: six is the most any game in the family
   counts at once (CRYBT's Broken Mirror echoes the Hero). */
.scorebd-cards {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px;
    padding: 9px;
    border-radius: 10px;
    background: #006400;
    border: 1px solid rgba(0, 0, 0, 0.35);
}
.card.scorebd-card { --cw: 46px; }
.scorebd-cards .card { --cw: 46px; }
/* Which card was turned decides the flush and the nobs, so it is ringed. */
.scorebd-cards .card.scorebd-turned { box-shadow: 0 0 0 2px var(--accent); }
.scorebd-turnednote { margin-top: 5px; font-size: 11.5px; opacity: 0.65; }
/* One row per reason: what it pays, what it is called, and the cards that make
   it. The points sit FIRST and adjacent to the label, the same way the scoring
   reference block above pairs them. */
.scorebd-list {
    list-style: none;
    margin: 11px 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* min-height: 0 is what lets the list shrink inside the flex card instead of
       pushing the total off the bottom on a long count (the perfect 29 has ten
       rows). */
    min-height: 0;
    flex: 1 1 auto;
}
.scorebd-item {
    display: grid;
    grid-template-columns: 2.2em minmax(0, 1fr) auto;
    align-items: center;
    gap: 4px 10px;
    padding: 3px 6px;
    border-radius: 7px;
    background: rgba(0, 0, 0, 0.18);
}
.scorebd-pts {
    font-weight: 700;
    font-size: 15px;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
    text-align: right;
}
.scorebd-label { font-size: 13px; }
.scorebd-made { display: flex; gap: 2px; justify-content: flex-end; flex: none; }
.scorebd-made .card { --cw: 26px; }
/* A set grouped into one row is the visually widest thing here (four cards), so
   it gets no special treatment; the label already says what it is. */
.scorebd-sum {
    display: grid;
    grid-template-columns: 2.2em minmax(0, 1fr);
    grid-template-areas: 'pts label';
    align-items: baseline;
    gap: 10px;
    margin-top: 9px;
    padding-top: 8px;
    border-top: 1px solid var(--panel-border);
    font-weight: 600;
}
.scorebd-sum .scorebd-pts { grid-area: pts; }
.scorebd-sum .scorebd-sumlabel { grid-area: label; font-size: 13.5px; }
.scorebd-none { margin: 11px 0 0; font-size: 13.5px; opacity: 0.85; }
.scorebd-note { margin-top: 8px; font-size: 12.5px; opacity: 0.8; }

/* The chip a score is painted as. A real button, because the arithmetic behind a
   number is a genuine action and has to be reachable by keyboard; each game adds
   its own class for the look it already had. */
.scorebd-chip {
    font: inherit;
    color: inherit;
    cursor: pointer;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 7px;
    padding: 0 4px;
}
.scorebd-chip:hover:not(:disabled) { border-color: var(--accent); }
.scorebd-chip:disabled { cursor: default; }
@media (max-width: 560px) {
    .scorebd { padding: 10px; }
    .scorebd-card { padding: 13px 14px; max-height: calc(100vh - 20px); }
    .scorebd-cards .card { --cw: 38px; }
    .scorebd-made .card { --cw: 22px; }
    .scorebd-item { grid-template-columns: 2em minmax(0, 1fr); }
    /* The cards that make a reason drop under its label rather than squeezing
       the label to nothing. */
    .scorebd-made { grid-column: 1 / -1; justify-content: flex-start; }
}

/* Tile metrics are deliberately tight: the hub is a LIST to scan, so fitting
   more games on screen beats airiness. Vertical padding, the icon box and the
   line-height all come down together - shrinking only one leaves the others
   setting the floor on tile height. */
.gamelist { list-style: none; padding: 0; margin: 14px 0 0; display: grid; gap: 7px; }
.gamelink, .gamelist .soon {
    display: flex; flex-direction: column; gap: 1px;
    padding: 9px 14px; border-radius: 10px;
    background: rgba(0, 0, 0, 0.2); border: 1px solid var(--panel-border);
    color: var(--ink); text-decoration: none;
}
.gamelink:hover { border-color: var(--accent); }
.gamename { font-weight: 700; font-size: 16.5px; line-height: 1.25; }
/* Two lines, then ellipsis: a handful of blurbs are long enough to run to
   four lines and make their tile twice its neighbors' height. The full text
   stays in the DOM, so the hub's search still matches on it (hub.ts reads
   textContent), and the game's own page carries the long version. */
.gamedesc {
    opacity: 0.75; font-size: 13.5px; line-height: 1.35;
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
    overflow: hidden;
}
.gamelist .soon { opacity: 0.45; }

/* ---------- game layout ---------- */

/* The cap was 1200px, which is where "the UI is too small on my laptop" came
   from: on any display wider than that, the page stopped growing and so did
   every card in it, leaving the felt empty on both sides.
   1680px is chosen to pair with the --cw curve: --cw finishes growing at
   1920px, and this leaves the play area growing at least as fast as the cards
   over the whole span, so a row that fitted N cards before still fits N.
   The LOG COLUMN stays at exactly 300px on purpose. Several boards reverse-
   engineer their card size from the page layout with hand-computed constants
   (Dracula's `(100vw - 560px) / 3`, Crosscribb's `(100vw - 800px) / 5`, and
   others), and those constants have 300 baked into them, so widening the log
   here would silently falsify them. Making the column fluid is safe only once
   those boards measure their own container instead, the way Duel Poker's does
   (src/duelpoker/style.css). */
#screen-game {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 300px;
    gap: 16px;
    padding: 16px;
    max-width: 1680px;
    margin: 0 auto;
}
.playarea { min-width: 0; }
@media (max-width: 950px) {
    #screen-game { grid-template-columns: 1fr; }
    #log-panel { order: 2; }
}

/* The two lines at the top of every board: what to do now, and where the match
   stands. Both are CENTERED, on the board's own axis, like every other row.
   The one cost is that a centered sentence shifts sideways as its wording
   changes; that is horizontal only, and the min-heights here (plus the taller
   per-game reservations) mean nothing below either line ever moves. */
.status { font-size: 18px; font-weight: 600; min-height: 26px; text-align: center; }
.progress {
    opacity: 0.7;
    font-size: 14px;
    min-height: 20px;
    margin-bottom: 8px;
    text-align: center;
}
/* Mid-game bot-difficulty control (shell-rendered into #level-line), centered on
   the board's axis. The class is the shell's, so the rules belong here; they were
   filed in src/split-court/style.css for a while, where they were global anyway
   because nothing scoped them to that game.
   It sits at the FOOT of the play area, under the board and the hand, not above
   the board: it is changed once a game at most, and above the board it cost
   every vs-AI game a line of height before the first card (gen-pages.mjs places
   the element). */
.level-line {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin: 10px 0 0;
    font-size: 13px;
}
.level-line .opt-label { color: rgba(255, 255, 255, 0.62); }
.level-line .seg { font-size: 12px; padding: 3px 10px; }
/* On phones the control explains itself (Beginner/Skilled/Expert), so the
   "Bot level" caption costs width for nothing; the box keeps its tooltip. */
@media (max-width: 560px) {
    .level-line { margin: 6px 0 0; }
    .level-line .opt-label { display: none; }
}

/* ---------- scoreboard (split-court, cupid) ----------
   The running total is the thing a player checks most often, so it is a
   scoreboard rather than another muted status line: the parent .progress sets
   opacity 0.7, which every rule here has to climb back out of.
   Several games race to a target and share the widget, so each rule below
   carries every game's opt-in class rather than the styling being duplicated. */
.progress.scr_scoreboard,
.progress.cupid-scoreboard,
.progress.truc-scoreboard,
.progress.cribbage-scoreboard,
.progress.crisps-scoreboard,
.progress.hearts-scoreboard,
.progress.gongzhu-scoreboard,
.progress.gin-scoreboard,
.progress.fox-scoreboard,
.progress.swoop-scoreboard,
.progress.twenty-scoreboard,
.progress.bon-scoreboard,
.progress.fiv-scoreboard,
.progress.xpd-scoreboard,
.progress.cnc-scoreboard,
.progress.rh-scoreboard,
.progress.wlf-scoreboard,
.progress.sono-scoreboard,
.progress.mei-scoreboard,
.progress.dib-scoreboard,
.progress.pop-scoreboard,
.progress.gbo-scoreboard {
    opacity: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    flex-wrap: wrap;
    min-height: 34px;
}
.progress-round {
    font-size: 13px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.6;
}
.progress-score {
    display: inline-flex;
    align-items: baseline;
    gap: 5px;
    padding: 3px 12px;
    border-radius: 999px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.progress-score.mine { border-color: rgba(255, 255, 255, 0.35); }
/* Whoever is ahead, so the standing reads at a glance. Ties light both. */
.progress-score.leading { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.scr_scorename,
.cupid-scorename,
.truc-scorename,
.cribbage-scorename,
.crisps-scorename,
.hearts-scorename,
.gongzhu-scorename,
.gin-scorename,
.fox-scorename,
.swoop-scorename,
.twenty-scorename,
.bon-scorename,
.sfd-scorename,
.fiv-scorename,
.xpd-scorename,
.cnc-scorename,
.rh-scorename,
.wlf-scorename,
.sono-scorename,
.mei-scorename,
.dib-scorename,
.pop-scorename,
.gbo-scorename { font-size: 13px; opacity: 0.75; }
.scr_scorenum,
.cupid-scorenum,
.truc-scorenum,
.cribbage-scorenum,
.crisps-scorenum,
.hearts-scorenum,
.gongzhu-scorenum,
.gin-scorenum,
.fox-scorenum,
.swoop-scorenum,
.twenty-scorenum,
.bon-scorenum,
.sfd-scorenum,
.fiv-scorenum,
.xpd-scorenum,
.cnc-scorenum,
.rh-scorenum,
.wlf-scorenum,
.sono-scorenum,
.mei-scorenum,
.dib-scorenum,
.pop-scorenum,
.gbo-scorenum {
    display: inline-block;
    font-size: 22px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.progress-score.leading .scr_scorenum,
.progress-score.leading .cupid-scorenum,
.progress-score.leading .truc-scorenum,
.progress-score.leading .cribbage-scorenum,
.progress-score.leading .crisps-scorenum,
.progress-score.leading .hearts-scorenum,
.progress-score.leading .gongzhu-scorenum,
.progress-score.leading .gin-scorenum,
.progress-score.leading .fox-scorenum,
.progress-score.leading .swoop-scorenum,
.progress-score.leading .twenty-scorenum,
.progress-score.leading .bon-scorenum,
.progress-score.leading .sfd-scorenum,
.progress-score.leading .fiv-scorenum,
.progress-score.leading .xpd-scorenum,
.progress-score.leading .cnc-scorenum,
.progress-score.leading .rh-scorenum,
.progress-score.leading .wlf-scorenum,
.progress-score.leading .sono-scorenum,
.progress-score.leading .mei-scorenum,
.progress-score.leading .dib-scorenum,
.progress-score.leading .pop-scorenum,
.progress-score.leading .gbo-scorenum { color: var(--accent); }
.scr_scoretarget,
.cupid-scoretarget,
.truc-scoretarget,
.cribbage-scoretarget,
.crisps-scoretarget,
.hearts-scoretarget,
.gongzhu-scoretarget,
.gin-scoretarget,
.fox-scoretarget,
.swoop-scoretarget,
.twenty-scoretarget,
.bon-scoretarget,
.wlf-scoretarget,
.sono-scoretarget,
.pop-scoretarget,
.dib-scoretarget { font-size: 12px; opacity: 0.5; }

/* Deck/discard chips: live counts, and the anchors cards fly from/to.
   Centered with the board axis (the Cuttle treatment, rolled out site-wide):
   the chips are fixed-content pills, so centering moves no container. */
.pile-line { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; justify-content: center; }
.pile-chip {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 10px; border-radius: 8px; font-size: 13px; opacity: 0.85;
    background: var(--panel); border: 1px solid var(--panel-border);
}
.pile-chip::before {
    content: ''; width: 13px; height: 18px; border-radius: 3px; flex: none;
    background: linear-gradient(135deg, #5c6bc0, #3949ab);
    border: 1px solid rgba(255, 255, 255, 0.45);
}

/* Centered with the board axis (the Cuttle treatment, rolled out site-wide):
   each .opponent is a fixed-content chip, so centering moves no container. */
#opponents { display: flex; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; justify-content: center; }
.opponent {
    padding: 8px 14px; border-radius: 10px;
    background: var(--panel); border: 1px solid var(--panel-border);
}
.opponent.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.opponent-name { font-weight: 700; }
.opponent-detail { font-size: 13px; opacity: 0.75; }
/* Duel Poker Play 4: the opponent's remaining-card count (that mode counts a
   hand down 4 -> 0 with no draws, so it is strategic). Dimmer than the name. */
.opponent-count { font-size: 13px; opacity: 0.75; margin-top: 2px; }

/* ---------- draft board ---------- */

.board-grid {
    display: grid;
    grid-template-columns: 46px auto;
    grid-template-rows: 40px auto;
    gap: 6px;
    width: fit-content;
    /* Centered at every width (it was left-aligned above the phone breakpoint,
       which is where "the board hugs one edge" came from). The tracks are
       fixed, so the fit-content box is a constant size and never re-centers
       mid-game. Club Fight reuses this frame and already centered its own
       instance (.cf-board's margin-inline: auto is now redundant, kept). */
    margin: 6px auto 14px;
}
.colbtns {
    grid-column: 2; grid-row: 1;
    display: grid; grid-template-columns: repeat(3, var(--cw)); gap: 10px;
}
.rowbtns {
    grid-column: 1; grid-row: 2;
    display: grid; grid-template-rows: repeat(3, calc(var(--cw) * 89 / 64)); gap: 10px;
}
.cells {
    grid-column: 2; grid-row: 2;
    display: grid;
    grid-template-columns: repeat(3, var(--cw));
    grid-template-rows: repeat(3, calc(var(--cw) * 89 / 64));
    gap: 10px;
}
.grid-cell { position: relative; }

.linebtn {
    font: inherit; font-size: 20px; font-weight: 700;
    width: 100%; height: 100%;
    color: var(--ink);
    background: rgba(0, 0, 0, 0.25);
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    cursor: pointer;
}
.linebtn:hover:not(:disabled) { background: var(--accent); color: var(--accent-ink); }
.linebtn:disabled { opacity: 0.25; cursor: default; }
/* ---------- cards ---------- */

.card {
    width: var(--cw);
    height: calc(var(--cw) * 89 / 64);
    /* assets/cards.png, or its four-color copy: see "deck colors" above. */
    background-image: var(--atlas-full);
    background-size: 1400% 400%;
    border-radius: 6%;
    box-shadow: 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
    border: 0;
    padding: 0;
    flex: none;
    /* cardEl() builds a <button> for any interactive card (hand cards, build-panel
       cards) and a <div> everywhere else, so these resets are what keep the two
       PIXEL-IDENTICAL. Without them a button card renders through the native
       control path and looks softer and washed out next to its div neighbors:
         appearance      - drops native theming, which composites ButtonFace
                           underneath the sprite
         background-color- same reason; the UA gives buttons an opaque face
         font            - the shadow below is in `em`, and a button's UA
                           font-size (13.3px) is not the inherited body size, so
                           button cards otherwise get a visibly smaller shadow
         color           - inherited, for the ::after badges some games add
       Reported as "cards turned low definition in my hand and when arranging" -
       which were exactly the two interactive (button) card containers. */
    appearance: none;
    -webkit-appearance: none;
    background-color: transparent;
    font: inherit;
    color: inherit;
}
.card.mini { --cw: 46px; }
/* Small-size copy of the FULL art (assets/cards-mini.png): same illustrations,
   same 14x4 cell layout, rasterized at 128px cells instead of 256px. Forcing
   the 256px cells of cards.png down into a 26-72px card is a 4-10x downscale
   the browser does with no mipmapping, which smears face cards into a dark
   blob - the "pixelated when shrunk" symptom.
   `art-mini` is set by sprites.ts, not by hand: the right atlas depends on the
   card's width in DEVICE pixels (cssWidth * devicePixelRatio), and CSS cannot
   branch on that. Above 128 device px this atlas would be UPSCALED and look
   worse than cards.png, so it is deliberately not tied to a size class like
   .card.mini - at devicePixelRatio 3 a 46px mini card is 138 device px and
   correctly stays on cards.png.
   The :not(.simple) guard leaves the simplified atlas opt-in intact;
   specificity (3 classes) also keeps this winning over .card.simple
   regardless of source order. */
.card.art-mini:not(.simple) { background-image: var(--atlas-mini); }
/* Simplified atlas (assets/cards-simple.png): flat rank-over-suit art that
   stays legible in a small area - log lines, revealed cards, opponent strips,
   taken piles, result exhibits, hand-ranking examples. Same 14x4 cell layout as
   the full art, so cardEl() positions it unchanged:
   cardEl(card, { extraClass: 'simple' }).

   USE IT WHENEVER THE CARD IS SMALL. Below roughly 34px the full art is a smear
   at any resolution, so cards-mini.png does not save it; between 34px and 46px
   it survives in the hand or on the board but not in a read-only strip. The
   rule and its thresholds are in .kiro/steering/ui-conventions.md.

   This used to advise `mini` alone on the grounds that switching art style
   partway down a screen reads as jarring. It does not: every game that measured
   its small zones ended up departing from it (Biscuits at 22px, Dracula's result
   exhibit at 26px, the Hearts taken rows at 17px), because a smudge you cannot
   read is worse than two art styles on one screen. */
.card.simple { background-image: var(--atlas-simple); }
/* Card back, simplified atlas only (col 13, row 2). Only applies to elements
   built WITHOUT cardEl(), which sets background-position inline. */
.card.simple.back { background-position: 100% 66.6667%; }
.card.empty {
    background: rgba(255, 255, 255, 0.05);
    border: 1px dashed rgba(255, 255, 255, 0.25);
    box-shadow: none;
}
/* Belt and braces for the rule above, whose `background` shorthand clears the
   atlas at only 2 classes: every tier rule below carries 3 (the `:not()`
   counts), so it would otherwise win and paint the sheet's top-left corner -
   the Ace of Clubs - into every empty slot. sprites.ts also refuses to
   classify `.empty` at all, which is the actual root fix; this is here so a
   future tier rule cannot resurrect the bug. */
.card.empty.art-mini,
.card.empty.art-mini:not(.simple),
.card.empty.simple { background-image: none; }
.card.clickable { cursor: pointer; transition: transform 120ms ease; }
.card.clickable:hover { transform: translateY(-5px); }
.card.lit { outline: 3px solid var(--accent); outline-offset: 1px; }
.card.hinted, .linebtn.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
@keyframes hintpulse {
    from { outline-color: var(--accent); }
    to { outline-color: transparent; }
}

/* ---------- tutorial coach panel (shared; see src/shared/tutorial/) ----------
   #coach-panel is the one static container the page generator emits into every
   page; engine.ts paints its contents on every render and hides the panel
   whenever no tutorial is running (R6.5). The blocks go content, then feedback,
   then controls: title h3, lead prose, the beat's points, what to do, the
   transient nudge, an error detail, the control row, the authoring strip. It
   lives next to the hint rules above because the non-card highlight below is
   the .hinted recipe generalized. */

/* The panel is the first child of the #log-panel aside, so on a wide viewport
   it reads as a sidebar card above the game log (R6.1): the shared .panel card
   recipe at the sidebar's tighter padding, spaced off the log heading the way
   the chat panel is. */
.coach {
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    padding: 10px 12px;
    margin-bottom: 16px;
}
.coach h3 { margin: 0 0 6px; font-size: 14px; }
/* The step counter, "8 of 20" (engine.ts paints it as the heading's data-step): small, in the
   panel's top right corner, inside the panel's 10px top padding, above the heading's glyphs.
   Out of the flow, so it never adds height and never pushes a word of a long title onto a
   second line (a float in the heading's line did both, measured across every tutorial). The
   second `content` hides the generated text from screen readers, which read the step in the
   heading's own name; a browser without the alt-text syntax keeps the first. */
.coach { position: relative; }
.coach h3[data-step]::before {
    content: attr(data-step);
    content: attr(data-step) / "";
    position: absolute;
    top: 1px;
    right: 12px;
    font-size: 10px;
    font-weight: 400;
    line-height: 9px;
    opacity: 0.8;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    pointer-events: none;
}
/* The lead sentence. Everything after it is a separate block: this panel is
   read mid-game, at a glance, and a wall of prose is read as nothing at all. */
.coach-prose { font-size: 14px; }
/* The beat's points, one per line with air between them rather than run
   together into a paragraph. No bullets: the vertical rhythm already separates
   them, and markers cost horizontal room the docked narrow-layout bar needs. */
.coach-points {
    margin: 8px 0 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 7px;
}
.coach-point { font-size: 13px; }
/* A suit glyph standing on its own in beat text, colored by the same convention
   the game logs use: the shared --red of the `.cardlabel.red` precedent, the
   modifier NAME only, because .cardlabel's bold and nowrap would re-weight the
   sentence around a glyph that is part of the prose. A ranked label in the same
   line is a `.cardlabel` chip and is colored by the rules above. Scoped to
   .coach so the bare modifier cannot leak out of the panel, and a descendant
   selector because the glyph may sit inside a `.coach-term` emphasis. */
.coach .red { color: var(--red); }
/* Per-suit refinement (see the .cardlabel block for why `red` stays on
   diamonds). One class deeper, so these win without !important. */
.coach .red.diamond { color: var(--red-diamond); }
.coach .club { color: var(--blue-club); }
/* What to do, last and on its own line (the player's actual question). Bright
   ink and a left rule set it apart from the explanation above without
   borrowing the gold accent, which the transient nudge below owns. */
.coach-do {
    margin-top: 10px;
    padding-left: 8px;
    border-left: 3px solid var(--accent);
    font-size: 14px;
    font-weight: 600;
    color: var(--ink);
}
/* The transient nudge (a rejected required move, a divergence note): the gold
   accent, which already means "look here" everywhere else on the page. */
.coach-note {
    margin-top: 8px;
    font-size: 13px;
    font-weight: 600;
    color: var(--accent);
}
/* A content error halts the run, so its detail reads as a banner rather than as
   another line. --red is the platform's warning color (see :root). */
.coach-error {
    margin-top: 8px;
    padding: 6px 8px;
    border: 1px solid rgba(255, 156, 156, 0.5);
    border-radius: 8px;
    background: rgba(255, 156, 156, 0.1);
    font-size: 13px;
    color: var(--red);
    overflow-wrap: anywhere;
}
.coach-controls { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }

/* The control row has a hierarchy, because at any moment the panel is waiting
   on ONE of these: Continue (and Done at the end) take the site's filled
   `.btn.primary`, the same accent every game's action row gives the play it
   wants made. Back and Exit tutorial are the ways off the current step and read
   lighter than even a plain .btn - no fill, no resting border, and the 0.75
   dimming this sheet uses for secondary text everywhere. Both come back on
   hover, so the control is still plainly a button when reached for: the fill is
   `.btn:hover`'s own, which outranks the transparent background here on
   specificity and needs no repeating. */
.coach-secondary {
    background: transparent;
    border-color: transparent;
    padding: 7px 10px;
    opacity: 0.75;
}
.coach-secondary:hover:not(:disabled) {
    border-color: var(--panel-border);
    opacity: 1;
}

/* The authoring strip (?author=1 only, R12.6): a wrapping row of developer
   controls with one output block below them. The pre is monospaced and scrolls
   in both axes, because what lands in it is a pretty-printed Authored_State
   hundreds of lines long that must not stretch the sidebar. */
.coach-author {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    align-items: center;
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px dotted var(--panel-border);
}
.coach-author pre {
    flex: 1 1 100%;
    margin: 6px 0 0;
    max-height: 240px;
    overflow: auto;
    font-family: ui-monospace, Menlo, Consolas, monospace;
    font-size: 11px;
    line-height: 1.35;
    white-space: pre;
}

/* The coach highlight for NON-CARD targets (R7.2): the .hinted outline recipe
   above, applicable to any element. Cards keep .hinted itself (R7.1). */
.coach-mark {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}
/* A steady outline still says "here", without the pulse, under "Animations:
   Off" (body.motion-off, shared/ui/motion.ts). */
body.motion-off .coach-mark { animation: none; }
/* A highlight whose ring an ancestor would clip draws the same ring just
   inside its own edge instead. The tutorial engine sets .coach-inset only
   after measuring the clip (insetIfClipped in src/shared/tutorial/engine.ts),
   so the outside ring stays the default. !important because several game
   sheets restate the outline offset on their own highlighted cards
   (.clp-cell > .card.hinted and others) at higher specificity, and this
   measured answer has to win over all of them. */
.coach-mark.coach-inset,
.hinted.coach-inset { outline-offset: -3px !important; }

/* Narrow layout: at the site's single-column breakpoint #log-panel reorders
   BELOW the play area, which would put the coaching out of sight, so the panel
   docks to the bottom of the viewport instead (R6.2). Fixed positioning is
   viewport-relative, so the panel's DOM home in the sidebar does not matter.
   Opaque felt, because the board keeps rendering underneath and must not bleed
   through the prose. z-index 30 is over the board and under every overlay on
   this sheet's ladder (500 the snapshot viewer, 550, 600 the pass-and-play
   handoff gate, 900/1000 the card-flight layers): a privacy cover must always
   win over the coach. */
@media (max-width: 950px) {
    #coach-panel:not([hidden]) {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 30;
        max-height: 38vh;
        overflow-y: auto;
        margin: 0;
        padding: 10px 14px;
        border-bottom: 0;
        border-radius: 12px 12px 0 0;
        background: var(--felt-dark);
        box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.45);
    }
    /* The step counter keeps to the docked panel's own right padding. */
    #coach-panel:not([hidden]) h3[data-step]::before { right: 14px; }
    /* Reserve the docked bar's room at the end of the page, so it can never
       cover the action row the game puts at the bottom of the play area. Keyed
       with :has, which this sheet already uses elsewhere; the documented
       fallback is a body class toggled by the engine, which is not needed. */
    body:has(#coach-panel:not([hidden])) { padding-bottom: 40vh; }
}

.cardrow { display: flex; gap: 8px; align-items: flex-start; }
.cardrow.wrap { flex-wrap: wrap; }
.cardrow.mini { gap: 4px; }
/* One-line rows that can outgrow their column: scroll INSIDE the row rather
   than letting the row widen the page. Opt-in, because making every .cardrow a
   scroll container would clip the badges several games hang off a card's corner
   at negative offsets (.scr_pick_badge, .casino-build-chip, .gc-hp, ...) - an
   overflow container clips both axes, there is no per-axis escape, and the
   padding needed to clear those badges would change the vertical rhythm of all
   ~89 rows. The padding here is only what .card.sel's 5px lift needs, which is
   the same allowance .gin-row makes for the same reason. */
.cardrow.scroll {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: thin;
    padding: 6px 0 2px;
}

/* HANDS ARE CENTERED, site-wide (the Cuttle treatment's board axis; see
   "Center on the board axis" in .kiro/steering/ui-conventions.md). This is a
   deliberate global rule on the shared ids, not a leak: every game's hand tray
   is #hand-cards, and the games that show an opponent's hand as a row use
   #opp-cards, so one rule here centers all of them at once. Per-game rules add
   flex/gap/size on top and no longer need to restate the centering.
   #hand-cards never carries .scroll, so centered content cannot be clipped
   behind an overflow edge. One thing DOES defeat this rule: a child with an
   auto margin. Auto margins take all of the row's free space before
   justify-content sees any, so a `margin-left: auto` child pins itself right
   and packs every card before it to the LEFT. Dracula's Joker slot did exactly
   that; it now sits out of the flow at the right edge, with symmetric inline
   padding of its width on the row (src/dracula/style.css). A game that wants
   something pinned beside a centered hand does the same, or uses a grid. */
#hand-cards, #opp-cards { justify-content: center; }

/* PUBLIC KNOWLEDGE: a corner eye on a card in the viewer's own hand that the
   other players have already seen. `eyeBadge` in shared/ui/panels.ts builds it,
   and the point of styling it here is that "everyone has seen this card" looks
   the same in every game. Evolution Poker's .evo-eye and Cuttle's .ctl-eye are
   the same declarations under their own names and still apply on top; they came
   first, and the games name them in `ghostStripClasses`. */
.eye-badge {
    position: absolute; right: 5%; bottom: 4%;
    width: 20px; height: 20px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.55); color: #ffd76a;
    pointer-events: none;
}
.eye-badge svg { width: 14px; height: 14px; display: block; }
.card.mini .eye-badge { width: 14px; height: 14px; }
.card.mini .eye-badge svg { width: 10px; height: 10px; }

/* ---------- panels ---------- */

.panel {
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    padding: 14px 16px;
    margin-bottom: 14px;
}
/* A box title is centered over the box it names, wherever it sits: on its own,
   or as the h3 of a .panel-head (which centers it by flex as well). */
.panel h3 { margin: 0 0 10px; font-size: 15px; opacity: 0.9; text-align: center; }

/* The shared build panel (renderBuildPanel) centers everything on the board
   axis - the Cuttle treatment, applied once here for the whole poker family
   and every other game that borrows the panel. Stability: each hand row is a
   FIXED five slots (empties render as placeholders), so the row's box never
   changes size; the tray wraps and re-centers per card, which is the accepted
   cost (see the Cuttle section); the controls row's content is fixed per
   phase. */
.build-hand { margin-bottom: 12px; }
.build-hand-title { font-weight: 600; margin-bottom: 6px; text-align: center; }
.build-hand .cardrow { justify-content: center; }
#build-tray { justify-content: center; }
#build-tray-title { text-align: center; }
#build-controls { justify-content: center; }
.handname {
    margin-left: 10px;
    background: var(--accent); color: var(--accent-ink);
    padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 700;
}
.controls { display: flex; justify-content: center; gap: 8px; flex-wrap: wrap; margin: 12px 0; }

/* ---------- results ---------- */

/* Centered, like every other row on the board axis. The result TABLE below is
   the deliberate exception: its columns have to line up across rows, so the
   cells stay left-aligned. */
.winner-banner { font-size: 24px; font-weight: 800; margin-bottom: 12px; text-align: center; }
.result-table { display: grid; gap: 4px; }
.result-row { display: grid; grid-template-columns: 70px repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; align-items: center; }
.result-row.header .result-cell { font-weight: 700; }
.result-label { opacity: 0.75; font-size: 13px; }
.result-handname { font-size: 13px; opacity: 0.85; margin-top: 3px; }
.result-handname.best { color: var(--accent); font-weight: 700; opacity: 1; }
.result-cell.total { font-size: 22px; font-weight: 800; }
.result-row.totals { margin-top: 6px; }
.result-note { margin-top: 10px; font-size: 13px; opacity: 0.8; text-align: center; }
.result-actions { display: flex; justify-content: center; gap: 10px; margin-top: 16px; }

/* ---------- the summary, lifted to a modal ----------
 *
 * Shell-owned (GameShell.syncRoundOverlay): whenever a game shows a summary in
 * #result-panel with a live button in it - between rounds or at the end of the
 * match - the shell lifts the panel to a centered dialog, so the player cannot
 * miss it below the fold. #result-panel is the LAST element on a page, under the
 * board, the seats, the hand and the log, so on a phone a match could end and the
 * only sign of it was a status line.
 *
 * The DOM node is the same #result-panel (ids, tests, and tutorial selectors keep
 * working); only this class changes its geometry. z-index 500 keeps it UNDER the
 * pass-and-play handoff gate (600), the snapshot-viewer rule. Centering is
 * inset+margin:auto rather than a transform: a transform would make the element
 * the containing block for its own fixed-position ::before backdrop.
 *
 * These rules are the shell's, so they live here. Cuttle and Override each
 * carried a private copy of them (`ctl-resultmodal`, `ovr-resultmodal`) for as
 * long as this path handled between-rounds only.
 *
 * Selected by CLASS rather than by `#result-panel`, because four games summarize
 * themselves in a panel of their own and name it with summaryPanelId
 * (bal-summary, cmt-summary, hgs-summary, l3-summary). Only the shell ever adds
 * `result-overlay`, and only to one panel at a time. */
.panel.result-overlay {
    position: fixed;
    inset: 0;
    margin: auto;
    width: min(92vw, 560px);
    height: fit-content;
    max-height: min(80vh, 680px);
    overflow: auto;
    z-index: 500;
    /* The .panel background is a translucent tint; over the live board it
       must be opaque, so the tint is layered onto the dark felt. */
    background: linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
.panel.result-overlay::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -1;
    background: rgba(0, 0, 0, 0.5);
}
/* Everything inside a lifted panel reads down the middle. The score TABLE is a
   grid of rows, so it is the block that centers and the cells keep their own
   alignment: a centered cell would stop the columns lining up across rows.
   Not reset here: the `max-width` the four games above put on their own summary
   box inline. It survives into the dialog, at 560px to 620px against the
   dialog's own min(92vw, 560px), so it never bites - and a reset would lose the
   specificity fight anyway, since a game's sheet is read after this one. */
/* The beat BEFORE a summary lifts (shell.ts, syncRoundOverlay): the round's last
   move is still landing, so the panel is kept out of the page for that moment
   rather than flashing into the flow and shoving the board around first. */
.panel.result-settling { display: none; }
.panel.result-overlay { text-align: center; }
.panel.result-overlay .result-table { margin: 0 auto; }
.panel.result-overlay .result-actions { flex-wrap: wrap; }
/* The shell's own head row inside the lifted panel: just the View board
   control, right-aligned above the game's summary. */
.result-overlay-head {
    display: flex;
    justify-content: flex-end;
    margin: 0 0 6px;
}
/* The floating re-open chip while the summary is set aside: the way back must
   stay one tap away. Under the overlay and the gate, above the board. */
.roundpeek {
    position: fixed;
    left: 50%;
    bottom: 14px;
    transform: translateX(-50%);
    z-index: 400;
    white-space: nowrap;
    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
}

/* Solo replay comparison (shared/solo-replay.ts): after a one-player game,
   the graded bots replay the SAME deal headlessly and this block compares
   outcomes inside #result-panel. Chip colors: green border = you out-did that
   bot, red-ish = it out-did you, neutral = tie or still running. */
.replay-compare { margin-top: 18px; }
.replay-title { font-size: 17px; font-weight: 700; text-align: center; }
.replay-compare .result-note { margin-top: 2px; }
/* The chips are centered on the board axis, and every chip is one width:
   --replay-chip where there is room, never below --replay-chip-min. You and
   the bots group (.replay-bots, renderSoloComparison) are the only two flex
   items, so the row breaks only between them: four chips across when they fit,
   else You alone and the three bots on the next row. The bots group is a "3 or
   1" row (the flex-basis trick, *999): three equal chips across while each can
   be --replay-chip-min wide, otherwise one per row, so no bot is ever left
   alone under the other two. You takes the bots' width in every case, from the
   same rule. Widths come from CSS only, never from the text, so the layout is
   identical while the bots play and once they finish. */
.replay-scores {
    --replay-chip: 10em;
    --replay-chip-min: 7em;
    --replay-gap: 8px;
    display: flex; flex-wrap: wrap; justify-content: center;
    gap: var(--replay-gap); margin: 12px 0 0; font-size: 13px;
}
.replay-scores > .replay-score {
    flex: 0 1 min(var(--replay-chip), max((100% - 2 * var(--replay-gap)) / 3,
        (3 * var(--replay-chip-min) + 2 * var(--replay-gap) - 100%) * 999));
    min-width: 0;
}
.replay-bots {
    flex: 0 1 calc(3 * var(--replay-chip) + 2 * var(--replay-gap));
    min-width: 0;
    display: flex; flex-wrap: wrap; justify-content: center; gap: var(--replay-gap);
}
.replay-bots > .replay-score {
    flex: 1 1 calc((3 * var(--replay-chip-min) + 2 * var(--replay-gap) - 100%) * 999);
    min-width: 0; max-width: var(--replay-chip);
}
/* A grid of one column, so a bot chip's line and its two invisible reserves
   (data-pending, data-reserve: the placeholder and YOUR line) share one cell,
   and the cell is as tall as the tallest of the three. That is what keeps the
   row from growing as the bots finish. */
.replay-score {
    display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; row-gap: 2px;
    padding: 6px 8px; border-radius: 10px;
    background: var(--panel); border: 1px solid var(--panel-border);
    overflow-wrap: break-word;
}
.replay-score-name { grid-area: 1 / 1; font-size: 13px; font-weight: 700; opacity: 0.85; }
.replay-score-line { grid-area: 2 / 1; font-size: 13px; }
.replay-score[data-reserve]::before,
.replay-score[data-pending]::after {
    grid-area: 2 / 1; font-size: 13px; visibility: hidden;
}
.replay-score[data-reserve]::before { content: attr(data-reserve); }
.replay-score[data-pending]::after { content: attr(data-pending); font-style: italic; }
.replay-score.you { border-color: var(--accent); }
.replay-score.you .replay-score-name { color: var(--accent); opacity: 1; }
.replay-score.beat { border-color: rgba(90, 200, 120, 0.55); }
.replay-score.lost { border-color: rgba(220, 90, 90, 0.55); }
.replay-score.pending .replay-score-line { opacity: 0.6; font-style: italic; }
.replay-verdict, .replay-verdict-slot {
    margin-top: 10px; min-height: 1.4em; font-size: 14px; font-weight: 700; text-align: center;
}

/* Split Court's comparison shows the two built hands at FULL card size, so it
   opts out of the shared row grid above (five 72px cards plus gaps needs far
   more than a 180px column). Each player becomes a labeled block and the two
   hands sit side by side within it, wrapping on a narrow screen. */
.result-table.scr_result { gap: 14px; }
.result-table.scr_result .result-row {
    display: block;
    padding: 10px 12px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.result-table.scr_result .result-label {
    font-size: 15px;
    font-weight: 700;
    opacity: 1;
    margin-bottom: 8px;
}
.result-table.scr_result .result-cell {
    display: inline-block;
    vertical-align: top;
    margin: 0 22px 6px 0;
}
.result-table.scr_result .result-handname { font-size: 14px; }

/* ---------- log ---------- */

#log-panel {
    background: rgba(0, 0, 0, 0.22);
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    padding: 12px 14px;
    align-self: start;
    position: sticky;
    top: 12px;
    max-height: calc(100vh - 24px);
    display: flex;
    flex-direction: column;
}
#log-panel h3 { margin: 0 0 8px; font-size: 14px; }
#log-entries { overflow-y: auto; font-size: 13px; min-height: 60px; }
.logline { padding: 2px 0; border-bottom: 1px dotted rgba(255, 255, 255, 0.08); }
/* A `.section` line opens or closes a unit of play (a deal, a scored round, the
   finish). The log runs NEWEST FIRST, so a unit is bracketed on screen by its
   closing line at the top and its opening line at the bottom, and a one-sided
   margin would separate one of the two from the lines it belongs to. Symmetric
   padding gives every bold line air whichever end of its unit it sits at. */
.logline.section { font-weight: 700; padding: 5px 0; }
/* ---------- a collapsible unit of play in the log ----------
   Built by renderGroupedLogInto (src/shared/ui/panels.ts): every finished round,
   hand or deal folds to one line carrying its score, so a long match reads as a
   list of rounds. The shape came from Dracula, whose stylesheet carried the
   `cursor: pointer` rule globally before this existed.

   The native marker is replaced with a caret that turns, which the games that
   grew their own folds all converged on independently (split-court, cribbage,
   draft cribbage): the default triangle sits badly against the log's tight line
   height and does not read as openable at this size.

   ONLY THE SUMMARY IS BOLD. The wrapper is `.logline.loggroup`, never
   `.logline.section`: font-weight inherits, so a bold wrapper made every line
   of an opened round bold, which shipped more than once. The wrapper takes the
   section's padding and the summary alone takes its weight. */
details.loggroup { padding: 5px 0; }
details.loggroup > summary {
    font-weight: 700;
    cursor: pointer;
    list-style: none;
}
details.loggroup > summary::-webkit-details-marker { display: none; }
/* THE DISCLOSURE CARET IS DRAWN, NEVER THE GLYPH. The small triangles U+25B8
   and U+25BE sit about 8% of the font size BELOW the cap height in the site
   font (measured in Avenir Next at both weights: their ink centers at 0.27em
   above the baseline, the capitals' at 0.35em), so a caret beside a bold
   heading read as dropped. Rotating the glyph to open it made that worse, the
   other way: its ink is left of its 1em box, so the turn about the box's
   center threw the down-pointing caret high. A border triangle has its ink
   exactly filling its own box, so it turns in place, and `vertical-align`
   puts that box's center on the cap height in any font that keeps its
   capitals near 0.7em (Avenir Next, Segoe UI, San Francisco all do).
   It is a clipped box, not a border triangle: border widths are floored to
   whole pixels, which at 13px cut 0.22em to 2px and dropped the caret a
   pixel. Width and height are laid out fractionally, so this stays exact.
   Footprint: 0.06 + 0.38 + 0.56 = 1em, the old glyph box, so the heading text
   starts where it always did. `.caret` is the same shape for an inline
   toggle in text (caretEl in src/shared/ui/panels.ts). */
details.loggroup > summary::before,
.caret {
    content: '';
    display: inline-block;
    width: 0.38em;
    height: 0.44em;
    background: currentColor;
    clip-path: polygon(0 0, 100% 50%, 0 100%);
    vertical-align: 0.13em;
    transition: transform 0.12s ease-out;
}
details.loggroup > summary::before {
    margin: 0 0.56em 0 0.06em;
    opacity: 0.7;
}
details.loggroup[open] > summary::before,
.caret.caret-open { transform: rotate(90deg); }
.caret.caret-lead { margin-right: 0.4em; }
.caret.caret-tail { margin-left: 0.4em; }
body.motion-off .caret { transition: none; }
/* The unit's own lines, indented under its heading, at normal weight: nothing
   above them is bold now, and renderGroupedLogInto strips `section` off a
   game's boundary lines when it puts them in here. */
.loggroup-body { padding-left: 12px; }
.loggroup-body > .logline { border-bottom: 0; }
.loggroup-body > .result-note { font-size: 12px; opacity: 0.8; margin: 2px 0 6px; }
.loggroup-body > .result-table { margin: 0 0 8px; max-width: 320px; gap: 2px; }
.loggroup-body .result-label,
.loggroup-body .result-cell { font-size: 12.5px; }
body.motion-off details.loggroup > summary::before { transition: none; }

.cardlabel { font-weight: 700; white-space: nowrap; }
/* Suit color on a card label, per SUIT rather than per color family - the
   classes come from suitTextClass() in src/shared/cards.ts. `red` stays the
   base for BOTH red suits, so every scoped `.red` rule elsewhere in this file
   keeps working untouched and a diamond still reads red on a surface that has
   not opted in; `.diamond` and `.club` only refine the hue where a companion
   rule like these exists. Spades gets no class and keeps --ink. */
.cardlabel.red { color: var(--red); }
.cardlabel.red.diamond { color: var(--red-diamond); }
.cardlabel.club { color: var(--blue-club); }

/* ---------- mobile / PWA ---------- */

button, .card.clickable { touch-action: manipulation; }
.installnote { margin-top: 16px; font-size: 13px; opacity: 0.7; }

@media (max-width: 560px) {
    /* Continuous shrink across phone widths, replacing a flat 56px that treated
       a 320px iPhone SE and a 560px large phone identically - the second half of
       the "too wide on my phone" cause (the first is a card row that cannot
       wrap; see .cardrow).
       22.4px + 6vw passes through (560, 56) and (360, 44): 56px exactly AT the
       breakpoint, so a phone that was getting the old flat 56px still does and
       only NARROWER screens change. The 56 -> 72 step at 561px is inherited
       behavior, not new - the base curve's floor is 72px, deliberately, so that
       the 561-1200px band keeps the sizes every board was tuned against. The
       40px floor is just under an SE's 41.6px, so it only catches something
       narrower than any phone in service. */
    :root { --cw: clamp(40px, calc(22.4px + 6vw), 56px); }
    /* The other half of "too wide on my phone", and the bigger half: .cardrow is
       display:flex with NO flex-wrap and .card is flex:none, so the default card
       row can neither wrap nor shrink nor scroll - it just gets wider than the
       screen and takes the whole PAGE sideways with it, chrome included. Only 22
       of the 89 rows in src/ opt into .wrap.
       On phones, wrapping is the right DEFAULT: every card stays visible and
       tappable, nothing is clipped, and no scrollbar appears.
       A DEFAULT, so it must lose to any row that says otherwise. The exclusions
       sit inside :where(), which leaves the rule at ONE class of specificity.
       Every game sheet comes after this one (tools/gen-css.mjs ORDER), so a
       game's own `.its-row { flex-wrap: nowrap }` or `gap` wins on order with
       no extra weight. Written as a chain of :not()s this was four classes, and
       it silently wrapped rows that said nowrap (Swoop's fan, Woo's seat fans,
       Gin Rummy's opponent strip) while two games stacked ids and extra classes
       to beat it. Do not add specificity back here; a row that must stay on one
       line says so itself, or uses .scroll.
       .otm-pile and .twenty-opprow are overlap fans that rely on the default
       nowrap rather than declaring it, so they stay excluded by name.
       The row-gap is tighter than .cardrow's 8px so a wrapped plain row reads as
       one row. .cardrow.mini keeps its own 4px, because two classes beat one. */
    .cardrow:where(:not(.scroll, .otm-pile, .twenty-opprow)) {
        flex-wrap: wrap;
        row-gap: 6px;
    }
    /* ONE chrome row on phones: the title shrinks and ellipsizes, and the
       toolbar buttons drop their text labels (the icons carry the meaning;
       every control keeps its aria-label and tooltip). This replaced a
       flex-wrap that stacked the chrome three rows / 152px tall and pushed
       the board down by a whole panel. The hub's About/Contact buttons have
       no .btnlabel span, so they keep their text and simply tighten. */
    .topbar { gap: 8px; padding: 8px 12px; }
    .topbar h1 { flex: 1 1 auto; min-width: 0; font-size: 17px; }
    /* Block, not flex: text-overflow only ellipsizes inline content. */
    .topbar h1 .pagehome {
        display: block;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .topbar .pageicon { width: 22px; height: 22px; vertical-align: -5px; }
    .toolbar { flex: none; flex-wrap: nowrap; gap: 6px; }
    .toolbar .btnlabel { display: none; }
    /* Square icon buttons: 18px glyph + padding = a ~38px tap target. */
    .toolbar .btn { padding: 9px; }
    .toolbar .btnicon { width: 18px; height: 18px; }
    #screen-game { padding: 10px; gap: 10px; }
    .setup-card { margin: 16px 12px; padding: 20px; }
    .result-row { grid-template-columns: 54px repeat(auto-fit, minmax(140px, 1fr)); }
    .card.mini { --cw: 34px; }
}
/* Whole-pixel card width on phones too - same reason and same @supports
   guard as the base curve near the top of this file. Phones are where this
   matters most: they run at devicePixelRatio 2-3, so a half-CSS-pixel error
   is a 1-2 device-pixel seam down the edge of every card. */
@supports (width: round(down, 1.5px, 1px)) {
    @media (max-width: 560px) {
        :root { --cw: round(down, clamp(40px, 22.4px + 6vw, 56px), 1px); }
    }
}

/* ---------- ring poker board ---------- */

.ring-board {
    --ring-r: 150px;
    --pair-r: 78px;
    position: relative;
    width: 420px;
    height: 420px;
    margin: 10px auto 16px;
}
.ring-slot, .pairbtn, .ring-center {
    position: absolute;
    left: 50%;
    top: 50%;
}
.ring-slot {
    transform: translate(-50%, -50%)
        rotate(var(--angle)) translate(var(--ring-r))
        rotate(calc(-1 * var(--angle)));
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.pile-badge {
    min-height: 18px;
    font-size: 11px;
    opacity: 0.65;
    background: rgba(0, 0, 0, 0.35);
    border-radius: 999px;
    padding: 1px 8px;
}
.pile-badge:empty { background: none; }
.ring-center {
    transform: translate(-50%, -50%);
    text-align: center;
    pointer-events: none;
}
.ring-round { font-size: 18px; font-weight: 800; }
.ring-total { font-size: 12px; opacity: 0.7; }
.pairbtn {
    transform: translate(-50%, -50%)
        rotate(var(--angle)) translate(var(--pair-r))
        rotate(calc(-1 * var(--angle)));
    width: 44px;
    height: 44px;
    border-radius: 50%;
    font-size: 18px;
    color: var(--accent-ink);
    background: var(--accent);
    border: 0;
    cursor: pointer;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
}
.pairbtn:hover { background: #ffdf8e; transform: translate(-50%, -50%) rotate(var(--angle)) translate(var(--pair-r)) rotate(calc(-1 * var(--angle))) scale(1.12); }
.pairbtn.hinted { animation: hintpulse 0.9s ease-in-out infinite alternate; outline: 3px solid #fff; }
@media (max-width: 560px) {
    /* Fluid below the breakpoint instead of a flat 330px square. 330 is wider
       than the 300px play area on a 320px screen, so the ring hung 20px off the
       right edge - a measured instance of "too wide on my phone".
       Every derived value is a RATIO of the one size variable (118/330 = .358,
       62/330 = .188, 52/330 = .158), so the ring keeps its proportions as it
       shrinks and is unchanged at 560px, where min() still picks 330.
       The size is a length (vw and px), never a percentage: the slots are
       absolutely positioned off --ring-r, and a percentage there would resolve
       against whichever property consumed it rather than the board's width. */
    .ring-board {
        --ring-size: min(330px, calc(100vw - 30px));
        --ring-r: calc(var(--ring-size) * 0.358);
        --pair-r: calc(var(--ring-size) * 0.188);
        width: var(--ring-size);
        height: var(--ring-size);
    }
    /* Same ratio treatment, rounded to a whole pixel for the atlas (see :root). */
    .ring-board .card { --cw: round(down, calc(var(--ring-size) * 0.158), 1px); }
    .pairbtn { width: 38px; height: 38px; }
}
/* No round() support: the un-snapped ratio, which is soft but correctly sized -
   the same fallback the other rounded rules take. Declared BEFORE the rounded
   form would be wrong (it must lose), so it is scoped by @supports not: the
   rule above stays the only one where round() exists. */
@supports not (width: round(down, 1.5px, 1px)) {
    @media (max-width: 560px) {
        .ring-board .card { --cw: calc(var(--ring-size) * 0.158); }
    }
}

/* ---------- exchange poker board ---------- */

/* Everything on one axis (Cuttle treatment): the center row, its controls,
   and the locked strip (the hand is centered by the site-wide #hand-cards
   rule). #center-row / #exchange-controls / #locked-cards are this page's own
   ids. The center row reserves one card row so the panel cannot collapse in
   the beat between phases. */
#center-row { justify-content: center; min-height: calc(var(--cw) * 89 / 64 + 17px); }
#exchange-controls { justify-content: center; }
/* Pass is laid out on every turn and only shown on yours, so the hand below the
   row never moves (it used to appear and disappear with a help line beside
   it). */
#exchange-controls .btn.idle { visibility: hidden; }
.panel.xch-handpanel #locked-cards { justify-content: center; }
.panel.xch-handpanel .locked-title { text-align: center; }
.center-cell { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.card.sel {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    transform: translateY(-5px);
}

/* Grid/Ring Poker show an opponent's ENTIRE collection (15-18 cards, all taken
   face up), so unlike Exchange Poker's short "taken" strip this one stacks the
   label above a wrapping row and is bounded by the panel, which is itself
   bounded by the viewport - otherwise 18 cards in a nowrap row would push the
   opponents strip off the side of a phone. */
.opponent { max-width: 100%; }
/* FIXED SIZE from the first pick to the last, because this strip sits above the
   board (ui-conventions, "Containers must not move"). A collection is 15 to 20
   cards, so the row is ten cards wide and two rows are reserved: it never widens
   as cards arrive and never grows a row mid-draft. In terms of the card width
   and the 89/64 aspect, so the reservation follows the card size. The 4px in
   both reservations is .cardrow.mini's gap, on both axes: it outranks the
   phone wrap rule's 6px row-gap. */
.opponent-cards { margin-top: 6px; --occw: 26px; }
.opponent-cards .card { --cw: var(--occw); }
.opponent-cards .cardrow.mini {
    width: calc(10 * var(--occw) + 9 * 4px);
    max-width: 100%;
    min-height: calc(2 * var(--occw) * 89 / 64 + 4px);
    align-content: flex-start;
    justify-content: center;
}
/* Grid and Ring Poker's status line alternates between one line ("AI is
   picking...") and two ("Your turn: take a row or column (...)") on a phone,
   and it sits above the board: without a reservation the board moved 25px at
   every turn change. Two lines are reserved (18px type at line-height 1.4). */
@media (max-width: 560px) {
    .status.draftpoker-status { min-height: 2.8em; }
}
/* Grid Poker's progress line carries the grid counter and the deck chip side by
   side, which saves the row a separate pile line cost above the board (28px on
   a phone). The line climbs out of .progress's 0.7 opacity so the chip keeps
   its own 0.85, and the counter takes the 0.7 instead. The min-height is the
   chip's height, so the line is as tall with the chip gone (building, behind
   the handoff gate) as with it. */
#progress-line.gpk-progress {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    opacity: 1;
    min-height: 28px;
}
.gpk-progress .gpk-round { opacity: 0.7; white-space: nowrap; }
.gpk-progress .gpk-deckchip { white-space: nowrap; }
/* Grid Poker's table: the opponents BESIDE the grid where the column has room
   for both, and above it (as before) where it does not. Stacked, a desktop
   board was the strip (139px) plus the grid (395px) tall, and the hand started
   at the bottom of a 720px window. The test is the column's own width
   (.gpk-table is the container), not the window's, because the play column is
   narrower than the window by the log beside it. 640px is one opponent panel
   (326px: ten 26px cards, the gaps, the padding) plus the grid at the desktop
   card size (294px) and the 20px between them. Side by side, the opponents
   stack in a column, so three players' two panels (286px) still sit within
   the grid's height. Only while the grid is shown: in the build it is hidden,
   and a column of three players' two chips would be 40px taller than the row
   they make above the build panel.
   THE GRID IS WHAT IS CENTERED, not the pair: it is the thing being played,
   and the status line above it and the hand below it are on the column's
   axis. Three tracks, `1fr auto 1fr`, the grid in the middle one and the
   opponents right-aligned in the left one. The opponents (326px and a 12px
   gap) fit the left track's half of the spare width in a 932px column at the
   desktop's 0.96 zoom, so the grid sits on the column's axis there. Where
   they do not, the track grows to fit them and the grid moves right by only
   that much (17px in the same column at full size, more in a narrower one). */
.gpk-table { container-type: inline-size; }
@container (min-width: 640px) {
    .gpk-tablerow:has(> #board:not([hidden])) {
        display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
    }
    .gpk-tablerow:has(> #board:not([hidden])) > #opponents {
        grid-column: 1; justify-self: end;
        flex-direction: column; flex-wrap: nowrap; margin: 0 12px 0 0;
    }
    /* #board is an inline-size container on every page (src/duelpoker/
       style.css), and a container has no width of its own to give the auto
       track it sits in: it laid out 0px wide, with the grid hanging out of it
       to the right (at 1024px, past the play area). The grid's tracks are
       fixed and nothing in it reads cqw, so here it can be sized by its
       content. The containment's other job, keeping the grid's margins from
       collapsing through #board, a grid item already does. */
    .gpk-tablerow:has(> #board:not([hidden])) > #board { container-type: normal; grid-column: 2; }
}
/* Grid Poker's results, shown in the page at the end (def.ts finalSummary:
   'inline'): as wide as the table and centered, not the column's full width
   with the table's tracks stretched across it. The shared rows are
   `70px repeat(auto-fit, minmax(180px, 1fr))`, which only sizes against a
   width it is given; here each cell is an implicit column exactly as wide as
   a hand of five mini cards (46px each, 4px apart), so the panel has a width
   of its own to fit and every row's columns still line up. A longer hand name
   wraps inside its cell.
   WHERE THE COLUMN IS NARROWER THAN THAT (three players at 1024px: 872px of
   table in a 676px column) the columns shrink together, and each cell is a
   container that sizes its five cards to fit, rather than the panel scrolling
   sideways. The narrow, stacked form (.result-stack, every phone) keeps its
   shared layout. */
.panel.gpk-result:has(> .result-table:not(.result-stack)) {
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
}
.panel.gpk-result .result-table:not(.result-stack) .result-row {
    grid-template-columns: 70px;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, calc(5 * 46px + 4 * 4px));
}
.panel.gpk-result .result-table:not(.result-stack) .result-cell { container-type: inline-size; }
.panel.gpk-result .result-table:not(.result-stack) .result-cell .card.mini {
    --cw: min(46px, calc((100cqw - 4 * 4px) / 5));
}
/* Grid Poker's hand on a desktop: ONE ROW, its cards no wider than the tray
   allows (the Piquet tray: #hand-cards is the container, and def.ts sets
   --gpk-n to the card count). A hand grows to 18 cards with 2 players and 20
   with 3, and wrapped onto a second row the panel grew 111px mid-draft, which
   pushed the hand off a 720px window (and, framed, would shrink the whole board
   mid-draft). Cards keep the site size until the row is full, then narrow as
   more arrive, never below the 40px floor (past it the row wraps).
   THE ROW IS RESERVED AT ONE FULL-SIZE CARD, the tallest this tray gets here.
   ui/handspace.ts cannot hold it: every pick adds 2 or 3 cards, which it reads
   as a fresh deal, and the tray starts the game empty, which it ignores. So
   without this the tray grew a row at the first pick and shrank as the cards
   narrowed.
   THE BUILD TRAY IS THE SAME ROW, sized for the whole collection (def.ts sets
   --gpk-n to it), so its cards keep one size while they are placed. Two rows
   of it put the build 70px past a 720px window at the 0.8 zoom floor. */
@media (min-width: 951px) {
    .panel.grid-handpanel, .panel.gpk-buildpanel { --gpk-handcw: var(--cw); }
    .panel.grid-handpanel #hand-cards, .panel.gpk-buildpanel #build-tray {
        container-type: inline-size;
        gap: 6px;
        min-height: calc(var(--gpk-handcw) * 89 / 64);
    }
    .panel.grid-handpanel #hand-cards .card, .panel.gpk-buildpanel #build-tray .card {
        --cw: max(40px, min(var(--gpk-handcw),
            calc((100cqw - (var(--gpk-n, 1) - 1) * 6px) / var(--gpk-n, 1))));
    }
}
/* Their latest pick, marked until their next one. Quieter than the bot
   telegraph (.ai-telegraph), which is the same accent at full strength. */
.opponent-cards .card.recent {
    outline: 2px solid rgba(255, 209, 102, 0.8);
    outline-offset: 1px;
}
/* ---------- deck tracker (shared/ui/decktracker.ts) ----------
   The whole deck as a grid: a face for every card still to come, a dimmed and
   struck-through face for every discard, a blank for everything already on
   screen. Grid and Ring Poker, below the hand. Cells use the simplified atlas at
   every size (26px, 20px on a phone). 14 columns plus the suit gutter fit a 390px
   screen at 20px; the box scrolls rather than clipping if a screen is narrower. */
.deck-track { overflow-x: auto; }
.deck-track-head {
    display: flex; justify-content: center; align-items: baseline;
    flex-wrap: wrap; gap: 4px 14px; margin-bottom: 6px;
}
.deck-track-head .deck-track-title { margin: 0; }
.deck-track-legend { display: flex; gap: 12px; font-size: 13px; opacity: 0.85; }
.deck-track-key::before {
    content: ''; display: inline-block; width: 10px; height: 13px;
    margin-right: 5px; vertical-align: -2px; border-radius: 2px;
    background: #f4f1e8;
}
.deck-track-key.discarded::before {
    opacity: 0.4;
    background: linear-gradient(to top right, #f4f1e8 45%, #c0392b 45%, #c0392b 55%, #f4f1e8 55%);
}
.deck-track-grid {
    --dtcw: 26px;
    display: grid;
    grid-template-columns: 1.4em repeat(14, var(--dtcw));
    gap: 2px;
    align-items: center;
    width: max-content;
    margin-inline: auto;
}
.deck-track-grid .card.mini { --cw: var(--dtcw); }
.deck-track-rank { font-size: 11px; text-align: center; opacity: 0.55; font-variant-numeric: tabular-nums; }
.deck-track-suit { font-size: 14px; text-align: center; }
.deck-track-suit.red { color: var(--red); }
.deck-track-suit.red.diamond { color: var(--red-diamond); }
.deck-track-suit.club { color: var(--blue-club); }
/* Out of the deck and on screen elsewhere: the cell keeps its shape so the grid
   stays a readable map, but the face goes (as in Solo Poker's grid). */
.deck-track-grid .card.deck-track-out {
    background-image: none;
    background-color: rgba(255, 255, 255, 0.05);
    box-shadow: none;
    border: 0;
}
/* Discarded: the face stays, so it can be read, but dimmed and struck through
   so it cannot be mistaken for a card still to come. */
.deck-track-grid .card.deck-track-discarded { position: relative; opacity: 0.45; }
.deck-track-grid .card.deck-track-discarded::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(to top right,
        transparent calc(50% - 1.5px), #c0392b calc(50% - 1.5px),
        #c0392b calc(50% + 1.5px), transparent calc(50% + 1.5px));
    pointer-events: none;
}
.deck-track-blank { width: var(--dtcw); }
@media (max-width: 560px) {
    .deck-track-grid { --dtcw: 20px; gap: 1px; }
}
/* Exchange Poker's "taken" strip: the cards an opponent took from the open
   center, oldest on the left. FIXED SIZE for the whole exchange phase, like the
   drafting games' strip above: the label sits over a row reserved for every
   take the game allows (`--taken-slots`, set by the renderer: phases times
   exchanges), so the panel never widens or wraps as cards arrive and the board
   below it stays put. */
.opponent-taken { margin-top: 6px; --tkcw: 26px; }
.opponent-taken .card { --cw: var(--tkcw); }
.opponent-taken .cardrow.mini.taken-row {
    gap: 4px;
    width: calc(var(--taken-slots, 9) * var(--tkcw) + (var(--taken-slots, 9) - 1) * 4px);
    max-width: 100%;
    min-height: calc(var(--tkcw) * 89 / 64);
    justify-content: center;
}
/* Three or four players: six takes per opponent at 21px is 146px of strip, so
   two panels (174px each) share a 370px phone row instead of one per row. */
#opponents.xch-crowded .opponent-taken { --tkcw: 21px; }
.opponent-taken-label { font-size: 11px; opacity: 0.65; text-transform: uppercase; letter-spacing: 0.4px; }

/* ---------- duel poker board ---------- */

/* One .duel-row per board row: a left .duel-side (4 fixed card cells), the
   .duel-shared center card, a right .duel-side - nine card columns in all.
   Side cells hold .card or .card.empty placeholders so the 4-slot capacity
   stays visible. --duel-cw, the cell width that makes the nine columns fit, is
   set in src/duelpoker/style.css from the width of the board's own container. */
.duel-board {
    --duel-gap: 6px;          /* between cells within a side */
    --duel-center-gap: 10px;  /* around the shared center column */
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: fit-content;
    margin: 10px auto 16px;
}
.duel-board .card { --cw: var(--duel-cw); }
.duel-row {
    position: relative;       /* anchors .duel-row-badge */
    display: grid;
    grid-template-columns: auto auto auto;
    gap: var(--duel-center-gap);
    align-items: center;
}
.duel-side {
    position: relative;       /* anchors .duel-side-caption */
    display: grid;
    grid-template-columns: repeat(4, var(--duel-cw));
    gap: var(--duel-gap);
    padding: 3px;
    /* Transparent by default so the .own/.legal borders never shift layout. */
    border: 1px solid transparent;
    border-radius: 10px;
}
/* The viewing player's own side of each row. */
.duel-side.own {
    background: rgba(255, 255, 255, 0.05);
    border-color: var(--panel-border);
}
/* Legal target while a card is selected or dragged; the drag module also
   toggles .dropzone-active on the hovered zone, same as the other games. */
.duel-side.legal {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.08);
    cursor: pointer;
}
.duel-side.legal:hover { background: rgba(255, 209, 102, 0.16); }
/* Crazy strengthens the always-on own-side distinction: with BOTH sides of
   every row a legal drop target, the transient gold .legal marking is no
   longer enough to tell your side from your opponent's, so the persistent
   tint carries more weight - a brighter own-side fill and a solid border,
   plus a faint wash on the opponent's side so the two read apart at rest. The
   transient .legal gold still overrides both during your turn. */
.duel-board.crazy .duel-side.own {
    background: rgba(255, 255, 255, 0.10);
    border-color: var(--panel-border);
}
.duel-board.crazy .duel-side:not(.own) {
    background: rgba(0, 0, 0, 0.16);
}
/* Persistent side-ownership header: names the two board columns
   viewer-relative ("You" / opponent). It reuses .duel-row's three-column grid
   and matches each column to the side/shared widths (4 cells + gaps + the
   side's 3px padding and 1px border = +8px; the center to one card + the same
   +8px), so the labels sit centered over their columns and the nine-column
   card grid below never shifts. Tucked close to the first row. */
.duel-owner-header { margin-bottom: -4px; }
.duel-owner-label {
    width: calc(4 * var(--duel-cw) + 3 * var(--duel-gap) + 8px);
    text-align: center;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    opacity: 0.6;
}
/* The viewer's own column reads in the gold accent (matching the .own side and
   the "You won" badge), so "your" side is unmistakable at a glance. */
.duel-owner-label.own { color: var(--accent); opacity: 0.85; }
.duel-owner-spacer { width: calc(var(--duel-cw) + 8px); }
/* Shared center card: gold framing, it belongs to both players' hands. */
.duel-shared {
    padding: 3px;
    border: 1px solid rgba(255, 209, 102, 0.55);
    border-radius: 10px;
    background: rgba(255, 209, 102, 0.08);
}
/* The most recently played card on the board (from the last `played` log
   event, so it survives resume/undo/online sync). A cool blue glow - the
   gold accent already means "selected/hinted/legal" (.sel, .hinted,
   .duel-side.legal), so the last-play marker gets its own hue (the blue
   already used by Split Court's set-a highlight). Play phase only: the
   showdown board belongs to the badges and captions. */
.duel-board .card.last-played {
    outline: 3px solid #6aa9e0;
    outline-offset: 1px;
    box-shadow: 0 0 10px rgba(106, 169, 224, 0.65);
}

/* Showdown outcome marker (left/right/tie), hung centered over the row. */
.duel-row-badge {
    position: absolute;
    top: -8px;
    left: 50%;
    transform: translateX(-50%);
    padding: 1px 8px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 800;
    color: var(--accent-ink);
    background: var(--accent);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
    white-space: nowrap;
    pointer-events: none;
}
.duel-row-badge:empty { display: none; }
/* A row already settled DURING play (rowStanding): the same badge, slightly
   dimmed and lighter-weight than the final showdown badge, so the live
   board reads a touch quieter than the finished one. */
.duel-row-badge.provisional {
    opacity: 0.8;
    font-weight: 700;
}
/* Viewer-relative outcome tint, additive over the base badge (and its
   .provisional dimming): green when the seat holding the device took the
   row, red when the opponent did; ties keep the neutral gold accent. Same
   light-pill-dark-ink pattern as the accent badge so both variants stay
   legible on the dark felt: the red reuses the palette's --red (the
   card-label/error red), the green is its brightness-matched counterpart
   (no green existed in the palette; Split Court's #2f6f4f "mine" green
   would vanish into the felt at this size). */
.duel-row-badge.won {
    background: #8fdca8;
    color: #0b3b20;
}
.duel-row-badge.lost {
    background: var(--red);
    color: #4a1010;
}

/* Completed-hand caption: once a side holds all four cards, the name of its
   five-card poker hand hangs centered on that side's bottom edge (both
   sides are public, so either player's full side carries one). Absolute
   like .duel-row-badge so the nine-column grid never shifts. It straddles
   the side's border: with the 1px border it pokes only 1px into the 10px
   row gap, staying clear of the next row's showdown badge, which reaches
   8px up into the same gap. Ellipsized to the side's width so long hand
   names never cross into the center column or collide with the badge at
   the showdown. Deliberately smaller and dimmer than the badge. */
.duel-side-caption {
    position: absolute;
    bottom: -2px;
    left: 50%;
    transform: translateX(-50%);
    max-width: calc(100% - 10px);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    padding: 0 8px;
    border-radius: 999px;
    font-size: 10px;
    line-height: 14px;
    font-weight: 600;
    color: var(--ink);
    opacity: 0.85;
    background: var(--felt-dark);
    border: 1px solid var(--panel-border);
    pointer-events: none;
}

/* A PARTIAL side's caption (1-3 placed cards): the made hand so far. It is
   provisional - the side will still improve or bust - so it sits dimmer
   than a full side's fixed-hand caption. */
.duel-side-caption.partial { opacity: 0.55; }

/* Phones: tighter gaps. --duel-cw reads them, so the cards widen to match. */
@media (max-width: 560px) {
    .duel-board {
        --duel-gap: 3px;
        --duel-center-gap: 6px;
    }
    /* Cards are ~37px tall at 320px wide: shrink the caption so it covers
       less of the card bottoms and cannot brush the showdown badge. */
    .duel-side-caption { font-size: 9px; line-height: 12px; padding: 0 6px; }
}

/* ---------- drag and drop ---------- */

.card.clickable, [data-card-id] { touch-action: none; }
.card.dragging { opacity: 0.35; }
.drag-ghost {
    position: fixed;
    transform: translate(-50%, -60%) rotate(3deg);
    pointer-events: none;
    z-index: 1000;
    opacity: 0.92;
    box-shadow: 0 8px 22px rgba(0, 0, 0, 0.55);
}
.cardrow.droppable { min-height: calc(var(--cw) * 89 / 64); border-radius: 8px; }
.dropzone-active {
    outline: 2px dashed var(--accent);
    outline-offset: 4px;
    background: rgba(255, 209, 102, 0.08);
}

/* ---------- exchange poker locked cards ---------- */

.locked-title { margin-top: 14px; }
.locked-row .card.locked { filter: saturate(0.75) brightness(0.9); }

/* ---------- game icons ---------- */

/* 44px, down from 56px: the icon box, not the text, was setting the minimum
   height of every tile without a record line. */
.gameicon { width: 44px; height: 44px; flex: none; }
.gamelink { flex-direction: row; align-items: center; gap: 12px; }
.gametext { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.pageicon { width: 26px; height: 26px; vertical-align: -6px; margin: 0 2px; }

/* ---------- compact results (4 players) ---------- */

#result-panel { overflow-x: auto; }
.result-table.compact .result-row {
    grid-template-columns: 54px repeat(auto-fit, minmax(150px, 1fr));
    gap: 6px;
}
.result-table.compact .cardrow.mini { gap: 2px; }
.result-table.compact .card.mini { --cw: 27px; }
.result-table.compact .result-handname { font-size: 12px; }

/* ---------- narrow-screen results (one stacked block per hand) ----------
   The row grid above sits every player's hand for a slot side by side, which
   cannot work on a phone: a five-card mini row needs 5x34 + 4x4 = 186px but the
   track floor is 140px. So #result-panel's overflow-x kicked in (swipe to see
   the table) AND the fifth card was painted past its track, because .card is
   `flex: none` and the mini row does not wrap. Both reported symptoms, one
   cause.

   With .result-stack each slot becomes a full-width labeled block: the header
   row is dropped, every cell names its own player instead, and a hand gets the
   whole screen width - so its five cards fit on one line at a LARGER size than
   before. The class is applied by renderResultPanel rather than by a media
   query here, because the width where the grid stops fitting depends on the
   player count (see stackBelowPx). Games that render their own results table
   never get it and keep their bespoke layout. */
.result-cell-who { display: none; }
.result-stack .result-row { grid-template-columns: 1fr; gap: 4px; }
.result-stack .result-row.header { display: none; }
.result-stack .result-row:not(.totals) {
    padding: 8px 10px; border-radius: 10px;
    background: var(--panel); border: 1px solid var(--panel-border);
}
.result-stack .result-label { font-size: 14px; font-weight: 700; opacity: 1; }
.result-stack .result-cell + .result-cell { margin-top: 8px; }
.result-stack .result-cell-who {
    display: block; font-size: 12px; font-weight: 700; opacity: 0.7; margin-bottom: 3px;
}
.result-stack .card.mini { --cw: 42px; }
/* A stacked block has no columns to line up with its neighbors, so the hand is
   centered like every other hand on the site (ui-conventions, "Center, do not
   left-align"). The lifted dialog already centered the TEXT; the card row is a
   flex row, which text-align cannot reach, so it sat against the left edge
   under a centered player name and hand name. */
.result-stack .result-row { text-align: center; }
.result-stack .cardrow.mini { flex-wrap: wrap; justify-content: center; }
/* Totals stay on one line ("Total  You 3  AI 1"): a flex row, since a grid
   item would blockify the cells and stack them down the screen. */
.result-stack .result-row.totals {
    display: flex; flex-wrap: wrap; gap: 4px 20px; align-items: baseline;
    justify-content: center;
}
.result-stack .result-row.totals .result-cell + .result-cell { margin-top: 0; }

/* ---------- pass-and-play handoff ---------- */

.setup-subhead { margin: 22px 0 10px; font-size: 14px; opacity: 0.85; }

.handoff {
    position: fixed;
    inset: 0;
    z-index: 600;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    /* Fully opaque: nothing behind may be readable during a handoff. */
    background: radial-gradient(ellipse at 50% -20%, var(--felt), var(--felt-dark) 70%);
}

.handoff-card {
    max-width: 460px;
    text-align: center;
    padding: 32px 36px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
}
.handoff-card h2 { margin: 0 0 10px; }
.handoff-card p { margin: 0 0 22px; opacity: 0.85; }

/* ---------- the shell's end-of-game buttons ---------- */

/* The strip under the status lines (#endgame-line, shell-owned): "Review game"
   and, online, "Change game", visible only once the game is over and only for
   a game that paints no result panel of its own to put them in. Named for the
   share button until that feature was archived (archive/share/). */
.endgame-line { display: flex; justify-content: center; gap: 8px; margin: 2px 0; }

/* ---------- game review (#review-line, shell-owned; see shell/review.ts) ----------
 *
 * The transport bar sits just below the endgame strip, above the board, and
 * keeps one height whatever the position shows: the board below must not
 * jump as the counter text changes. Controls wrap on a phone; the scrubber
 * takes whatever width is left on its row. */
.review-line {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin: 2px 0 8px;
    padding: 8px 10px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.04);
}
.review-transport {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    min-height: 32px;
}
.review-nav, .review-play { min-width: 36px; font-size: 13px; line-height: 1.2; }
/* The scrubber sits in a wrapper so round tick marks can overlay its track.
   The input is draggable (the transport updates in place, so the node under
   the pointer survives every jump - see shell/review.ts TransportRefs). */
.review-scrubwrap {
    position: relative;
    display: flex;
    align-items: center;
    flex: 1 1 140px;
    min-width: 120px;
}
.review-scrub {
    width: 100%;
    accent-color: var(--accent);
    cursor: pointer;
}
/* Round boundaries: thin ticks over the track, inert to the pointer so a
   drag that starts on one still lands on the input. */
.review-scrub-marks { position: absolute; inset: 0; pointer-events: none; }
.review-scrub-mark {
    position: absolute;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 2px;
    height: 12px;
    border-radius: 1px;
    background: rgba(255, 255, 255, 0.55);
}
.review-counter {
    /* --review-counter-w is the widest label of this review (review.ts
       holdCounterWidth), so the scrubber beside it keeps one width. */
    min-width: max(9ch, var(--review-counter-w, 0px));
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    color: rgba(255, 255, 255, 0.78);
    white-space: nowrap;
}
.review-speed .seg { font-size: 12px; padding: 3px 8px; }
.review-exit { margin-left: auto; }
.review-view {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    font-size: 13px;
}
.review-view .opt-label { color: rgba(255, 255, 255, 0.62); }
.review-seats .seg { font-size: 12px; padding: 3px 10px; }
.review-note { font-size: 12px; color: rgba(255, 255, 255, 0.62); }
/* Share this moment and Play on from here (shell/review.ts). A group whose
   buttons are built once and only enabled or disabled, so it never changes
   height; the note beside them stays on one line. It sits at the right end of
   the View as row, and wraps under it as one group when the row is full. A
   game with one seat has no View as row, and the group is a centered row of
   its own. */
.review-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px;
}
.review-view > .review-actions { margin-left: auto; }
.review-action-note { font-size: 12px; color: rgba(255, 255, 255, 0.72); white-space: nowrap; }
.review-action-note:empty { display: none; }

/* The analysis strip (shell/review.ts renderAnalysis): progress, the
   standing at the cursor, the Expert's verdict on the move made, and the
   turning points as jump buttons. Reserves a few lines so the board does not
   move as the numbers arrive. */
.review-analysis {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-height: 3.2em;
    padding-top: 6px;
    border-top: 1px solid var(--panel-border);
    font-size: 13px;
}
.review-progress { color: rgba(255, 255, 255, 0.62); font-variant-numeric: tabular-nums; }
/* The position at the cursor (shell/review.ts positionBlock): the standing,
   then the move that reached it, the Expert's view of it, and Show Expert's
   line, as ONE run of text that wraps word by word. Every piece is inline
   (the button inline-block), so a long verdict starts on the standing's line
   and fills it before wrapping; review.ts joins the pieces with real spaces,
   which are the break opportunities. The row holds a button's height even on
   a step with no verdict or no button: 24.8px for a .btn.small, and 36px
   where the coarse-pointer rule in src/duelpoker/style.css makes it taller. */
.review-now {
    min-height: 25px;
    line-height: 1.8;
}
@media (pointer: coarse) {
    .review-now { min-height: 36px; }
}
.review-now > .review-standing, .review-verdict { display: inline; }
.review-now .review-expert-toggle { vertical-align: baseline; line-height: 1.2; }
/* The block around that row, the Expert's estimate, and the Expert's-line
   note: everything that changes per step. shell/review.ts holdPositionHeight
   keeps it at the tallest it has been in the review (an inline min-height),
   so a long verdict followed by a short one does not move the board. */
.review-pos {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.review-standing { color: rgba(255, 255, 255, 0.88); }
/* The dot between the standing and the verdict (a text span, review-sep). */
.review-sep { color: rgba(255, 255, 255, 0.45); }
.review-verdict-text { color: rgba(255, 255, 255, 0.88); }
.review-expert-note { color: var(--accent); }
.review-turning {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}
.review-turning .opt-label { color: rgba(255, 255, 255, 0.62); }
.review-turn.selected { outline: 2px solid var(--accent); outline-offset: -2px; }
/* A turning point reads "Blunder  You discard 7♦.  −12": its size, the move in
   the log's own wording (colored card labels included), and what it cost. The
   size word carries the same color as the graph's dot for it. */
.review-turn {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    max-width: 100%;
    text-align: left;
}
.review-turn-kind { font-weight: 700; white-space: nowrap; }
.review-turn-kind.blunder { color: #e06a66; }
.review-turn-kind.mistake, .review-turn-kind.inaccuracy { color: #e0a030; }
.review-turn-kind.strong { color: #6cc46c; }
.review-turn .review-step {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.review-turn-cost { font-variant-numeric: tabular-nums; white-space: nowrap; opacity: 0.8; }
/* The move a verdict judges, ahead of the Expert's alternative. */
.review-played { font-weight: 600; }
/* One line on how the viewed seat played: "You played the Expert's move 31
   of 38 times: 1 blunder, 2 inaccuracies." */
.review-summary { color: rgba(255, 255, 255, 0.88); }
/* What the Expert expected of the moves it weighed, under the verdict. The
   Expert's choice is bold and the move made is underlined, so the gap
   between them reads at a glance; the values line up as tabular figures. */
.review-estimate { color: rgba(255, 255, 255, 0.72); font-size: 12px; }
.review-estimate-head { margin-right: 6px; }
.review-estimate-row.chosen .review-estimate-move { font-weight: 700; color: rgba(255, 255, 255, 0.92); }
.review-estimate-row.actual .review-estimate-move { text-decoration: underline; text-underline-offset: 2px; }
.review-estimate-value { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The margin graph: a fixed-height sparkline so it never resizes the strip. */
.review-graph { height: 44px; }
.review-graph-svg { display: block; width: 100%; height: 44px; cursor: pointer; }
.review-graph-zero { stroke: rgba(255, 255, 255, 0.25); stroke-width: 1; }
.review-graph-line { fill: none; stroke: var(--accent); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.review-graph-cursor { stroke: rgba(255, 255, 255, 0.7); stroke-width: 1; vector-effect: non-scaling-stroke; }
.review-graph-dot { fill: #e0a030; stroke: #1b1b1b; stroke-width: 1; }
.review-graph-dot.blunder { fill: #d9534f; }
.review-graph-dot.strong { fill: #5cb85c; }
@media (max-width: 560px) {
    .review-line { padding: 6px 8px; }
    .review-counter { min-width: var(--review-counter-w, 0px); }
    .review-view .opt-label { display: none; }
    .review-analysis { min-height: 0; }
}

/* ---------- panel headers with inline controls ---------- */

/* A box title sits on the board's axis like everything else, so the head CENTERS
   what it holds. Roughly 70 pages emit one of these heads and another 18 build
   one in def.ts, so this is the whole sweep in one rule.
   Two kinds of thing sit beside a title and they are treated differently. A
   CAPTION (`4 cards gone for good`, `best five of 7`, a hand's combination) is
   part of the title and stays a flex item, so title and caption center as one
   unit. A CONTROL - always `.btn.small` here, the sort toggle - is not, and comes
   out of the flow to the right: left in it, it would push the title off-center by
   half the button's width, and `text-align` on the h3 cannot fix that, because a
   shrink-to-fit flex item is exactly as wide as its text.
   `min-height` reserves the button's own height, since an out-of-flow child no
   longer contributes any, and a head that shrank when the button went away would
   walk everything below it up the screen. */
.panel-head {
    position: relative;
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 10px;
    min-height: 24px;
}
.panel-head h3 { margin: 0 0 10px; }
.panel-head > .btn.small { position: absolute; right: 0; top: 0; }
.btn.small { padding: 3px 10px; font-size: 12px; }
/* Touch screens. The coarse-pointer block in src/duelpoker/style.css (site-wide,
   despite the file it lives in) gives every `.btn.small` and `#hand-sort` a 36px
   minimum height to make it easy to tap. In a `.panel-head` that is taller than
   the 31px the head reserves (its min-height plus the h3's bottom margin), and
   because the button is out of the flow nothing grows to make room: it hung 5px
   into the first row of cards, over the two cards at the right end of the hand
   (measured at 390px and 360px with a coarse pointer, every game with a sorted
   hand). So in a head the button keeps its desktop height, a little shorter and
   moved up 2px to sit centered on the title's line, and the tap target grows
   invisibly instead: the ::after box adds 6px above and below, which still ends
   inside the head and so never takes a tap meant for a card. The `#hand-sort`
   selector is there for specificity, since the coarse rule names the id. */
@media (pointer: coarse) {
    .panel-head > .btn.small,
    .panel-head > #hand-sort {
        min-height: 0;
        top: -2px;
        padding-top: 2px;
        padding-bottom: 2px;
    }
    .panel-head > .btn.small::after {
        content: '';
        position: absolute;
        inset: -6px -4px;
    }
}

/* ---------- exchange poker swap markers ----------
   A small glyph UNDER the card, in the flow, with its line reserved in every
   cell whether or not the card was swapped in. It used to be a 22px gold disc
   hanging over the card's corner: louder than the cards it labeled, and able to
   paint beneath a card that lifted on hover or selection, since a transform
   puts the card on a layer of its own. Below the card it can neither cover nor
   be covered, and the reserved line keeps every cell, and the row, one height. */
.swap-mark {
    display: block;
    height: 14px;
    line-height: 14px;
    font-size: 12px;
    font-weight: 700;
    text-align: center;
    color: var(--accent);
    opacity: 0.8;
    pointer-events: none;
}
.swap-legend { font-size: 12px; opacity: 0.75; white-space: nowrap; }
/* Laid out from the start and shown once a mark exists, so the panel head
   keeps one height. */
.swap-legend.swap-legend-off { visibility: hidden; }

/* ---------- online play (P2P lobby + in-game status) ---------- */

.online-form { margin: 14px 0 18px; }
.online-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
/* A visible label over each field, so "optional" survives a prefilled value
   (the lobby refills the last table name, and then no placeholder shows). */
.online-label { display: block; margin: 12px 0 4px; font-size: 13.5px; font-weight: 600; }
.online-optional { font-weight: 400; opacity: 0.7; }
.online-label + .online-row { margin-top: 0; }
/* Two players still at their table on the setup screen, between games
   (shell.ts renderSetupTable). Its lines reuse .p2p-undo from the in-game box. */
.online-table {
    margin-top: 10px;
    padding: 10px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.04);
    font-size: 14px;
}
.online-table[hidden] { display: none; }
.online-table-line { margin: 0; }
.online-table-line.warn { color: var(--accent); font-weight: 600; }
.online-table-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.textinput {
    font: inherit;
    color: var(--ink);
    background: rgba(0, 0, 0, 0.25);
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    padding: 7px 12px;
    min-width: 0;
}
.textinput::placeholder { color: rgba(242, 239, 233, 0.45); }
.textinput:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; }
.textinput:disabled { opacity: 0.4; }
#online-player-name { width: 180px; }
#online-table-name { flex: 1 1 160px; }
.online-status { margin-top: 10px; min-height: 1.4em; font-size: 14px; opacity: 0.9; }
.online-status.error { color: var(--red); opacity: 1; }
.online-note { font-size: 12.5px; opacity: 0.65; margin: 10px 0 0; }
/* Why a share link did not open (shell.ts, openShareFragment). Painted once,
   on the setup screen a failed link lands on, and never shown mid-game. */
.share-note { margin: 12px 0 0; font-size: 14px; color: var(--red); text-align: center; }

/* The online status box is shell-owned on every page, so these rules are
   legitimately global. Its line-height is stated rather than inherited
   because the reservations below count lines of it. */
.p2p-status {
    font-size: 13px;
    line-height: 1.4;
    opacity: 0.8;
    margin: 2px 0 8px;
}
.p2p-status.warn {
    color: var(--accent);
    opacity: 1;
    font-weight: 600;
}
/* "Play on against a bot" rides inside the status line, after the warning
   (R2.6), and must not make the box taller than the warning alone (R2.7). A
   plain .btn.small is 24.8px against the 18.2px line, so this one drops its
   vertical padding and holds 16px of text plus a 2px border, top-aligned so it
   cannot stretch the line box. */
.p2p-status .p2p-line .btn {
    padding-block: 0;
    line-height: 16px;
    vertical-align: top;
}
/* Where the warning plus the offer wraps, reserve the lines it takes with a
   20-character name (the online form's maximum), in EVERY state of the box: a
   Bye paints the warning and the offer in one repaint straight from the
   connected line, and a returning peer takes both away at once (R28.5).
   Measured in Chromium: 1 line from 1100px, 2 from 421px, 3 at 420px and
   below. */
@media (max-width: 1099px) {
    .p2p-status { min-height: calc(2 * 1.4em); }
}
@media (max-width: 420px) {
    .p2p-status { min-height: calc(3 * 1.4em); }
}
/* The online undo negotiation prompt (shell-owned, every online game). */
.p2p-undo {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-top: 6px;
    opacity: 1;
    font-weight: 600;
}
.p2p-undo-msg { margin-right: 2px; }

/* The peer's undo request, as a dialog over the board (src/shared/ui/peer-prompt.ts,
   rendered into the page's bare #peer-prompt anchor). The online status box
   above held it as one line, which was easy to miss while the whole table sat
   waiting on the answer.

   Z-INDEX 650: over the lifted round summary (500), under the report and
   reference dialogs (700). The flex centering and the fixed ::before scrim are
   the game picker's (.sw-modal): no transform, so the scrim sizes to the
   viewport, and the card is positioned so its own scrim cannot paint over it.
   Selected by class only; the anchor's id is on every page. */
.peer-prompt {
    position: fixed;
    inset: 0;
    z-index: 650;
    display: none;
    padding: 20px;
}
.peer-prompt.peer-prompt-open {
    display: flex;
    align-items: center;
    justify-content: center;
}
.peer-prompt[hidden] { display: none; }
.peer-prompt::before {
    content: '';
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
}
.peer-prompt-card {
    position: relative;
    max-width: 420px;
    text-align: center;
    padding: 24px 28px;
    background: linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
.peer-prompt-title { margin: 0 0 8px; font-size: 20px; }
.peer-prompt-message { margin: 0 0 18px; opacity: 0.9; }
.peer-prompt-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }

/* ---------- card movement animations ---------- */

.card-fly {
    position: fixed;
    margin: 0;
    z-index: 900;
    pointer-events: none;
    transition: transform 0.38s cubic-bezier(0.2, 0.7, 0.3, 1), opacity 0.42s ease;
    will-change: transform, opacity;
}
/* A move nobody clicked (an auto-played forced action) flies slower, so it can
   be followed. Must agree with SLOW_FLY_MS / SLOW_VANISH_MS in ui/animate.ts. */
.card-fly.slow {
    transition: transform 0.62s cubic-bezier(0.2, 0.7, 0.3, 1), opacity 0.68s ease;
}
/* A bot's chosen cards, lit where they sit before they move (the shell's
   aiTelegraph pass). Outline and glow only, never a transform: Ring Poker
   places its cards with transforms, and the flight ghost is cloned from this
   element, so a lift here would ride along and jump when the card lands. */
.card.ai-telegraph {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    animation: aitelegraph 0.7s ease-out;
    box-shadow: 0 0 0 4px rgba(255, 209, 102, 0.35), 0 4px 12px rgba(0, 0, 0, 0.45);
}
@keyframes aitelegraph {
    from { box-shadow: 0 0 0 0 rgba(255, 209, 102, 0.9), 0 4px 12px rgba(0, 0, 0, 0.45); }
    to { box-shadow: 0 0 0 4px rgba(255, 209, 102, 0.35), 0 4px 12px rgba(0, 0, 0, 0.45); }
}
.card-enter { animation: cardenter 0.35s ease; }
@keyframes cardenter {
    from { opacity: 0; transform: translateY(-8px) scale(0.9); }
    to { opacity: 1; transform: none; }
}
/* ---------- score changes ----------
   A score that goes up counts to its new value and swells while it climbs, so
   scoring is something you SEE rather than something you notice later. Applied
   by ui/scoreanim.ts, which also skips the whole thing under "Animations: Off". */
.score-bump {
    animation: scorebump 0.52s cubic-bezier(0.2, 0.8, 0.3, 1);
    color: var(--accent);
}
/* The digits-only nodes the animator counts up. Inline-block so the bump can
   scale them, and tabular figures so the width does not jitter while climbing. */
.dracula-aggnum, .aow-inflnum {
    display: inline-block;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
@keyframes scorebump {
    0% { transform: scale(1); }
    35% { transform: scale(1.42); }
    100% { transform: scale(1); }
}

/* body.motion-off = the player chose "Animations: Off" (shared/ui/motion.ts).
   The OS reduced-motion request plays no part. */
body.motion-off .card-fly,
body.motion-off .card-fly.slow { transition: none; }
body.motion-off .card-enter { animation: none; }
body.motion-off .card.ai-telegraph { animation: none; }
body.motion-off .score-bump { animation: none; color: inherit; }
/* The global stop under "Animations: Off": no element runs a CSS animation or
   transition, and every style change shows its end state at the next paint.
   Zeroed durations and delays rather than `animation: none`, because a few
   elements are only visible through a forwards fill: #boot-note sits at
   opacity 0 and is faded in by `bootnote-in ... forwards`, so `none` would hide
   it for good. A zero-length animation jumps straight to its fill, and one with
   no fill ends at once and leaves the base style. The per-rule static stand-ins
   above (a steady ring, `animation: none; opacity: 1`) still set
   animation-name, which this rule leaves alone, so they keep working. */
body.motion-off, body.motion-off *, body.motion-off *::before, body.motion-off *::after {
    animation-duration: 0s !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0s !important;
    transition-delay: 0s !important;
}

/* ---------- setup: mode buttons + segmented options ---------- */

.setup-options {
    display: flex;
    gap: 22px;
    flex-wrap: wrap;
    margin: 16px 0 6px;
}
.opt-group { display: flex; align-items: center; gap: 10px; }
.opt-label { font-size: 13px; opacity: 0.8; }
/* Inline explanation beside an option (e.g. why animations are off on 'System'). */
.opt-note { font-size: 12px; opacity: 0.7; max-width: 320px; }

/* ---------- the option help bubble ----------
   A circled question mark beside an option's caption, opening a sentence that
   says what the setting does. Authored by `optHelp` in pages/snippets.mjs, which
   carries the rationale for the element choice; what matters here is geometry.

   IT IS A BUTTON PLUS A NATIVE POPOVER, so the platform supplies the top layer,
   Escape, the tap-outside dismiss and the one-at-a-time behavior, and this file
   supplies no positioning at all beyond the size cap. Two things follow that are
   easy to miss when editing:

   NOTHING BELOW CAN MOVE, because a popover is in the top layer and out of flow
   entirely. That is the containers-must-not-move rule in
   .kiro/steering/ui-conventions.md applied to a form, and it is the platform's
   guarantee here rather than something these rules have to arrange.

   IT IS A SPEECH BALLOON, ANCHORED ABOVE ITS OWN MARK, via CSS anchor
   positioning: `position-anchor: auto` binds a popover to the `popovertarget`
   button that opened it with no per-instance names and no JS, and
   `position-area: top` puts it in the tile above, horizontally centered on the
   mark, which is what lets the tail point at it.

   THE HAND-POSITIONED VERSION IS WHAT THIS REPLACES, and why the fallbacks
   below matter. Positioning a panel against a mark that can sit anywhere in a
   wrapping row, by hand, put a 292px bubble at left: -190px on a 375px phone.
   `position-try-fallbacks` is the declarative answer: slide sideways first so the
   balloon stays above its mark, and only drop below if even that will not fit.

   EVERYTHING ANCHORED IS INSIDE @supports, so a browser without anchor
   positioning keeps the UA's own centered popover (`inset: 0; margin: auto`),
   which is where this control started and is still perfectly usable. The tail is
   in there too: a tail on a centered panel would point at nothing. Anchor
   positioning reached Baseline in January 2026 (Chrome 125, Safari 26,
   Firefox 147), so the fallback is for older browsers rather than for current
   ones.

   THE BACKGROUND IS OPAQUE AND LIGHT, which is why it uses --balloon rather
   than --panel. Every other panel on the site is `rgba(255,255,255,0.07)` over
   the felt, which is right for something that IS the page; a balloon sits in
   front of it, and a rules block reading through 12.5px of text makes it hard to
   read. A near-felt opaque green was tried first and measured only ~15 per
   channel off the felt, which left the 8px tail invisible in a screenshot, so it
   inverts instead. The tail must use the same token or the seam shows.

   THE MARK IS A FULL-SIZE TAP TARGET. 22px square at every width, the same
   height as a .segmented .seg (13px text plus 6px padding either side), so it
   sits on the caption's row without changing the group's height. Deliberately
   not shrunk on phones: it is the one control on this screen whose whole purpose
   is to be found by somebody who does not know the game. */
.opt-help {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    /* The containing block for the tail (see .opt-help::after). */
    position: relative;
    width: 22px;
    height: 22px;
    padding: 0;
    /* Pulls the mark back against the caption it belongs to, out of the
       .opt-group 10px gap, so it reads as part of the label rather than as a
       third item between the label and the control. */
    margin-left: -4px;
    border: 1px solid var(--panel-border);
    border-radius: 50%;
    background: transparent;
    font: inherit;
    font-size: 12px;
    font-weight: 700;
    line-height: 1;
    color: var(--ink);
    opacity: 0.65;
    cursor: pointer;
}
.opt-help:hover { opacity: 1; }
.opt-help-body {
    width: max-content;
    /* 260px rather than 360px: narrower is what keeps a balloon centered on its
       own mark inside a 320px phone without a fallback having to slide it. */
    max-width: min(260px, calc(100vw - 24px));
    padding: 10px 12px;
    background: var(--balloon);
    border: 1px solid var(--balloon-edge);
    border-radius: 10px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--balloon-ink);
}
/* NO ::backdrop. There was a dim here while the panel was centered in the
   viewport, because a centered panel has to read as the thing in front. A
   balloon attached to the control says that by where it is, and dimming the page
   behind a 22px question mark reads as a modal the player has to deal with. */

/* The settings table: one row per button in the group, labeled in the group's own
   words. A grid rather than the dl default, so the labels form a column the eye
   runs down and the descriptions align against it. */
.opt-help-rows {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 3px 10px;
    margin: 0;
}
.opt-help-rows dt { font-weight: 700; white-space: nowrap; }
.opt-help-rows dd { margin: 0; opacity: 0.78; }
/* The group-level line, above the table and set apart from it. */
.opt-help-lead { margin: 0 0 7px; }
.opt-help-lead:last-child { margin: 0; }

@supports (position-area: top) {
    .opt-help-body {
        position: absolute;
        /* The invoker that opened it: no names, no per-game CSS. */
        position-anchor: auto;
        position-area: top;
        /* 9px of clearance for the tail, and 12px off the viewport sides so a
           balloon that has to slide on a phone still does not touch the edge. */
        margin: 0 12px 9px;
        /* Sideways ONLY. There is deliberately no flip-below fallback: the tail
           lives on the mark and always points up at the balloon, so a balloon
           that moved underneath would be pointed at from the wrong side. Fitting
           above is guaranteed instead, by the max-height below. */
        position-try-fallbacks: top span-right, top span-left;
        /* 100% of the inset-modified containing block, which for `top` is the
           space between the viewport top and the mark. So there is always room
           above and the balloon scrolls rather than flipping or overflowing. */
        max-height: 100%;
        overflow-y: auto;
        overscroll-behavior: contain;
    }
    /*
     * THE TAIL IS ON THE MARK, NOT ON THE BALLOON, which is the one detail here
     * worth keeping. A tail on the balloon's bottom edge is centered on the
     * BALLOON, and the moment a fallback slides the balloon sideways to stay on
     * screen the two part company: measured at 390px, the balloon moved 38px and
     * the tail pointed at nothing. The mark cannot drift relative to itself.
     *
     * Shown only while this group's balloon is open. The button and its popover
     * are siblings inside .opt-group, which is what makes the :has() reach work.
     * TWO TRIANGLES, for the same reason the balloon has a border: the fill is
     * only ~15 per channel lighter than the felt it sits on, so a bare triangle
     * in it is invisible (measured: it did not read at all in a screenshot). The
     * outer triangle is the balloon's border color and sits 1px lower, so a 1px
     * rim shows along the two slanted edges and the tail has an edge to be seen
     * by. The fill's top edge meets the balloon's bottom edge exactly, because
     * the balloon's 9px bottom margin is this triangle's height.
     */
    .opt-help::before,
    .opt-help::after {
        content: '';
        position: absolute;
        left: 50%;
        width: 0;
        height: 0;
        border: 9px solid transparent;
        border-bottom: 0;
        translate: -50% 0;
        opacity: 0;
    }
    .opt-help::before {
        bottom: 100%;
        border-top-color: var(--balloon-edge);
    }
    .opt-help::after {
        bottom: calc(100% + 1px);
        border-width: 8px;
        border-bottom: 0;
        border-top-color: var(--balloon);
    }
    .opt-group:has(> .opt-help-body:popover-open) > .opt-help::before,
    .opt-group:has(> .opt-help-body:popover-open) > .opt-help::after { opacity: 1; }
}
.segmented {
    display: inline-flex;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    overflow: hidden;
}
.segmented .seg {
    font: inherit;
    font-size: 13px;
    color: var(--ink);
    background: transparent;
    border: 0;
    padding: 6px 12px;
    cursor: pointer;
}
.segmented .seg + .seg { border-left: 1px solid var(--panel-border); }
.segmented .seg.selected {
    background: var(--accent);
    color: var(--accent-ink);
    font-weight: 700;
}

/* ---------- setup: the launch row ----------
   ONE ROW between the intro prose and the secondary row, holding TWO CONTROLS:
   #setup-play, a divider, #opt-mode. Pressing Play and choosing how you play are
   one decision taken in one place, so they share a line, Play leads because it is
   the control the player came for, and the divider is what says those two are a
   single group with a boundary in it. There is no caption: the radiogroup's
   aria-label is its accessible name, and the mode labels say what the control is.
   The visible selected state is `.segmented .seg.selected` directly above,
   unchanged, so all that is left here is the row, the divider and the phone.

   #setup-play-tutorial IS NOT IN THIS ROW. It was for one round, and it is back in
   #setup-secondary beside Resume, which is why `.setup-launch-tut` and its phone
   override are gone from this file. Two controls is what lets this row stay ONE
   row down to a phone: that button is 103.55px on its short label and 122.09px on
   its finished one, plus 8px of separation, and paying for it is what forced the
   stack at 560px. Every ordinary phone is under 560px, so the row the player
   actually saw was a column, which is the report this band answers.

   Every selector below names .setup-launch, .setup-launch-rule or .setup-modes,
   the three classes this row owns, and none names #opt-mode or #setup-play. Those
   ids are on all ~90 pages, so a bare id selector here would be exactly the leak
   .kiro/steering/ui-conventions.md records eight of.

   NOTHING IN THIS ROW MOVES WITH THE SELECTION, and the markup needed help for
   that to be true. .setup-actions wraps, and .segmented is a shrinkable flex
   item, so at a narrow width the segments were squeezed and their labels
   wrapped, and the selected label is bolder and therefore wider: measured on
   Grid Poker, the group is 239px with Online selected and 243px with Pass and
   play selected. Those 4px flipped the wrap at four widths per page, and the
   group went from 32px tall to 50px and took #setup-play down 19px with it,
   which is a panel moving when the stored mode is applied. Three rules hold the
   height to a function of the viewport width alone: `nowrap` on the row, so the
   wrap decision is made at the one fixed breakpoint below and never by content;
   `nowrap` on each mode label, so no label can reflow inside a segment; and
   `nowrap` on the tutorial button, whose label is the one thing in this row that
   changes between visits ('Tutorial' becomes 'Tutorial \u2713' once the
   walkthrough is finished, in renderSetupExtras). That third one is what keeps
   the change a WIDTH change: measured on Fox in the Forest at 16 widths from 320
   to 1920, the button is 49.8px tall on both labels and the row's height and
   #setup-play's, #opt-mode's and #setup-secondary's offsetTop are all unchanged
   either side of the relabel. What moves is 18.54px of width, 103.55 to 122.09.
   .segmented hides its overflow, so a label with no room is clipped rather than
   reflowed, and the height stays fixed either way.

   LEFT-ALIGNED, NOT CENTERED. Every other row in this card is left-aligned (the
   secondary row, the option groups, the stats line), and the centering rule in
   .kiro/steering/ui-conventions.md is about the board axis and hand rows, not a
   form card. `align-items: center` below is the CROSS axis and says nothing
   about the main one: it holds the 31px selector vertically centered against
   the 46px Play button, instead of letting .setup-actions' default stretch pull
   the segments up to the button's height.

   THAT IS ALSO WHY "THEY SHARE A LINE" IS NOT AN offsetTop EQUALITY. Centered on
   the cross axis, a 31px selector beside a 50px button legitimately sits about 9px
   lower, so the two tops differ in a perfectly good row. The reading that answers
   the question is whether the two boxes OVERLAP on the block axis, which a row
   always does and a column never does, and that is what group 1c of
   tools/measure-setup.mjs grades from 320 to 1920.

   A game offering one mode ships #opt-mode hidden and inert, and the [hidden]
   rule near the top of this file carries !important, so it collapses to nothing
   rather than staying laid out as a flex row. THE DIVIDER IS NOT HIDDEN HERE ANY
   MORE: it would hang beside Play with nothing to divide, but there is no rule
   below that takes it away, because the generator emits no divider at all on
   those pages (modeSelector in tools/gen-pages.mjs). This used to be
   `.setup-modes[hidden] + .setup-launch-rule { display: none }`, which stopped
   reaching once Play moved in front and the divider became the selector's
   PREVIOUS sibling, and `:has()` is not the repair: #opt-mode's `hidden` is a
   build-time fact the generator already knows and the shell never writes, so an
   element that only exists when there is something to divide is simpler than one
   that is hidden. */
.setup-launch {
    /* 18px above and below is .setup-actions' own margin, unchanged. The intro
       ends in a <p> on most pages, so its 16px bottom margin collapses into
       that, and in a .online-note with no bottom margin on a page written to
       end with one, so the gap above this row is the same whatever the prose
       above happens to end with. */
    flex-wrap: nowrap;
    align-items: center;
}
.setup-modes .seg { white-space: nowrap; }
/* The divider. Stretched by the row rather than given a size of its own, so it
   can never be the thing that sets the row's height, and `align-self` covers
   both orientations because stretch is along the CROSS axis: a 1px width makes
   it vertical in the row here, a 1px height makes it horizontal in the stacked
   layout below. The 4px either side is on top of the row's own 12px gap, so the
   boundary reads as a boundary rather than as more of the same rhythm.
   0.35 alpha rather than --panel-border's 0.14, which measures 1.45 to 1
   against this card and would be a hairline nobody can see. It is decoration,
   and aria-hidden in the markup for that reason.

   IT IS THE FIRST THING THE PHONE BAND SPENDS, and it is by far the cheapest:
   1px of hairline, 4px of margin either side, and the whole 12px row gap it stops
   being, so 21px of the 24px the tightening band needs. It says Play and the
   selector are one group with a boundary in it, and a clipped mode label says
   nothing at all, so a phone gets the group without the boundary. */
.setup-launch-rule {
    flex: 0 0 auto;
    align-self: stretch;
    width: 1px;
    margin: 2px 4px;
    background: rgba(255, 255, 255, 0.35);
}
/* THREE BANDS BELOW, AND EACH WIDTH IS A MEASUREMENT RATHER THAN A ROUND NUMBER
   SOMEBODY LIKED. The row is `nowrap` and .segmented is the only item in it that
   can shrink (its overflow: hidden puts its automatic minimum size at 0), so
   every pixel the row cannot afford is taken out of the selector, and .segmented
   CLIPS what it cannot fit rather than reflowing it. So "it still fits" means no
   mode label is cut, not merely that nothing wrapped.

   Measured in chromium on the widest page there is, a three-mode 'vs bots' page
   (american-bookshop), sweeping every width from 320 to 420 and asking at each one
   whether #setup-play and #opt-mode overlap on the block axis AND no segment is
   clipped. The lowest width holding one clean line, per treatment:

     divider, 12px gap, 8px/10px segments   406     what shipped before this band
     divider dropped                        385     21px, and the cheapest of the four
     gap 12px to 8px                        381     4px
     segments 8px/10px to 8px/8px           369     12px
     segments 8px/8px to 8px/6px            357     12px more
     labels 13px to 12px                    344     13px, NOT TAKEN

   THE LAST LINE IS DECLINED ON PURPOSE. A 12px control label on the smallest
   screen there is buys one more phone width and costs legibility where legibility
   is already tightest, so 13px stays and the row stacks a little sooner.

   THE FONT IS WHY THE BREAKPOINTS CARRY HEADROOM. The body stack is "Avenir Next",
   "Segoe UI", system-ui, sans-serif, so the label widths are the platform's, not
   this repo's: the same selector at 8px/6px measures 203.98px in Avenir Next here
   and 208.38px under system-ui, which moves the figure above from 357 to 362. Every
   band below is set above the widest of those, so a platform whose system font is
   wider than the one measured still fits. It also has to be: MODE_LABEL_CAP in
   tools/gen-pages.mjs allows a 20-character bots label against the 7 of 'vs bots',
   so today's widest page is the widest page today and not the widest possible one.
   A band with 6px of slack survives a font change; one set at the measured edge
   does not.

   Plain arithmetic for reading the figures back: a phone card leaves the viewport
   less 64px (12px margin and 20px padding either side) less its 1px borders, so
   254px at 320px and 311px at 375px, and the row needs Play's 79px plus the gap
   plus the selector inside that. */
@media (max-width: 560px) {
    /* The card's own phone treatment starts here (.setup-card above), and so does
       a taller tap target on the segments while the width is still going spare:
       227.98px of segments inside the 495px the card leaves at 560px. The row
       needs nothing else yet, and keeps its divider and its 12px gap, because the
       untightened row holds one clean line down to 406px here and 411px under the
       widest system font measured. */
    .setup-modes .seg { padding: 8px 10px; }
}
@media (max-width: 420px) {
    /* THE TIGHTENING BAND: still one row, but paid for. 420px rather than 410px
       leaves the untightened row 10px of slack above the 411px it needs under the
       widest system font, so no width in either band is ever squeezed.

       Three savings, cheapest first, together worth 49px: the divider goes (21px,
       its hairline plus 4px either side plus the 12px row gap it stops being), the
       gap goes to 8px (4px), and the segments go to 8px/6px (24px, 4px off each
       side of three segments). The vertical padding is untouched, so the tap
       target keeps the height the band above gave it.

       WHAT THAT BUYS: one clean line at 375px and at 390px, which is where most
       phones sit, with 13px and 28px of slack against the widest system font
       measured. 360px does not make it, and would want either 8px/5px segments or
       the 12px label above; both were declined. */
    .setup-launch { gap: 8px; }
    .setup-launch-rule { display: none; }
    .setup-modes .seg { padding: 8px 6px; }
}
@media (max-width: 368px) {
    /* THE STACK, and it is its own layout rather than a squeezed row. At 320px the
       segments alone measure 203.98px in the 254px the card leaves, so 79px of
       Play cannot join them whatever else is given up. Both children stack in the
       row's own order, Play then the selector, at this FIXED width rather than
       wherever the content happens to give out: the selected mode label is bolder
       and so about 4px wider, and a content-driven wrap flipped on those 4px at
       four widths per page and took #setup-play down 19px with it.

       368px rather than 356px leaves the tightened row 6px of slack above the
       362px it needs under the widest system font measured.

       THE CONNECTOR IS ALREADY GONE, dropped by the band above rather than turned
       horizontal here. It said Play and the selector are one group with a boundary
       in it, and in a two-item column 8px of gap says the same thing without
       spending the width; the four-item column it used to hold together (Play, the
       connector, the selector, Tutorial) no longer exists. */
    .setup-launch { flex-direction: column; align-items: flex-start; }
}

/* ---------- setup: the secondary row and the quiet button ----------
   #setup-secondary sits under the launch row and holds TWO controls, #setup-resume
   then #setup-play-tutorial. Neither reads a single setup option, which is the line
   the two rows are drawn on: Resume reopens a position that was already dealt and
   Tutorial opens a scripted walkthrough, so the mode selector qualifies neither,
   while it is the whole reason Play sits beside it (secondaryRow and modeSelector
   in tools/gen-pages.mjs). Keeping the launch row down to two controls is also what
   lets it stay one row on a phone, which is the launch row's note above.

   NEITHER IS A FIXED WIDTH and both change label: Resume takes one of three forms
   and the shell swaps setup-quiet for primary on two of them, and Tutorial gains a
   check mark once the walkthrough is finished. This row WRAPS, which .setup-actions
   already gives it, and it sits below the launch row and above everything whose
   height already changes at runtime, so a second line here pushes nothing that was
   holding still. At 320px the two together do not fit on one line and it takes
   that second line; nothing above it moves when they do.

   Tutorial carries NO class of its own here. It had `setup-launch-tut` in the launch
   row for 8px of inline margin off the selector, and in a row of two quiet buttons
   there is nothing to set it apart from, so .setup-actions' 12px gap is the whole
   separation. The two selectors below name .setup-secondary and .setup-quiet, the
   two classes this feature owns, and neither names a shared element id, so no rule
   can reach another page's controls.

   THE ROW IS EMPTY MORE OFTEN THAN NOT. Both buttons ship hidden, Resume unhiding
   only when a save is waiting and Tutorial only on a game with a walkthrough, so a
   first visit to a game without one shows an empty row.

   Its HEIGHT was already 0 when empty, because gap is only paid BETWEEN visible
   items and [hidden] carries display: none !important. What it still paid was
   .setup-actions' `margin: 18px 0`, and worse, being a flex container it is a
   block formatting context, so it also stopped the launch row's 18px bottom
   margin collapsing with the stats line's 2px top margin. Measured on Aces at
   every width from 320 to 1920: 36px between the Play button and the stats line
   with the row empty, against 18px with the row taken out of the document.

   So an empty row stops being a flex container. `display: block` with no margin
   is self-collapsing, which is exactly the shape wanted: the two neighboring
   margins collapse with each other as if the row were not there (measured 18px,
   equal to the row removed), while the row keeps a real position in the flow, so
   its own offsetTop reads the same whether it is empty or full (measured equal
   at all 13 widths). That last part is why this is not `display: none`, which
   would report an offsetTop of 0 and break the measurement harness's reading of
   this element.

   The populated row needs no rule at all: .setup-actions already gives it the
   flex row, the 12px gap and the 18px margins. `:has()` is live, so the shell
   unhiding Resume restores all of that with no class to toggle, which matters
   because the shell hides and unhides that button on every repaint. It is
   written `> :not([hidden])` rather than naming a button, so it counts whatever
   the row holds, which is why it has needed no edit across two children, one, and
   two again, and why EITHER button showing on its own is enough to bring the row
   back.
   A browser without :has() drops this rule and gets the row exactly as it is
   today, which is 18px of margin it has not earned rather than a broken card. */
.setup-secondary:not(:has(> :not([hidden]))) { display: block; margin: 0; }

/* The quiet style is .btn's own look, and that is the point: the Play button
   wears .primary's accent fill, so a plain .btn beside it already reads as the
   lesser action and needs nothing added to say so. Two things are added.

   A BORDER OF ITS OWN, because --panel-border at 0.14 alpha measures 1.45 to 1
   against the setup card over --felt-dark and 1.39 to 1 over --felt, and the
   boundary floor is 3.0. White at 0.6 measures 4.22 and 3.53 against those two
   surfaces, so it clears the floor at both ends of the body's radial gradient;
   the floor is crossed around 0.45 alpha over the dark end and 0.5 over the
   light end, which is why this is not tuned finer. The label needs nothing:
   --ink on .btn's own --panel background measures 6.12 to 1 over --felt-dark
   and 4.91 over --felt, against a 4.5 floor.

   Single-class specificity throughout, deliberately. The shell swaps setup-quiet
   for primary on #setup-resume as its label changes, and .btn.primary at 0-2-0
   beats .setup-quiet at 0-1-0, so the accent fill and its transparent border win
   even on an element that somehow carried both. Nothing here sets box-shadow
   either, which is what .btn.tutnudge animates: at 0-2-0 it would outrank this
   rule anyway, but a shadow declared here would still be the value the animation
   returns to. The "Animations: Off" answer for that flash already exists further up
   this file, and it is a steady ring rather than nothing.

   min-height and min-width put a 44px floor under the pointer target. Neither
   binds today, because .btn.big renders 50px tall at every width from 320 to
   1920 and the shortest of these labels is wider than 44px, but the floor is
   what makes the target size a property of the style rather than of the label.
   inline-flex centering is what keeps the label centered if it ever does bind:
   a button centers its content by default, and this says so rather than relying
   on it. It also matches .toolbar .btn, which centers its icon the same way. */
.setup-quiet {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
    border-color: rgba(255, 255, 255, 0.6);
}

/* ---------- the game picker (#switch-modal) ----------
   Two online peers change game together, and the picker is the list they choose
   from. It renders into the page's bare #switch-modal anchor, which ships empty
   and hidden; src/shared/ui/game-picker.ts clears it, sets its class names and
   builds every node with el(), so the page carries no markup this sheet or that
   module would have to keep matching.

   EVERY SELECTOR BELOW NAMES A .sw- CLASS AND NONE NAMES AN ELEMENT ID. The
   anchor's id is on every page, so a bare id selector here would reach the
   picker on all of them, which is the leak .kiro/steering/ui-conventions.md
   records eight of. Nothing here can reach #board, #hand-panel, #hand-cards,
   #result-panel or #status-line, on this page or on any other.

   The class vocabulary, all of it owned by this feature:
     .sw-modal   the anchor while the picker is mounted, .sw-open while it shows
     .sw-card    the dialog box, a flex column
     .sw-head    the title and the Close control
     .sw-filter  the filter field's row
     .sw-list    the scrolling list box, and the one element with a height cap
     .sw-entry   one game, holding .sw-icon and .sw-name
     .sw-empty   the line shown while the filter matches nothing
     .sw-focus   a mirror of the focus cursor, and DELIBERATELY UNSTYLED. The
                 test fakes record `focused` on an element and never clear it, so
                 the class is the only surface a test can assert exactly one of.
                 Keyboard focus is already painted by `button:focus-visible` and
                 `.textinput:focus-visible` further up this file, and a second
                 ring declared here would double up on a real browser.

   Z-INDEX 500, under the pass-and-play handoff gate's 600. The gate covers
   everything private during a handoff, so it outranks every other layer on this
   sheet and the picker is no exception.

   NO TRANSFORM, AND THE BACKDROP FROM A FIXED ::before. Those two go together: a
   transform on this element would make it the containing block for every
   position: fixed descendant, so the backdrop pseudo-element would then size to
   the dialog rather than to the viewport. Flex centering wants no transform, so
   there is none, and `position: fixed` inside still means the viewport.

   .sw-card MUST BE POSITIONED, and that is the one thing the ::before backdrop
   costs. A fixed pseudo-element is a positioned box, and positioned boxes paint
   after in-flow ones; ::before precedes the card in tree order, so an
   unpositioned card would be painted over by its own backdrop. position:
   relative puts the two in the same paint step, where tree order puts the card
   on top.

   THE PAGE BEHIND KEEPS THE SCROLL OFFSET IT HELD, and nothing here locks it.
   The overlay is position: fixed, so it adds no height to the document and moves
   no in-flow box, which is also what keeps the top offsets of the board and the
   hand panel at the values they held. The list scrolls inside .sw-list, and
   overscroll-behavior: contain stops that scroll chaining out to the page once
   the list reaches an end. Setting overflow: hidden on the body was the other
   way and is worse: it is the one thing that would change the very offset this
   is meant to preserve.

   THE 60vh CAP IS ON .sw-list ALONE. About 70 games qualify for online play, so
   the list always overflows and the box always measures its cap. The card is
   capped as well, at the viewport less the overlay's own padding, and .sw-list is
   a shrinkable flex item inside it, so on a viewport too short for 60vh plus the
   card's chrome the list shrinks BELOW the cap rather than pushing the card off
   the screen. That is what makes 60 percent a ceiling by construction instead of
   a number to keep in step with the chrome above it.

   Measured in WebKit with 70 entries, which is the ordinary case: 480px of an
   800px viewport at 1280 wide, 648px of 1080px at 1920, and 351px of 585px at
   1024, every one of them 60.00 percent, over a card chrome of 126.4px. The card
   cap binds only in landscape, and there the list takes LESS than the ceiling:
   223.6px of 390px, or 57.34 percent, at 844 by 390. With the page scrolled to
   200 first, #board and #hand-panel keep their offsetTop and window.scrollY is
   unchanged across the open at all six sizes, body overflow stays visible, and
   scrolling the list to its end leaves scrollY where it was. The paint order is
   measured too: with .sw-card positioned, the topmost box at the middle of the
   card is the card's own content, and with position: static it is the backdrop. */
.sw-modal {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: none;
    padding: 20px;
}
.sw-modal.sw-open {
    display: flex;
    align-items: center;
    justify-content: center;
}
.sw-modal::before {
    content: '';
    position: fixed;
    inset: 0;
    /* Translucent, unlike the handoff gate's opaque cover: a finished game has
       nothing left to hide, and seeing the board through it says which game is
       being left. */
    background: rgba(0, 0, 0, 0.55);
}
.sw-card {
    position: relative;
    display: flex;
    flex-direction: column;
    width: min(520px, 100%);
    max-height: calc(100vh - 40px);
    padding: 18px 20px;
    background: var(--felt-dark);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
    box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
}
.sw-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 12px;
}
.sw-head h2 { margin: 0; font-size: 20px; }
.sw-filter { display: flex; margin-bottom: 10px; }
.sw-filter .textinput { flex: 1 1 auto; }
.sw-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    /* min-height: 0 is what lets the shrink actually happen: a flex item's
       automatic minimum size is its content, which here is 70 rows. */
    flex: 1 1 auto;
    min-height: 0;
    max-height: 60vh;
    overflow-y: auto;
    overscroll-behavior: contain;
}
/* One row per game. The icon box is a fixed size for every entry, so an icon
   that fails to load and has its src cleared leaves the row exactly as tall as
   its neighbors, and the row stays selectable with nothing broken shown.

   A BORDER OF ITS OWN, for the reason the quiet button above has one:
   --panel-border at 0.14 alpha measures 1.28 to 1 against this card and the
   boundary floor is 3.0. White at 0.45 measures 3.17 to 1, and it is lower than
   the quiet button's 0.6 because that button sits on the setup card over the
   body's radial gradient and had to clear the floor at both ends of it, where
   this card's --felt-dark is opaque and so is one surface. */
.sw-entry {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: 44px;
    padding: 6px 10px;
    font: inherit;
    text-align: left;
    color: var(--ink);
    background: rgba(0, 0, 0, 0.18);
    border: 1px solid rgba(255, 255, 255, 0.45);
    border-radius: 8px;
    cursor: pointer;
}
.sw-entry:hover { border-color: var(--accent); }
.sw-icon {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
}
.sw-icon img { display: block; width: 100%; height: 100%; }
.sw-name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.sw-empty { margin: 0; padding: 12px 2px; font-size: 14px; opacity: 0.75; }
@media (max-width: 560px) {
    /* THE 60vh CAP IS RE-DERIVED HERE, not assumed to carry over. The overlay
       gives back half its padding and the card 4px of its own, and a shorter
       title takes the head down with it, which together is what keeps the cap
       the binding constraint on a phone rather than the card's own height.
       Measured in WebKit with 70 entries: 340.8px of a 568px viewport at 320
       wide and 506.4px of 844px at 390 wide, both 60.00 percent, over a card
       chrome of 115.6px. */
    .sw-modal { padding: 10px; }
    .sw-card { max-height: calc(100vh - 20px); padding: 14px; }
    .sw-head h2 { font-size: 18px; }
}

.setup-othergames {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
    margin: 20px 0 4px;
    padding-top: 14px;
    border-top: 1px solid var(--panel-border);
}
.setup-othergames .othergames-label { font-size: 13px; opacity: 0.75; }
.setup-othergames a {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--ink);
    text-decoration: none;
    padding: 6px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.18);
}
.setup-othergames a:hover { border-color: var(--accent); }
.setup-othergames img { width: 26px; height: 26px; }
/* Any link wearing the button costume drops the underline (the hub's About and
   Contact, alongside "All games"). */
.toolbar a.btn { text-decoration: none; }

/* Attribution: designer, year published, and links out to BGG and to the rules
   as published elsewhere (tools/gen-pages.mjs gameFacts). It sits at the foot of
   the setup card, above "More games", and is quiet on purpose - a credit, not a
   call to action. Any part the catalog does not know is simply not emitted, so
   this row is often one item long and sometimes absent. */
.gamefacts {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 14px;
    margin: 18px 0 0;
    font-size: 13px;
}
.gamefact { color: rgba(255, 255, 255, 0.6); }
.gamefacts .factlink { color: rgba(255, 255, 255, 0.72); text-decoration: underline; }
.gamefacts .factlink:hover { color: var(--accent); }

/* ---------- site pages (About, Contact) and the site footer ---------- */

.pagedoc h3 {
    margin: 26px 0 6px;
    font-size: 16px;
    letter-spacing: 0.3px;
}
.pagedoc p, .pagedoc li { line-height: 1.55; }
.pagedoc ul { margin: 6px 0; padding-left: 22px; }
.pagedoc li { margin: 5px 0; }
.pagedoc a { color: var(--accent); }
/* A game's Rules_Page (rulesPage in tools/gen-pages.mjs): the hub tile's four
   fact chips under the heading, and the button back to the game page. The
   chips keep the tile's own classes, so only the row is styled here. A link in
   the button costume drops the underline, as the toolbar's do. */
.rulesfacts { display: flex; flex-wrap: wrap; gap: 5px; margin: 0 0 6px; }
.rulesplay { margin: 22px 0 0; }
.rulesplay a.btn { display: inline-block; text-decoration: none; }

/* One field per row, label above input: a contact form is read top to bottom
   and has no reason to compete for horizontal space. The fields stay a
   left-aligned column; the Send button and the result line under it sit on the
   page axis (ui-conventions.md "Center, do not left-align"). */
.contactform { display: flex; flex-direction: column; gap: 14px; margin: 20px 0 4px; }
.contactfield { display: flex; flex-direction: column; gap: 5px; font-size: 14px; }
.contactfield .textinput { width: 100%; }
.contactfield textarea.textinput { resize: vertical; min-height: 120px; }
.fieldnote { font-size: 12.5px; color: rgba(255, 255, 255, 0.55); }
/* "(optional)" inside a Contact_Form label. Only Message is required, and the
   label says so for the others rather than leaving it to be guessed; being in
   the label, it is read out with the field's name. The online form's own
   `.online-optional` is the same idea at its bolder label weight. */
.fieldoptional { color: rgba(255, 255, 255, 0.55); }
.contactform .btn { align-self: center; }
.contactline { margin: 20px 0 0; }
/* The Contact_Form's result (contactPage in tools/gen-pages.mjs). The script's
   status line and the two no-JS notices share ONE grid cell, so whichever is
   showing takes the same place under the Send button and nothing above it
   moves. The status line reserves its one line even while empty. A notice
   shows only while it is the URL's :target, which is where the
   Feedback_Function's redirect points a plain form post; [hidden] lets the
   script retire a notice left targeted by an earlier visit. */
.contactresult { display: grid; }
.contactresult > * { grid-area: 1 / 1; margin: 0; font-size: 14px; text-align: center; }
.contactstatus { min-height: 1.55em; }
.contactstatus.error, .contactnotice.error { color: var(--red); }
.contactnotice { display: none; }
.contactnotice:target:not([hidden]) { display: block; }
/* The two Report_Buttons (gamePage, HANDOFF_AND_LOG in tools/gen-pages.mjs):
   static markup on every game page, on the page axis (ui-conventions.md
   "Center, do not left-align"), so neither moves anything when it renders.
   The setup copy sits between the game facts line (and the unofficial note
   under it, where there is one) and More games; the log
   copy is the last item in #log-panel. */
.btn.report-open {
    display: flex; align-items: center; gap: 6px;
    width: fit-content; margin: 10px auto 0;
}
.report-flag { flex: none; }
/* The Report_Dialog (src/shared/ui/report.ts). A fixed overlay over the whole
   page: at 700 it sits above the result overlay (500) and the handoff gate
   (600), below the drag ghost and flying cards. Being fixed, opening it and
   any status line in it move nothing on the board (R28.5). The ::before scrim
   dims and catches clicks outside the box, the way .result-overlay's does.
   The status line reserves its line, so a result appearing does not move the
   buttons above it. */
.panel.report-dialog {
    position: fixed;
    inset: 0;
    margin: auto;
    width: min(92vw, 480px);
    height: fit-content;
    max-height: min(86vh, 620px);
    overflow: auto;
    z-index: 700;
    display: flex;
    flex-direction: column;
    gap: 12px;
    background: linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
.panel.report-dialog[hidden] { display: none; }
.panel.report-dialog::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -1;
    background: rgba(0, 0, 0, 0.5);
}
.report-dialog h3 { margin: 0; }
.report-note { margin: 0; font-size: 13.5px; }
.report-field { display: flex; flex-direction: column; gap: 5px; font-size: 14px; }
.report-field .textinput { width: 100%; resize: vertical; min-height: 110px; }
.report-attach { font-size: 14px; }
.report-attach input { margin-right: 6px; }
.report-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }
.report-status { min-height: 1.55em; margin: 0; font-size: 14px; text-align: center; }
.report-status.error { color: var(--red); }
/* On a phone the on-screen keyboard covers the bottom of the screen without
   shrinking the layout viewport (iOS Safari, and Chrome on Android by
   default), so a box centered in it puts Send and Close under the keyboard.
   Here the box sits at the top of what is VISIBLE instead: report.ts writes
   the visual viewport's height and offset into --report-vvh and --report-vvtop
   while the dialog is open. Without them (no visualViewport) the box sits at
   the top of the layout viewport. `top`, never a transform: a transform would
   make the box the containing block of its fixed ::before scrim. */
@media (max-width: 560px), (max-height: 560px) {
    .panel.report-dialog {
        inset: calc(var(--report-vvtop, 0px) + max(12px, env(safe-area-inset-top))) 0 auto 0;
        margin: 0 auto;
        max-height: calc(var(--report-vvh, 100dvh) - 24px);
    }
    .report-field .textinput { min-height: 88px; }
}
/* The Reference_Dialog (src/shared/ui/reference.ts): a game's hand rankings or
   meld types over the page, opened from #btn-reference in the toolbar. The
   Report_Dialog's geometry, scrim, and layer, wider for a list with example
   cards, and taller. The box itself does not scroll: the body does, so the
   title and Close stay in view however long the list is. */
.panel.reference-dialog {
    position: fixed;
    inset: 0;
    margin: auto;
    width: min(94vw, 640px);
    height: fit-content;
    max-height: 88vh;
    z-index: 700;
    display: flex;
    flex-direction: column;
    gap: 8px;
    background: linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
.panel.reference-dialog[hidden] { display: none; }
.panel.reference-dialog::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -1;
    background: rgba(0, 0, 0, 0.5);
}
/* The title centered on the box's axis, Close out of the flow at the right
   (the .panel-head arrangement, ui-conventions "Center, do not left-align"). */
.reference-head {
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 32px;
    flex: none;
}
.reference-head h3 { margin: 0; font-size: 17px; }
.reference-head .btn.small { position: absolute; right: 0; }
.reference-body {
    min-height: 0;
    overflow: auto;
    overscroll-behavior: contain;
    padding: 0 2px 4px;
    font-size: 14px;
    line-height: 1.5;
}
.reference-body:focus { outline: none; }
.reference-body:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
.reference-body > :first-child { margin-top: 0; }
/* Several sections (referenceDialog, .reference-multi): a desktop has a toolbar
   control per section, so the tablist is a phone affordance only. On a phone
   the toolbar keeps the first control alone (.refextra hidden, see below) and
   the tabs reach the rest, centered on the box's axis. */
.reference-tabs { display: none; }
@media (max-width: 560px) {
    .panel.reference-dialog { width: calc(100vw - 16px); max-height: calc(100vh - 16px); }
    .toolbar .btn.refextra { display: none; }
    .reference-multi .reference-tabs {
        display: flex;
        flex: none;
        justify-content: center;
        gap: 6px;
    }
    .reference-tab[aria-selected="true"] {
        background: var(--accent);
        color: var(--accent-ink);
        border-color: var(--accent);
    }
}
/* The ability badge (src/shared/ui/ability-badge.ts): a power card's text
   mark in its top-right corner, the corner the sprite art leaves clear. Sized
   from the card's own --cw, floored so a phone hand stays readable and capped
   so a large card is not all badge. A fixed box with flex centering, because
   arrow and star glyphs carry very different metrics. .ability-mark is the
   same mark inline in a Card abilities section, so the rules and the table
   read alike. */
.card.has-ability:not(.card-fly) { position: relative; }
.ability-badge {
    position: absolute;
    top: 2px;
    right: 2px;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: clamp(14px, calc(var(--cw) * 0.26), 22px);
    height: clamp(14px, calc(var(--cw) * 0.26), 22px);
    border-radius: 6px;
    border: 1px solid rgba(0, 0, 0, 0.45);
    background: var(--accent);
    color: #3a2c00;
    font-size: clamp(10px, calc(var(--cw) * 0.19), 15px);
    font-weight: 800;
    line-height: 1;
    pointer-events: none;
}
.ability-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.5em;
    height: 1.5em;
    margin-right: 5px;
    padding: 0 2px;
    border-radius: 6px;
    border: 1px solid rgba(0, 0, 0, 0.45);
    background: var(--accent);
    color: #3a2c00;
    font-weight: 800;
    line-height: 1;
    vertical-align: -0.2em;
}
/* The Honeypot_Field, on the Contact_Form and the Report_Dialog alike. Moved
   off-screen rather than display: none, because some bots skip an input that
   is not rendered; its markup takes it out of the tab order and hides it from
   assistive technology. A negative offset adds no scrollable overflow. */
.hp-field {
    position: absolute;
    left: -10000px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}
/* The Settings_Page (settingsPage in tools/gen-pages.mjs): one setting per row,
   the name field in the contact form's label-above-input shape and each other
   setting in the setup screen's .opt-group. The name field is capped near its
   20 characters so it does not stretch across the card. The confirmation is an
   inline panel in the card's own border color, and the status line reserves
   its one line so showing a message does not move the page below it.
   The controls sit on the page axis (ui-conventions.md "Center, do not
   left-align"), while the paragraphs around them stay left-aligned prose:
   each row, the name field's label and note, the Clear button, its confirm
   buttons, and the status line are centered, all scoped through the page's
   own classes so the setup screen's .opt-group is untouched. */
.settingslist { display: flex; flex-direction: column; gap: 16px; margin: 18px 0 6px; }
.settingslist .opt-group { justify-content: center; flex-wrap: wrap; }
.settingsname { align-items: center; text-align: center; }
/* The name itself on the axis too: an input does not take text-align from its
   parent, so the field says it. */
.settingsname .textinput { max-width: 280px; text-align: center; }
.settingsclear { text-align: center; }
.settingsconfirm {
    margin: 12px 0 0;
    padding: 4px 14px 14px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
}
.settingsconfirm-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }
.settingsstatus { min-height: 1.55em; margin: 10px 0 0; font-size: 14px; text-align: center; }
.settingsstatus.error { color: var(--red); }
/* Card colors (deckSetting in tools/gen-pages.mjs): the label over two tiles,
   each a .seg button that shows its deck. The tiles sit side by side and stack
   on a phone, centered either way.
   The selected tile is ringed in the accent and lightly tinted rather than
   filled with it, as a .segmented .seg is: the samples are felt-tuned suit
   inks and card art, and the solid accent would put both on yellow.
   Every row height is reserved from the card size, so cardEl() filling the two
   card rows after load moves nothing (ui-conventions "Containers must not
   move"). 46px is .card.mini, the smallest full art the site paints in a
   hand; the simplified row is the size of a log picture's cards. */
.settingsdeck { flex-direction: column; gap: 8px; }
.deckchoice { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; }
.deckchoice .seg {
    font: inherit;
    color: var(--ink);
    background: rgba(0, 0, 0, 0.18);
    border: 2px solid var(--panel-border);
    border-radius: 10px;
    padding: 8px 12px 10px;
    cursor: pointer;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}
.deckchoice .seg:hover { border-color: rgba(255, 255, 255, 0.35); }
.deckchoice .seg.selected {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.12);
}
.deckopt-name { font-size: 14px; font-weight: 700; }
.deckopt-sample { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.deckopt-cards { display: flex; gap: 4px; justify-content: center; }
.deckopt-full { --cw: 46px; height: calc(46px * 89 / 64); }
.deckopt-simple { --cw: 30px; height: calc(30px * 89 / 64); }
.deckopt-cards .card { box-shadow: 0.03em 0.08em 0.2em rgba(0, 0, 0, 0.4); }
.deckopt-log { font-size: 14px; white-space: nowrap; }
/* The Settings dialog (settingsDialog in tools/gen-pages.mjs, wired by
   src/shared/ui/settings-dialog.ts): the controls above, over the page. The
   Reference_Dialog's geometry, scrim, and layer, a little narrower; the head
   (title centered, Close at the right) stays in view while the body scrolls.
   The body is .pagedoc, so its prose and heading match the Settings_Page. */
.panel.settings-dialog {
    position: fixed;
    inset: 0;
    margin: auto;
    width: min(94vw, 600px);
    height: fit-content;
    max-height: 88vh;
    z-index: 700;
    display: flex;
    flex-direction: column;
    gap: 8px;
    background: linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
.panel.settings-dialog[hidden] { display: none; }
.panel.settings-dialog::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -1;
    background: rgba(0, 0, 0, 0.5);
}
.settings-head {
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 32px;
    flex: none;
}
.settings-head h3 { margin: 0; font-size: 17px; }
.settings-head .btn.small { position: absolute; right: 0; }
.settings-body {
    min-height: 0;
    overflow: auto;
    overscroll-behavior: contain;
    padding: 0 2px 4px;
}
.settings-body:focus { outline: none; }
.settings-body > :first-child { margin-top: 0; }
/* A phone: the full width, and the top of what is VISIBLE, as the Report_Dialog
   does (settings-dialog.ts writes --settings-vvh and --settings-vvtop from the
   visual viewport while open), so the on-screen keyboard raised by the name
   field cannot cover the box. `top`, never a transform, which would make the
   box the containing block of its fixed ::before scrim. */
@media (max-width: 560px), (max-height: 560px) {
    .panel.settings-dialog {
        inset: calc(var(--settings-vvtop, 0px) + max(8px, env(safe-area-inset-top))) 0 auto 0;
        margin: 0 auto;
        width: calc(100vw - 16px);
        max-height: calc(var(--settings-vvh, 100dvh) - 16px);
    }
}

.sitefoot {
    display: flex;
    justify-content: center;
    gap: 22px;
    padding: 8px 18px 28px;
    font-size: 13.5px;
}
.sitefoot a { color: rgba(255, 255, 255, 0.6); }
.sitefoot a:hover { color: var(--accent); }

/* ---------- online chat ---------- */

.chat { margin-bottom: 16px; }
.chat h3 { margin: 0 0 6px; }
.chat-log {
    display: flex;
    flex-direction: column;
    gap: 3px;
    max-height: 180px;
    overflow-y: auto;
    font-size: 13px;
    margin-bottom: 6px;
}
.chat-line {
    padding: 3px 8px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.18);
    overflow-wrap: anywhere;
}
.chat-line.mine { background: rgba(255, 209, 102, 0.14); }
.chat-from { font-weight: 600; opacity: 0.85; }
.chat-row { display: flex; gap: 6px; }
.chat-row .textinput { flex: 1 1 auto; min-width: 0; }
/* ---------- the game-only view ----------
   A page opened with `?focus` (src/shared/ui/game-only.ts) has html.game-only
   from a <head> script before first paint, and these rules leave out the
   links to the rest of the site: the All games button, the hub link in a site
   page's title, the similar-games strip with its All games by similarity link
   (or the More games fallback), the "N more games" designer note, and the
   Site-wide stats link. Everything needed to play the game stays.

   `!important` for the reason [hidden] carries it: this has to win over
   every breakpoint and per-game display rule, `.toolbar .btn`'s inline-flex
   and the phone `.topbar h1 .pagehome` block among them.

   A site page's title is the site name twice (tools/gen-pages.mjs sitePage):
   the `.sitehome` link, and `.sitetitle`, the same name as plain text. The
   view swaps the one for the other, so the title slot stays filled and the
   toolbar stays at the end edge. The plain text takes the phone title's
   ellipsis at every width; at desktop widths nothing narrows it. */
.sitetitle { display: none; }
html.game-only .topbar .allgames,
html.game-only .topbar .sitehome,
html.game-only .setup-othergames,
html.game-only .designermore,
html.game-only #stats-site { display: none !important; }
html.game-only .topbar .sitetitle {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}


/* ---------- setup diagrams (src/shared/ui/setupdiagram/, README "Setup diagrams (opt-in per game)") ----------
   A rules page that opts in carries one <figure class="sdg"> after each numbered Setup step. The
   generator writes each step's size in card widths on the figure (--sdg-wu/--sdg-hu, and the
   narrow class's --sdg-wun/--sdg-hun), so the figure has its final height in the first paint and
   nothing below it moves while it loads or plays. Every row below the stage has a fixed height.
   Two classes wherever a text metric, a margin, or a height is set: the figure sits in .pagedoc li,
   and .pagedoc p (0,1,1) would otherwise give the caption a 1.55 line height. No prefers-reduced-
   motion block: player.ts reads the device setting, and the layout is the same in both modes. No
   `transform` anywhere: cards are placed with `translate` and turned with `rotate`. */
.sdg { display: none; container-type: inline-size; margin: 10px auto 4px; line-height: 1.4; }
.sdg-js .sdg { display: block; }
.rules li > .sdg { margin-left: -20px; width: calc(100% + 20px); }  /* over .rules ol's 20px indent */
/* Each stage is its own stacking context: flights (z 20000), tokens (19000), and over paths (18000)
   stack among the diagram's cards and never over the page's dialogs, summary modal, or coach panel
   (z 400 to 1000). Mini Golf's ball painted over the coach panel until .mg-board got the same. */
.sdg .sdg-stage { --u: var(--sdg-wu); --v: var(--sdg-hu); --cw: min(44px, 100cqi / var(--u));
                  position: relative; height: calc(var(--cw) * var(--v)); overflow: hidden; isolation: isolate; }
.sdg .sdg-stage .sdg-card { position: absolute; left: 0; top: 0; margin: 0; }
/* Show faces: a face-down card drawn face up, ringed outside its edge in the one accent color. */
.sdg .sdg-stage .sdg-card.sdg-shown {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
    box-shadow: 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45), 0 0 6px rgba(255, 209, 102, 0.35);
}
/* "Into the box" has a place: the .card.empty border recipe, behind the box's pile. */
.sdg .sdg-box, .sdg-still .sdg-box { position: absolute; border-radius: 6%; background: rgba(255, 255, 255, 0.05);
                                     border: 1px dashed rgba(255, 255, 255, 0.25); }
.sdg .sdg-label, .sdg-still .sdg-label { position: absolute; font-size: calc(var(--cw) * 0.38); line-height: 1; white-space: nowrap; }
.sdg .sdg-label { translate: -50% 0; }
.sdg .sdg-controls { display: flex; justify-content: center; align-items: center; gap: 6px;
                     height: 36px; margin: 6px 0 0; }
.sdg .sdg-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px;
                height: 36px; min-width: 36px; padding: 0 10px; font-size: 14px; line-height: 1; }
.sdg .sdg-btn.sdg-unavailable { opacity: 0.45; cursor: default; }
.sdg .sdg-playword { min-width: 3.4em; text-align: left; }
.sdg .sdg-captionbox { display: block; margin: 4px 0 0; }
.sdg .sdg-caption, .sdg .sdg-note { margin: 0; line-height: 1.4; text-align: center; }
.sdg .sdg-caption { font-size: 14px; height: 2.8em; overflow: hidden; }
.sdg .sdg-switchrow { display: flex; flex-direction: column; align-items: center; gap: 2px;
                      height: 54px; margin: 4px 0 0; }
/* Show faces on the figure's axis, its legend in the right-hand column beside it. Two equal outer
   columns keep the button centered; the legend wraps to two lines where its column is narrow. */
.sdg .sdg-switchline { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center;
                       align-self: stretch; column-gap: 10px; height: 32px; }
.sdg .sdg-faces { grid-column: 2; height: 32px; font-size: 13px; }
.sdg .sdg-legend { grid-column: 3; justify-self: start; display: inline-flex; align-items: center; gap: 6px; font-size: 13px;
                   line-height: 1; text-wrap: balance; visibility: hidden; }
.sdg.sdg-faces-on .sdg-legend { visibility: visible; }
.sdg .sdg-swatch { display: inline-block; flex: none; width: 11px; height: 15px; border-radius: 2px;
                   background: rgba(255, 255, 255, 0.85); outline: 2px solid var(--accent); outline-offset: 1px;
                   box-shadow: 0 0 6px rgba(255, 209, 102, 0.35); }
.sdg .sdg-note { font-size: 13px; height: 20px; white-space: nowrap; overflow: hidden; }
@container (max-width: 479.98px) {
    .sdg .sdg-stage { --u: var(--sdg-wun, var(--sdg-wu)); --v: var(--sdg-hun, var(--sdg-hu)); }
    .sdg .sdg-caption { height: 4.2em; }
    .sdg .sdg-word { display: none; }
}
@media (pointer: coarse) {
    .sdg .sdg-controls, .sdg .sdg-switchline { height: 44px; }
    .sdg .sdg-btn, .sdg .sdg-faces { height: 44px; min-width: 44px; }
    .sdg .sdg-switchrow { height: 66px; }
}
/* Example diagrams (wave 3 design 7.3): the marks, shared by a still and an animated host (the
   player sizes its own inline, in px). Before the still's rules, so its one-class hide rule comes
   later at the same specificity. A glow ring sits outside its card's edge, wider than Show faces'
   2px ring so the two never read alike (they never share a figure anyway). */
/* In a host the player places each mark by `translate` from the stage's corner, as it does a card. */
.sdg .sdg-ring, .sdg .sdg-tag, .sdg .sdg-token { left: 0; top: 0; }
.sdg .sdg-ring, .sdg-still .sdg-ring { position: absolute; box-sizing: border-box; border-radius: 6%;
                                       outline: 3px solid var(--accent); outline-offset: 2px;
                                       box-shadow: 0 0 10px var(--accent); pointer-events: none; }
.sdg .sdg-tag, .sdg-still .sdg-tag { position: absolute; width: calc(var(--cw) * var(--w)); height: calc(var(--cw) * 0.55);
                                     display: flex; align-items: center; justify-content: center;
                                     font-size: calc(var(--cw) * 0.38); line-height: 1; white-space: nowrap; }
.sdg .sdg-paths, .sdg-still .sdg-paths { position: absolute; left: 0; top: 0; width: 100%; height: 100%; overflow: visible;
                                         pointer-events: none; fill: none; stroke: var(--accent); stroke-width: 0.07;
                                         stroke-linecap: round; stroke-linejoin: round; }
/* A token is a piece, not a card: a disc with a dark rim, which carries it against a white face.
   Each tone is a pattern as well as a color (a solid; b a ring round a light center; c solid
   with a center dot; d a ring with a center dot), so a seat is never told by color alone, and
   each color is at least 3:1 against the rules panel. A chip's letter reads on tone a or b,
   which have no center dot. The ball is Mini Golf's. */
.sdg .sdg-token, .sdg-still .sdg-token { position: absolute; box-sizing: border-box; border-radius: 50%;
                                         border: 1.5px solid #2b2b2b; display: flex; align-items: center; justify-content: center;
                                         color: #2b2b2b; font-weight: 800; line-height: 1; pointer-events: none;
                                         box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5); }
.sdg .sdg-ball, .sdg-still .sdg-ball { background: radial-gradient(circle at 35% 30%, #ffffff 0%, #f0f0f0 38%, #b9b9b9 100%);
                                       box-shadow: 0 2px 6px rgba(0, 0, 0, 0.6), inset 0 -2px 3px rgba(0, 0, 0, 0.15); }
.sdg .sdg-tone-a, .sdg-still .sdg-tone-a { --sdg-tone: #ffd166; background: var(--sdg-tone); }
.sdg .sdg-tone-b, .sdg-still .sdg-tone-b { --sdg-tone: #8ecae6; background: radial-gradient(circle, #f5f2ec 0 34%, var(--sdg-tone) 36%); }
.sdg .sdg-tone-c, .sdg-still .sdg-tone-c { --sdg-tone: #ffa8a8; background: radial-gradient(circle, #2b2b2b 0 18%, var(--sdg-tone) 20%); }
.sdg .sdg-tone-d, .sdg-still .sdg-tone-d { --sdg-tone: #d9b3ff;
                                           background: radial-gradient(circle, #2b2b2b 0 16%, #f5f2ec 18% 34%, var(--sdg-tone) 36%); }
/* A still (.sdg-still) is plain HTML the generator writes, so it shows with no script, on the game
   page's Rules summaries too. Every element is a .sdg-p placed by --x/--y (the narrow class's
   --xn/--yn) in card widths, and stacked only by its --z (narrow --zn), the one z table
   framePlacement gives the player as well: no other still rule sets z-index.
   An element in one width class only carries that class's pair and sdg-wide or sdg-narrow. A size
   that differs in the narrow class rides beside it as --wn, --hn, or --sn. */
.sdg-still { container-type: inline-size; margin: 10px auto 4px; }
.sdg-still .sdg-stage { --u: var(--sdg-wu); --v: var(--sdg-hu); --cw: min(44px, 100cqi / var(--u));
                        position: relative; isolation: isolate;
                        width: calc(var(--cw) * var(--u)); height: calc(var(--cw) * var(--v)); margin: 0 auto; }
.sdg-still .sdg-p { position: absolute; left: 0; top: 0; margin: 0; z-index: var(--z, auto);
                    translate: calc(var(--cw) * var(--x)) calc(var(--cw) * var(--y)); }
.sdg-still .sdg-box, .sdg-still .sdg-ring { width: calc(var(--cw) * var(--w)); height: calc(var(--cw) * var(--h)); }
.sdg-still .sdg-label { width: calc(var(--cw) * var(--w)); text-align: center; }
.sdg-still .sdg-token { width: calc(var(--cw) * var(--s)); height: calc(var(--cw) * var(--s)); font-size: calc(var(--cw) * var(--s) * 0.6); }
/* Hide the other class's elements without setting display on the present ones, so a tag keeps
   display: flex and an svg its own display. `not` covers the 479.98 to 480 sliver too. */
@container not (max-width: 479.98px) { .sdg-still .sdg-narrow { display: none; } }
@container (max-width: 479.98px) {
    .sdg-still .sdg-stage { --u: var(--sdg-wun); --v: var(--sdg-hun); }
    .sdg-still .sdg-p { z-index: var(--zn, var(--z, auto));
                        translate: calc(var(--cw) * var(--xn)) calc(var(--cw) * var(--yn)); }
    .sdg-still .sdg-box, .sdg-still .sdg-ring { width: calc(var(--cw) * var(--wn, var(--w))); height: calc(var(--cw) * var(--hn, var(--h))); }
    .sdg-still .sdg-label { width: calc(var(--cw) * var(--wn, var(--w))); }
    .sdg-still .sdg-token { width: calc(var(--cw) * var(--sn, var(--s))); height: calc(var(--cw) * var(--sn, var(--s)));
                            font-size: calc(var(--cw) * var(--sn, var(--s)) * 0.6); }
    .sdg-still .sdg-wide { display: none; }
}
/* In a reference row, a picture takes its own full-width row under the text. Three classes beat
   `.rules li > .sdg` (FC's -20px for `.rules ol`), which would push it out of a reflist row. */
.cheatsheet .reflist > li > .sdg, .cheatsheet .reflist > li > .sdg-still { grid-column: 1 / -1; margin: 6px 0 2px; width: auto; }
/* A small picture beside its text (ui-conventions.md "Examples fill the text column"). The generator
   marks a picture data-sdg-side s (at most 260px wide) or m (at most 360px; tools/setup-diagram.mjs
   sideOf), and where the list leaves the row's text at least 176px beside it, the picture takes the
   row's right-hand column and the name and description sit on its left, from the top. The figure is
   exactly its reserved width at the 44px card cap (a host at least its 240px transport row), so its
   card width and its height are the ones the generator reserved, and nothing moves while it loads or
   plays. Narrower lists keep the picture on its own line under the text, as above. A container query
   rather than a media query, because a list's width differs between the rules page and the in-game
   Rules summaries at one viewport. */
.cheatsheet .reflist:has(> li > [data-sdg-side]) { container: exlist / inline-size; }
/* Rounded up to a whole pixel: the player measures its stage in whole pixels, so a fractional
   width would draw its cards a hair under the 44px the reservation assumes. */
[data-sdg-side] { --sdg-sidew: round(up, calc(max(var(--sdg-wu), var(--sdg-wun)) * 44px), 1px); }
@container exlist (min-width: 480px) {
    .cheatsheet ul.reflist > li:has(> [data-sdg-side="s"]) {
        grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: auto 1fr;
        grid-template-areas: 'name fig' 'what fig'; align-items: start;
    }
    .cheatsheet ol.reflist > li:has(> [data-sdg-side="s"]) {
        grid-template-columns: 1.4em minmax(0, 1fr) auto; grid-template-rows: auto 1fr;
        grid-template-areas: 'num name fig' 'num what fig'; align-items: start;
    }
    .cheatsheet .reflist > li:has(> [data-sdg-side="s"]) > .reflist-name,
    .cheatsheet .reflist > li:has(> [data-sdg-side="s"]) > .reflist-what { align-self: start; }
    .cheatsheet .reflist > li > .sdg-still[data-sdg-side="s"],
    .cheatsheet .reflist > li > .sdg[data-sdg-side="s"] { grid-area: fig; margin: 2px 0; }
    .cheatsheet .reflist > li > .sdg-still[data-sdg-side="s"] { width: var(--sdg-sidew); }
    .cheatsheet .reflist > li > .sdg[data-sdg-side="s"] { width: max(240px, var(--sdg-sidew)); }
}
@container exlist (min-width: 570px) {
    .cheatsheet ul.reflist > li:has(> [data-sdg-side="m"]) {
        grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: auto 1fr;
        grid-template-areas: 'name fig' 'what fig'; align-items: start;
    }
    .cheatsheet ol.reflist > li:has(> [data-sdg-side="m"]) {
        grid-template-columns: 1.4em minmax(0, 1fr) auto; grid-template-rows: auto 1fr;
        grid-template-areas: 'num name fig' 'num what fig'; align-items: start;
    }
    .cheatsheet .reflist > li:has(> [data-sdg-side="m"]) > .reflist-name,
    .cheatsheet .reflist > li:has(> [data-sdg-side="m"]) > .reflist-what { align-self: start; }
    .cheatsheet .reflist > li > .sdg-still[data-sdg-side="m"],
    .cheatsheet .reflist > li > .sdg[data-sdg-side="m"] { grid-area: fig; margin: 2px 0; }
    .cheatsheet .reflist > li > .sdg-still[data-sdg-side="m"] { width: var(--sdg-sidew); }
    .cheatsheet .reflist > li > .sdg[data-sdg-side="m"] { width: max(240px, var(--sdg-sidew)); }
}
/* An animated example host has no Show faces row: the note line only. Three classes beat FC's
   coarse-pointer 66px (two). */
.sdg.sdg-ex .sdg-switchrow { height: 20px; }
/* ---------- the app layout ----------
 *
 * The game screen as an app: the window is the frame and the page never
 * scrolls. The topbar keeps its height, #screen-game takes exactly the rest,
 * the play area fills its column, and the log scrolls inside its own panel.
 * src/shared/ui/app-layout.ts builds the dock, which holds the Rules summary
 * and Strategy tips that used to sit under the board; an open one is a sheet
 * over the board. docs/guides/app-layout.md is the overview.
 *
 * THREE BODY CLASSES, from tools/gen-pages.mjs and pages/app-layout.config.mjs:
 *   app-layout  the dock, the sheets, and the bot level control at the top of
 *               the log. At every width, scrolling page or not.
 *   app-desk    the frame above 950px, for a game whose board fits a desktop
 *               window (1280x720 and up).
 *   app-phone   the frame at 950px and below, for a game whose board fits a
 *               phone (390x844), with the log in a drawer.
 * A game with neither frame class keeps the scrolling page it always had, and
 * one with just one of them scrolls at the other width.
 *
 * EVERY FRAME RULE IS KEYED ON THE GAME SCREEN SHOWING, through
 * `:has(> #screen-game:not([hidden]))`, so the setup screen, a document of
 * prose, scrolls as it always has. 100dvh rather than 100vh, so a phone's
 * frame is the height actually visible; with nothing to scroll the address bar
 * never collapses, so that height does not change under the player.
 *
 * The play area is a scroll box only as the fallback for a window so small
 * that not even ui/app-fit.ts's smallest zoom fits the board; while the board
 * fits it carries `app-fitted` and cannot scroll at all. Inside it nothing
 * changes: the same block box at the same width, so a board's own layout is
 * untouched.
 */
/* ---- the dock and the sheets, at every width ---- */
/* Desktop: the dock heads the log column, and the play area spans both rows.
   Areas rather than order, because the base grid has two columns and an
   auto-placed dock would take the second column's first row from the log. */
@media (min-width: 951px) {
    body.app-layout > #screen-game {
        grid-template-rows: auto minmax(0, 1fr);
        grid-template-areas: "play dock" "play log";
    }
    body.app-layout .playarea { grid-area: play; }
    body.app-layout .app-dock { grid-area: dock; }
    body.app-layout #log-panel { grid-area: log; }
}
/* Narrow: one column, the dock under the play area and over the log. */
@media (max-width: 950px) {
    body.app-layout .app-dock { order: 1; }
}
body.app-layout .app-dock {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 6px 8px;
    min-width: 0;
}
/* The Log toggle and the drawer's Close exist for the phone frame only. */
body.app-layout .app-logtoggle,
body.app-layout .app-logclose { display: none; }
/* The bot level control, moved to the top of the log panel: one line, set
   off from the log heading under it. */
body.app-layout #log-panel > .level-line { margin: 0 0 12px; flex-wrap: wrap; }
body.app-layout #log-panel > .level-line .opt-label { white-space: nowrap; }
/* A closed rules block is its summary, drawn as a small button, and the Log
   toggle wears the same. */
body.app-layout .app-dock > .app-logtoggle,
body.app-layout .app-dock > details.rules > summary {
    list-style: none;
    font-size: 12px;
    font-weight: 600;
    padding: 4px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    background: var(--panel);
    color: var(--ink);
    white-space: nowrap;
    cursor: pointer;
}
body.app-layout .app-dock > details.rules > summary { display: inline-block; }
body.app-layout .app-dock > details.rules > summary::-webkit-details-marker { display: none; }
body.app-layout .app-dock > .app-logtoggle:hover,
body.app-layout .app-dock > details.rules > summary:hover { background: rgba(255, 255, 255, 0.14); }
body.app-layout .app-dock > details.rules > summary:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}
/* An open one is a sheet over the board, centered the way the lifted round
   summary is (`.panel.result-overlay`), with its own scroll. Its summary stays
   at the top as the way back, with a close mark. z-index 46 is over the coach
   (30) and under the round summary (500) and everything above it. */
body.app-layout .app-dock > details.rules[open] {
    position: fixed;
    inset: 0;
    margin: auto;
    z-index: 46;
    width: min(760px, calc(100vw - 24px));
    height: fit-content;
    max-height: calc(100vh - 32px);
    max-height: calc(100dvh - 32px);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0 18px 16px;
    background: var(--felt-dark);
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
body.app-layout .app-dock > details.rules[open] > summary {
    display: block;
    position: sticky;
    top: 0;
    z-index: 1;
    margin: 0 -18px;
    padding: 12px 18px 10px;
    font-size: 15px;
    border: 0;
    border-bottom: 1px solid var(--panel-border);
    border-radius: 0;
    background: var(--felt-dark);
}
body.app-layout .app-dock > details.rules[open] > summary::after {
    content: '\00d7';
    content: '\00d7' / '';
    float: right;
    font-size: 20px;
    line-height: 18px;
    opacity: 0.8;
}
/* One backdrop for the sheets and the drawer; a click on it closes them. */
body.app-layout .app-backdrop {
    position: fixed;
    inset: 0;
    z-index: 44;
    background: rgba(0, 0, 0, 0.5);
}
/* ---- an inspect view floats over the foot of the board ----
 * `.app-float` marks a panel the player opens to look at something and then
 * closes (a pile spread, an unplayed-cards view), placed in the flow under
 * the board on the scrolling page. In a frame it would grow the board past
 * the window and ui/app-fit.ts would shrink the whole board for the rest of
 * the game; instead, while the frame is on, it floats over the foot of the
 * board until closed, and the board does not move. Floating, it can cover the
 * control that opened it, so it needs a Close of its own: a panel that has
 * none gives one the class `.app-float-close`, which shows only while the
 * panel floats. Nothing here closes it. */
.app-float-close { display: none; }
@media (min-width: 951px) {
    body.app-desk .app-float .app-float-close { display: inline-block; }
    body.app-desk .app-float:not([hidden]) {
        position: fixed;
        left: 50%;
        bottom: 12px;
        translate: -50% 0;
        z-index: 40;
        box-sizing: border-box;
        width: max-content;
        max-width: min(900px, calc(100vw - 24px));
        max-height: 55vh;
        max-height: 55dvh;
        overflow-y: auto;
        margin: 0;
        background: linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
        border: 1px solid var(--panel-border);
        border-radius: 12px;
        box-shadow: 0 10px 40px rgba(0, 0, 0, 0.55);
    }
}
@media (max-width: 950px) {
    body.app-phone .app-float .app-float-close { display: inline-block; }
    body.app-phone .app-float:not([hidden]) {
        position: fixed;
        left: 8px;
        right: 8px;
        bottom: 8px;
        z-index: 40;
        box-sizing: border-box;
        width: auto;
        max-width: none;
        max-height: 55vh;
        max-height: 55dvh;
        overflow-y: auto;
        margin: 0;
        background: linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
        border: 1px solid var(--panel-border);
        border-radius: 12px;
        box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.45);
    }
}
/* ---- the desktop frame ---- */
@media (min-width: 951px) {
    html:has(> body.app-desk > #screen-game:not([hidden])) { overflow: hidden; overscroll-behavior: none; }
    body.app-desk:has(> #screen-game:not([hidden])) {
        display: flex;
        flex-direction: column;
        height: 100vh;
        height: 100dvh;
        min-height: 0;
        overflow: hidden;
        overscroll-behavior: none;
    }
    body.app-desk > .topbar { flex: none; }
    /* `width: 100%` because a flex item with `margin: 0 auto` (kept from the
       base rule, which still centers the 1680px cap) would otherwise shrink to
       fit its content and collapse the grid's 1fr column. */
    /* Half the scrolling page's 16px above and below: in a frame, every pixel
       of height the chrome keeps is one the board has to give up. */
    body.app-desk > #screen-game:not([hidden]) {
        flex: 1 1 auto;
        min-height: 0;
        width: 100%;
        padding-top: 8px;
        padding-bottom: 8px;
    }
    body.app-desk .playarea {
        min-height: 0;
        overflow-x: hidden;
        overflow-y: auto;
        overscroll-behavior: contain;
    }
    /* While the board fits (ui/app-fit.ts), no scrollbar at all: a box that can
       scroll by a hairline shows one on every resize, and a lifted card at the
       edge would make it scrollable for a moment. */
    body.app-desk .playarea.app-fitted { overflow: hidden; }
    /* The play area clips, so its clip edge sits 6px outside the board: a
       tutorial ring (.coach-mark, 3px outline at a 2px offset) or a .hinted
       card on a full-width panel is drawn whole instead of cut along the
       edge. The negative margin hands the padding back, so the board keeps
       its place and width. Same in the phone frame below. */
    body.app-desk .playarea { margin: -6px; padding: 6px; }

    /* The log column: the same card, as tall as its lines up to the column's
       height, then it scrolls inside. Positioned (relative, as sticky was) so
       the previews that place themselves against it (Agents of War's card
       chips) keep their containing block. */
    body.app-desk #log-panel { position: relative; top: auto; max-height: 100%; min-height: 0; }
}
/* ---- the phone frame: one column, the log in a drawer ----
 * The play area keeps the whole height, the dock is one row under it, and the
 * log opens from the Log toggle as a drawer over the board. The rows after the
 * first are implicit, so a panel that is not displayed costs no row and no gap.
 *
 * A running tutorial's coach panel is the one part of the log panel that stays
 * on screen with the drawer shut: it sits between the play area and the dock,
 * in the flow, instead of docking over the board as it does on the scrolling
 * page, so the play area gives up its height rather than being covered. */
@media (max-width: 950px) {
    html:has(> body.app-phone > #screen-game:not([hidden])) { overflow: hidden; overscroll-behavior: none; }
    body.app-phone:has(> #screen-game:not([hidden])) {
        display: flex;
        flex-direction: column;
        height: 100vh;
        height: 100dvh;
        min-height: 0;
        overflow: hidden;
        overscroll-behavior: none;
    }
    body.app-phone > .topbar { flex: none; }
    body.app-phone > #screen-game:not([hidden]) {
        flex: 1 1 auto;
        min-height: 0;
        width: 100%;
        grid-template-rows: minmax(0, 1fr);
        padding-top: 6px;
        padding-bottom: 6px;
        row-gap: 6px;
    }
    body.app-phone .playarea {
        order: 0;
        min-height: 0;
        overflow-x: hidden;
        overflow-y: auto;
        overscroll-behavior: contain;
    }
    body.app-phone .playarea.app-fitted { overflow: hidden; }
    body.app-phone .playarea { margin: -6px; padding: 6px; }
    body.app-phone #log-panel { order: 1; max-height: none; }
    body.app-phone .app-dock { order: 2; }
    body.app-phone .app-dock > .app-logtoggle { display: inline-block; }
    body.app-phone .app-logtoggle.app-unread::after {
        content: '';
        display: inline-block;
        width: 7px;
        height: 7px;
        margin-left: 6px;
        border-radius: 50%;
        background: var(--accent);
        vertical-align: 1px;
    }
    body.app-phone:not(.app-log-open) #log-panel > :not(#coach-panel) { display: none; }
    body.app-phone:not(.app-log-open) #log-panel:has(> #coach-panel[hidden]) { display: none; }
    body.app-phone:not(.app-log-open) #log-panel {
        padding: 0;
        border: 0;
        background: none;
    }
    body.app-phone #coach-panel:not([hidden]) {
        position: static;
        max-height: 34dvh;
        overflow-y: auto;
        margin: 0;
        border-radius: 12px;
        border-bottom: 1px solid var(--panel-border);
        box-shadow: none;
    }
    body.app-phone:has(#coach-panel:not([hidden])) { padding-bottom: 0; }
    /* The drawer: a sheet from the bottom edge, over the dock and the board. */
    body.app-phone.app-log-open #log-panel {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 45;
        height: min(78dvh, 640px);
        max-height: none;
        border-radius: 12px 12px 0 0;
        border-bottom: 0;
        background: var(--felt-dark);
        box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.45);
    }
    body.app-phone.app-log-open .app-logclose { display: block; align-self: flex-end; margin: 0 0 6px; }
    body.app-phone.app-log-open #log-entries { flex: 1 1 auto; }
}
/* ---------- round pictures in the log ----------
   Built by logPicture (src/shared/ui/logpic.ts): the cards that decided a
   finished round, under its heading in the grouped log, so the round reads
   without being opened.

   One grid for the whole picture: label, strip, value. The rows are
   `display: contents`, so every row's strip starts at the same x and card N of
   one row sits over card N of the next. A strip too long for the 300px log
   column wraps inside its own cell rather than widening the column.

   22px cards, which is below the 34px line where ui-conventions makes the
   simplified atlas unconditional, and logpic.ts always asks for it. The
   picture sits inside a bold <summary>, so it resets the weight: only the
   heading's own text is bold, and the value column is bold on its own. */
.logpic {
    --cw: 22px;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 3px 8px;
    /* A right gutter: #log-entries has no padding, so a value column flush to
       its edge sits under an overlay scrollbar while the log scrolls. */
    margin: 4px 8px 2px 1em;
    font-size: 12px;
    font-weight: 400;
}
.logpic-row { display: contents; }
.logpic-name {
    max-width: 7em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    opacity: 0.8;
}
.logpic-cards {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px;
    min-width: 0;
}
.logpic .card.logpic-card { --cw: 22px; flex: none; }
.logpic-value {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}
.logpic-winner > .logpic-name,
.logpic-winner > .logpic-value { color: var(--accent); opacity: 1; }
.logpic-value.logpic-bad { color: var(--red); text-decoration: line-through; }
.logpic-empty { opacity: 0.6; }
.logpic-gap { flex: none; width: 5px; }
/* A trick's winner, ringed: in a row of one card per trick, the ring is what
   says who took it. */
.logpic .card.logpic-won { outline: 2px solid var(--accent); outline-offset: 0; }
.logpic .card.logpic-lost { opacity: 0.45; }
.logpic .card.logpic-struck { opacity: 0.4; filter: grayscale(1); }
/* A sideways card, in a box as wide as it lies, turned with `rotate` and
   centered with `translate` under that box (ui-conventions, "A sideways card
   turns with rotate"). */
.logpic-turn {
    position: relative;
    flex: none;
    width: calc(var(--cw) * 89 / 64);
    height: calc(var(--cw) * 89 / 64);
}
.logpic-turn > .card.logpic-card {
    position: absolute;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    rotate: 90deg;
}
/* ---------- draft pictures in the log ----------
   Built by draftPicture (src/shared/poker/draftpic.ts): Grid Poker's grid and
   Ring Poker's ring under a finished unit's heading. The board on the left,
   in the dealt places, and the key beside it. The cards are logpic.css's 22px
   simplified faces (.logpic-card); a taken card is ringed in its pick's color
   with that pick's number at its corner, and a card nobody took is faded
   (.logpic-lost). The picture sits inside a bold <summary>, so it resets the
   weight, as .logpic does. */
.draftpic {
    --cw: 22px;
    --dp1: var(--accent);
    --dp2: #8ec5ff;
    --dp3: #9be7a9;
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 4px 8px 2px 1em;
    font-size: 12px;
    font-weight: 400;
}
.draftpic .card.logpic-card { --cw: 22px; flex: none; }
/* logpic.css fades a card only inside .logpic, so the same fade here. */
.draftpic .card.logpic-lost { opacity: 0.35; }
.draftpic-board { position: relative; flex: none; }
.draftpic-grid .draftpic-board {
    display: grid;
    grid-template-columns: repeat(3, var(--cw));
    grid-auto-rows: calc(var(--cw) * 89 / 64);
    gap: 4px;
}
/* The ring: six places on a circle, position 1 at the top and clockwise, the
   board's own angles. The radius keeps the two cards on each side clear of
   each other (they sit one radius apart, one above the other). The slot is
   placed with a transform on the SLOT, never on the card (ui-conventions, "A
   sideways card turns with rotate"). */
.draftpic-ring .draftpic-board {
    --dpr: 40px;
    width: calc(2 * var(--dpr) * 0.866 + var(--cw) + 8px);
    height: calc(2 * var(--dpr) + var(--cw) * 89 / 64 + 6px);
}
.draftpic-ring .draftpic-slot {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%)
        rotate(var(--angle)) translate(var(--dpr))
        rotate(calc(-1 * var(--angle)));
}
.draftpic-slot { position: relative; display: block; line-height: 0; }
.draftpic-slot.draftpic-taken > .card { outline: 2px solid var(--dpc); outline-offset: 0; }
.draftpic-p1 { --dpc: var(--dp1); }
.draftpic-p2 { --dpc: var(--dp2); }
.draftpic-p3 { --dpc: var(--dp3); }
.draftpic-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--dpc);
    color: #10281a;
    font-size: 9px;
    font-weight: 800;
    line-height: 1;
    flex: none;
}
.draftpic-slot > .draftpic-badge {
    position: absolute;
    right: -5px;
    bottom: -4px;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45);
}
.draftpic-key { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.draftpic-keyline { display: flex; align-items: center; gap: 6px; min-width: 0; }
.draftpic-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    opacity: 0.85;
}
/* The key's sample of a faded card: a blank face at the faded strength. */
.draftpic-swatch {
    width: 9px;
    height: 12px;
    border-radius: 2px;
    background: #f4f1ea;
    opacity: 0.45;
    flex: none;
}
.draftpic-left .draftpic-name { opacity: 0.6; }
/* ---------- a look at an opponent's draft, while hands are built ----------
   Built by peekBox (src/shared/poker/peek.ts) for Grid and Ring Poker. The
   opponent's box is a button in the build phase: the look of the panel it was,
   a caret after the name, and the accent ring while its cards are open. The
   cards open in a panel UNDER THE OPPONENTS' ROW, floating over the build
   panel, so nothing moves and the window's fit is not shrunk for a look. The
   strip inside is the draft phase's own .opponent-cards (26px simplified
   cards, ten to a row). */
button.opponent.poker-peekable {
    display: block;
    margin: 0;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}
.opponent.poker-peekable > .opponent-name,
.opponent.poker-peekable > .opponent-detail { display: block; }
.opponent.poker-peekable > .opponent-name::after {
    content: '\25BE';
    content: '\25BE' / '';
    margin-left: 6px;
    opacity: 0.7;
}
.opponent.poker-peekable.open > .opponent-name::after {
    content: '\25B4';
    content: '\25B4' / '';
}
.opponent.poker-peekable:hover,
.opponent.poker-peekable:focus-visible { border-color: var(--accent); }
.opponent.poker-peekable.open { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
/* The row is what the panel is placed against, so it centers under the row
   and never runs off a phone's edge, whichever box opened it. */
#opponents:has(> .poker-peek) { position: relative; }
.poker-peek {
    position: absolute;
    top: calc(100% + 6px);
    left: 50%;
    translate: -50% 0;
    z-index: 30;
    display: block;
    box-sizing: border-box;
    width: max-content;
    max-width: 100%;
    margin: 0;
    padding: 10px 14px;
    border-radius: 10px;
    background: linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
    border: 1px solid var(--accent);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
    cursor: default;
}
.poker-peek > .opponent-taken-label { margin-bottom: 6px; }
/* Exchange Poker: the face-down cards (the ones only they have seen) sit a
   little apart from the faces they took in the open. */
.poker-peek .card.poker-peek-unseen { margin-left: 8px; }
/*
 * COLLAPSIBLE TRACKERS (src/shared/ui/collapsible.ts): a box marked
 * `data-collapsible` shows only its first child, its head, while it carries
 * `collapsed`. The head stays as the game drew it, with a caret button at its
 * start that the module puts there, and the whole head is the click target.
 *
 * Shared, so it sits before every game's sheet (tools/gen-css.mjs ORDER). The
 * selectors weigh three (an attribute, a class, and a pseudo-class or a
 * second class), so a game's own one- or two-class rules for its head and
 * body (`.lf-unseenhead`, `.panel.lf-unseen`) do not override the collapse.
 */
[data-collapsible] > :first-child { cursor: pointer; }
[data-collapsible].collapsed > :not(:first-child) { display: none; }
/* The head's own bottom margin spaces it from a body that is not shown. */
[data-collapsible].collapsed > :first-child { margin-bottom: 0; }
/* A closed panel is one line: about half its usual vertical padding. */
.panel[data-collapsible].collapsed { padding-top: 7px; padding-bottom: 7px; }
/* The caret, as a bare button: no box, the head's color and size. */
.collapse-toggle {
    display: inline-flex;
    align-items: center;
    flex: none;
    margin: 0 0.4em 0 0;
    padding: 2px;
    border: 0;
    border-radius: 4px;
    background: none;
    color: inherit;
    font: inherit;
    line-height: 1;
    cursor: pointer;
    vertical-align: baseline;
}
[data-collapsible] > :first-child:hover .collapse-toggle,
.collapse-toggle:hover { color: var(--accent); }
.collapse-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (pointer: coarse) {
    /* A finger needs more than the caret: the head is the target, and this
       keeps the caret's own box at a tappable size. */
    .collapse-toggle { min-width: 28px; min-height: 28px; justify-content: center; margin-right: 0.15em; }
}
/* ---------- the peer prompt's cover (src/shared/ui/peer-prompt.ts) ----------
   Pass and play asks the table before an undo on this dialog, and the device
   may be shown to a player who must not see the cards on screen. So with
   `cover` the backdrop is as opaque as the handoff gate: nothing behind it may
   be readable. The dialog's own rules (.peer-prompt, .peer-prompt-card) are in
   platform.css. */
.peer-prompt.peer-prompt-cover::before {
    background: radial-gradient(ellipse at 50% -20%, var(--felt), var(--felt-dark) 70%);
}
/* =========================================================================
 * Split Court
 *
 * Prefix policy: every Split-Court-specific class is `scr_`. The shared layout
 * classes (.card, .cardrow, .panel, .build-hand, .result-*, .pile-chip, ...) are
 * reused as-is, so only what is genuinely new lives here.
 * ========================================================================= */

/* The face-down reserved card reuses the existing `.card simple back` cell from
   the simplified atlas, so nothing new is needed for it here. */

/* --- draft action strip ------------------------------------------------- */

.scr_actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 8px 0;
}

.scr_action_token {
    flex: 1 1 120px;
    min-width: 108px;
    padding: 6px;
    border: 2px solid rgba(255, 255, 255, 0.18);
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.06);
    text-align: center;
    transition: opacity 0.2s ease, border-color 0.2s ease;
}

.scr_action_icon {
    width: 34px;
    height: 34px;
    margin: 0 auto 2px;
    color: #ffd166;
}

.scr_action_label {
    font-size: 12px;
    font-weight: 700;
}

.scr_action_cost {
    font-size: 11px;
    opacity: 0.7;
}

.scr_action_marks {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 4px;
    min-height: 16px;
}

.scr_action_mark {
    font-size: 10px;
    padding: 1px 6px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.16);
}

.scr_action_mark.mine {
    background: #2f6f4f;
}

.scr_action_mark.theirs {
    background: #7a4b1e;
}

/* Who reached the action first. Informational, but it is the only record of the
   order the two players committed, so it is worth marking. */
.scr_action_mark.first {
    outline: 1px solid #ffd166;
}

.scr_action_token.choosable {
    cursor: pointer;
    border-color: rgba(255, 209, 102, 0.5);
}

.scr_action_token.choosable:hover,
.scr_action_token.choosable:focus-visible {
    border-color: #ffd166;
    background: rgba(255, 209, 102, 0.12);
    outline: none;
}

.scr_action_token.chosen {
    border-color: #ffd166;
    background: rgba(255, 209, 102, 0.2);
}

/* Used by BOTH players: spent for the rest of the round. */
.scr_action_token.spent {
    opacity: 0.4;
    border-style: dashed;
}

/* --- collections -------------------------------------------------------- */

.scr_collection {
    margin-bottom: 8px;
    padding: 6px 8px;
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.04);
}

.scr_collection.mine {
    border-color: rgba(47, 111, 79, 0.9);
    background: rgba(47, 111, 79, 0.14);
}

.scr_collection_title {
    font-size: 13px;
    font-weight: 700;
    margin-bottom: 4px;
}

.card.reserved {
    outline: 2px dashed #ffd166;
    outline-offset: 1px;
}

/* --- the offer ---------------------------------------------------------- */

.scr_offer {
    border: 2px solid #ffd166;
    background: rgba(255, 209, 102, 0.1);
}

.scr_offer_title {
    font-size: 13px;
    font-weight: 700;
    margin-bottom: 6px;
}

/* One row, always: an offer is read by comparing its groups side by side, and on
   a phone the third Offer 3 card used to wrap onto a line of its own. The groups
   may shrink (min-width 0) and the phone rule below trims their padding and the
   Keep button, so three single cards fit at 360px. Centered on the board axis. */
.scr_offer_sets {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: 14px;
}

.scr_offer_set {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 8px;
    border: 2px solid transparent;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.18);
}

.scr_offer_set > .card {
    display: inline-block;
}

.scr_offer_set.choosable {
    border-color: rgba(255, 255, 255, 0.22);
}

.scr_offer_set.choosable:hover {
    border-color: #ffd166;
    background: rgba(0, 0, 0, 0.3);
}

/* --- card selection ----------------------------------------------------- */

/* Offer 2 Sets: the first two picks form set A and the next two set B. Without a
   visible grouping the player cannot tell what they are actually offering.
   `position: relative` is scoped to these two rather than added to the shared
   `.card`, so the badge anchors without affecting any other game. */
.card.set-a,
.card.set-b {
    position: relative;
}

.card.set-a {
    outline: 3px solid #6aa9e0;
    outline-offset: 1px;
}

.card.set-b {
    outline: 3px solid #e0a06a;
    outline-offset: 1px;
}

.scr_set_badge {
    position: absolute;
    top: 2px;
    left: 2px;
    min-width: 15px;
    padding: 0 3px;
    border-radius: 3px;
    background: rgba(0, 0, 0, 0.8);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    line-height: 15px;
    text-align: center;
}

.scr_hand_controls {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 8px;
}

@media (max-width: 640px) {
    .scr_action_token {
        flex-basis: 45%;
        min-width: 0;
    }

    .scr_offer_sets {
        gap: 8px;
    }

    .scr_offer_set {
        min-width: 0;
        padding: 6px;
    }

    .scr_offer_set > .btn {
        padding: 6px 8px;
        font-size: 13px;
        white-space: nowrap;
    }
}

/* A token that cannot be armed right now still answers a tap: it shows what
   happened with that action this round (renderActionStrip). */
.scr_action_token.scr_inspectable {
    cursor: pointer;
}

.scr_action_token.scr_inspectable:focus-visible {
    outline: 2px solid rgba(255, 255, 255, 0.6);
    outline-offset: 2px;
}

.scr_action_token.inspected {
    border-color: rgba(255, 255, 255, 0.7);
    border-style: solid;
    opacity: 1;
}

/* Cards picked first, more than this action takes: it cannot be the one. */
.scr_action_token.scr_toosmall {
    opacity: 0.45;
}

/* While building: the four cards the opponent's face-down card could be. */
.scr_hidden_pool {
    margin-top: 4px;
    font-size: 13px;
    color: rgba(255, 255, 255, 0.8);
}

/* --- Split Court: armed action + motion -------------------------------- */

/* The armed action carries its own progress counter, so no separate "3/4 selected"
   text is needed in the status line or next to the hand. */
.scr_action_progress {
    margin-top: 3px;
    font-size: 12px;
    font-weight: 700;
    color: #ffd166;
}

.scr_action_token.chosen {
    border-color: #ffd166;
    background: rgba(255, 209, 102, 0.2);
    animation: scr-arm 500ms ease-out;
}

@keyframes scr-arm {
    0% {
        box-shadow: 0 0 0 0 rgba(255, 209, 102, 0.85);
    }
    100% {
        box-shadow: 0 0 0 12px rgba(255, 209, 102, 0);
    }
}

/* Selecting a card for an action: animate the lift so the eye follows which card
   just joined the selection, rather than the row silently changing. */
.scr_offer_set > .card,
#hand-cards .card,
#build-hands .card,
#build-tray .card {
    transition: transform 130ms ease, outline-color 130ms ease, box-shadow 130ms ease;
}

/* A card you have picked for the current action. The lift alone was too subtle to
   read as "chosen", so a picked card also gets the accent outline, a brightened
   face, and a numbered badge. */
#hand-cards .card.selected {
    position: relative;
    transform: translateY(-10px);
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 6px 14px rgba(0, 0, 0, 0.5);
    z-index: 1;
}

/* Everything NOT picked recedes once a selection is under way, so the chosen
   cards are what the eye lands on. */
#hand-cards.scr_picking .card:not(.selected) {
    opacity: 0.55;
}

#hand-cards.scr_picking .card:not(.selected):hover {
    opacity: 1;
}

/* The pick badge: the order you clicked, or the set letter for Offer 2 Sets where
   pick order decides the pairing. */
.scr_pick_badge {
    position: absolute;
    top: -6px;
    left: -6px;
    min-width: 20px;
    height: 20px;
    padding: 0 4px;
    border-radius: 10px;
    background: var(--accent);
    color: var(--accent-ink);
    font-size: 12px;
    font-weight: 700;
    line-height: 20px;
    text-align: center;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
    pointer-events: none;
}

/* Offer 2 Sets keeps the A/B color coding on top of the shared highlight. */
#hand-cards .card.selected.set-a {
    outline-color: #6aa9e0;
}

#hand-cards .card.selected.set-b {
    outline-color: #e0a06a;
}

#hand-cards .card.selected.set-a .scr_pick_badge {
    background: #6aa9e0;
    color: #08243c;
}

#hand-cards .card.selected.set-b .scr_pick_badge {
    background: #e0a06a;
    color: #3c2408;
}

/* The offer is waiting on YOU. Split Court's offers were being missed entirely,
   so the panel breathes until a choice is made. */
.scr_offer {
    animation: scr-await 1.6s ease-in-out infinite;
}

@keyframes scr-await {
    0%,
    100% {
        border-color: #ffd166;
    }
    50% {
        border-color: rgba(255, 209, 102, 0.45);
    }
}

.scr_offer_set.choosable {
    cursor: pointer;
}

.scr_offer_set.choosable:hover,
.scr_offer_set.choosable:focus-within {
    transform: translateY(-4px);
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.35);
}

body.motion-off .scr_action_token.chosen,
body.motion-off .scr_offer,
body.motion-off .scr_offer_set.choosable:hover {
    animation: none;
    transition: none;
    transform: none;
}

/* The outline and badge stay: they carry the meaning. Only the motion goes. */
body.motion-off #hand-cards .card.selected {
    animation: none;
    transition: none;
    transform: none;
}

/* --- Split Court: the per-round log ------------------------------------- */
/*
 * The collapsible round itself is styled by the platform now, for every game:
 * `details.loggroup` / `.loggroup-body` in src/shared/styles/platform.css, built
 * by the shared renderGroupedLogInto. Only the scoring entry INSIDE a round is
 * Split Court's own, and it stays here.
 */

/* The round's scoring entry, stacked: a row per player's two hands, then a row
   per comparison. As one sentence it ran well past 180 characters. */
.logline.scr_log_score {
    display: flex;
    flex-direction: column;
    gap: 1px;
    margin: 4px 0;
    padding-top: 4px;
    border-top: 1px dotted rgba(255, 255, 255, 0.14);
}
.scr_log_who {
    font-weight: 700;
    /* Keeps the two players' hand names starting at the same column. */
    display: inline-block;
    min-width: 78px;
}
.scr_log_verdict {
    padding-left: 78px;
    opacity: 0.85;
}
.scr_log_verdict::before {
    content: '\2192\00a0';
    opacity: 0.6;
}
@media (max-width: 720px) {
    /* Too narrow to hold a label column: let the names run inline instead. */
    .scr_log_who { min-width: 0; }
    .scr_log_verdict { padding-left: 10px; }
}

/* ---------- UX-audit additions (boot note, stats, level control, badges,
   touch targets, viewport density) ---------- */

/* Pre-JS placeholder: visible until the app's first render removes it, and
   the noscript fallback. Centered and muted - it must read as scaffolding,
   never as content. */
.bootnote {
    margin: 18vh auto 0;
    max-width: 400px;
    text-align: center;
    color: rgba(255, 255, 255, 0.55);
    font-size: 15px;
}
/* The shell removes #boot-note on its first render, which on a cached load is
   near-instant - fast enough that a visible "Loading the game" only flashes for
   a fraction of a second on the way in from the hub. Hold it invisible, then
   fade it in, so it appears only when a load is genuinely slow and never
   flickers on a quick one. The <noscript> .bootnote is a separate element and
   is unaffected: a JS-disabled visitor still sees its message at once. */
#boot-note { opacity: 0; animation: bootnote-in 200ms ease-out 450ms forwards; }
@keyframes bootnote-in { to { opacity: 1; } }
/* Under "Animations: Off" the global zeroed duration already jumps the fade to
   its fill, but that still leaves a finished animation on the element. The
   static stand-in shows the note at its end state with no animation at all. */
body.motion-off #boot-note { animation: none; opacity: 1; }

/* When the shell reveals the setup screen (drops [hidden]), ease it in rather
   than popping, so arriving from the hub lands softly. Scoped to setup on
   purpose: it is a simple centered card with no position:fixed descendants,
   whereas animating the game board's opacity would, for the length of the
   fade, make it the containing block for any fixed child and shift it. */
#screen-setup:not([hidden]) { animation: screen-in 160ms ease-out; }
@keyframes screen-in { from { opacity: 0; } to { opacity: 1; } }

body.motion-off #boot-note { animation-duration: 1ms; }
body.motion-off #screen-setup:not([hidden]) { animation: none; }

/* Setup-screen record line + reset control (shell-rendered). */
.statsrow {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin: 2px 0 6px;
    min-height: 0;
}
.statsline {
    color: rgba(255, 255, 255, 0.62);
    font-size: 13px;
}
.btn.subtle {
    opacity: 0.65;
    font-size: 12px;
    padding: 2px 8px;
}
.btn.subtle:hover:not(:disabled) { opacity: 1; }

/* Hub badges: save-state pill + record line under each game blurb. Both live
   in ONE row (see decorate() in src/shared/hub.ts) so a game you have played
   is a single line taller than a fresh one, not three. */
.gamestatus {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 4px 8px;
    margin-top: 3px;
}
.gamebadge {
    padding: 1px 8px;
    border-radius: 999px;
    background: rgba(255, 209, 102, 0.16);
    border: 1px solid rgba(255, 209, 102, 0.45);
    color: var(--accent);
    font-size: 12px;
    font-weight: 600;
}
.gamestats {
    color: rgba(255, 255, 255, 0.55);
    font-size: 12px;
}

/* Split Court: the caption under the strip. The armed action's consequence, then
   any earlier use of it; or, for a token tapped to look back, that action's plays
   this round. */
.scr_action_hint {
    flex: 1 1 100%;
    margin-top: 4px;
    color: rgba(255, 255, 255, 0.66);
    font-size: 13px;
    text-align: center;
}

.scr_action_detail {
    color: rgba(255, 255, 255, 0.85);
    margin-top: 2px;
}

.scr_action_detail_head {
    font-weight: 700;
}
/* =========================================================================
 * Cascade
 *
 * Prefix policy: every Cascade-specific class is `cascade-` (matching `duel-`
 * and `ring-`, not Split Court's `scr_`). The shared layout classes (.card,
 * .cardrow, .panel, .pile-chip, .result-*, ...) are reused as-is, so only what
 * is genuinely new lives here.
 * ========================================================================= */

/* --- board and seats ---------------------------------------------------- */

/* THE SIZE SEAM. A grid is --cascade-n columns square: 4, or 5 on a 5x5 match.
   renderBoard puts .cascade-size-5 on the board AND on the tray (#pile-line,
   .cascade-pile), because the arrange and choose panels sit in the tray,
   outside the board, and size their cards off the same column count. No other
   rule names a column count: every formula below reads --cascade-n.

   --cascade-own-cw is the viewing seat's cell width, declared on both elements
   so the board's grid and the arrange tray (which restates it for the dealt
   cards) cannot drift apart. Each breakpoint redefines it once, here and in the
   950px and 560px blocks.

   Two .cascade-seat panels side by side, so the widest layout is 2n card
   columns. The cap fits those 2n columns to the viewport (the --duel-cw
   technique): the subtracted chrome is 428px of everything beside the cards
   (page padding, the log column on wide layouts, seat padding, the between-seat
   gap, scrollbar slack) plus the in-grid gaps, n - 1 per grid. At n = 4 that is
   428 + 2 * 3 * 6 = 464px over 8 columns, the formula this replaced, so a 4x4
   board is the same size it always was; at n = 5 it is 428 + 48 = 476px over 10
   columns (80px a cell at 1280px wide, 47px just above 950px). */
/* THE HEIGHT CAP. The whole game fits one screen: the own cell is also capped
   so that n rows of cells fit the viewport height after --cascade-chrome, the
   height of everything above and below the grid (the topbar, the status and
   progress lines, the tray, the turn banner, the seat chrome, and the margins).
   --cascade-chrome is MEASURED per breakpoint with a browser probe (the header
   of this block names the widths), not derived, because it sums shared rows
   this file does not own. --cascade-vh is the SMALL viewport height where the
   browser has it, so a phone's collapsing address bar never resizes the board
   mid-game. Floored at --cascade-min-cw: below that a short window scrolls
   rather than shrink the cards to a smear.

   --cascade-board-w is the board's width at the wide layout (two seats at the
   own cell, each with 22px of padding and border, 14px between them). The
   tray and the turn banner take it as their width, centered, so all three
   share one left edge and nothing in the tray slides sideways when its
   contents change between phases. The narrow layouts set it to 100%.

   --cascade-tray-cw sizes the tray's cards (the draw pile, the held card, the
   discard pile, and the pile previews), apart from the grid's cells, so the
   tray is one short fixed-height row at every grid size. */
.cascade-board, .cascade-pile, .cascade-turn {
    --cascade-n: 4;
    --cascade-gap: 6px;      /* between cells within a grid */
    --cascade-vh: 100vh;
    --cascade-chrome: 404px;
    --cascade-min-cw: 36px;
    --cascade-tray-cw: 50px;
    --cascade-fit-cw: calc((var(--cascade-vh) - var(--cascade-chrome)
        - (var(--cascade-n) - 1) * var(--cascade-gap)) / var(--cascade-n) * 64 / 89);
    --cascade-own-cw: max(var(--cascade-min-cw), min(var(--cw), var(--cascade-fit-cw),
        calc((100vw - 428px - 2 * (var(--cascade-n) - 1) * var(--cascade-gap))
            / (2 * var(--cascade-n)))));
    --cascade-board-w: calc(2 * (var(--cascade-n) * var(--cascade-own-cw)
        + (var(--cascade-n) - 1) * var(--cascade-gap) + 22px) + 14px);
}
@supports (height: 100svh) {
    .cascade-board, .cascade-pile, .cascade-turn { --cascade-vh: 100svh; }
}
.cascade-board.cascade-size-5, .cascade-pile.cascade-size-5,
.cascade-turn.cascade-size-5 { --cascade-n: 5; }
/* The tray and the banner sit on the board's axis at the board's width. */
.cascade-pile, .cascade-turn {
    width: min(100%, var(--cascade-board-w));
    margin-inline: auto;
}
.cascade-board {
    --cascade-cw: var(--cascade-own-cw);
    /* The opponent's floor in the narrow layouts (see the 950px block). */
    --cascade-opp-floor: 24px;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: 14px;
    width: fit-content;
    margin: 10px auto 16px;
}

.cascade-seat {
    padding: 10px;
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    background: var(--panel);
}
/* The viewing player's own grid. */
.cascade-seat.own { background: rgba(255, 255, 255, 0.05); }
/* Whose turn it is. Same treatment as .opponent.active. */
.cascade-seat.active {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}

.cascade-seat-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
.cascade-seat-name { font-weight: 700; }
.cascade-seat-detail { font-size: 13px; opacity: 0.75; }

/* --- grid and cells ----------------------------------------------------- */

.cascade-grid {
    display: grid;
    grid-template-columns: repeat(var(--cascade-n), var(--cascade-cw));
    gap: var(--cascade-gap);
}
/* Less the cell's 1px border on each side, so a card fills its cell exactly
   instead of spilling into the gap (everything here is border-box). */
.cascade-grid .card { --cw: calc(var(--cascade-cw) - 2px); }

.cascade-cell {
    width: var(--cascade-cw);
    height: calc(var(--cascade-cw) * 89 / 64);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 8px;
    /* Transparent by default so the highlight borders never shift layout. */
    border: 1px solid transparent;
}
/* Empty cells stay visible, reusing the .card.empty dashed placeholder. */
.cascade-cell.empty {
    background: rgba(255, 255, 255, 0.05);
    border: 1px dashed rgba(255, 255, 255, 0.25);
    box-shadow: none;
}

/* Legal target for the held card: a neutral light ring plus a faint wash.
   Yellow is reserved for "you go again" below, so ordinary legal cells get the
   neutral treatment - the reverse of the Duel Poker convention, on purpose. */
.cascade-cell.legal {
    border: 1px solid rgba(255, 255, 255, 0.55);
    background: rgba(255, 255, 255, 0.1);
    cursor: pointer;
}
.cascade-cell.legal:hover { background: rgba(255, 255, 255, 0.18); }

/* Legal AND it produces a diagonal match: gold, and pulsing. hintpulse
   animates outline-color, so the outline is what carries the pulse; the border
   and wash carry the meaning on their own if the animation is suppressed. */
.cascade-cell.legal-extra {
    border: 1px solid var(--accent);
    background: rgba(255, 209, 102, 0.18);
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    cursor: pointer;
}
.cascade-cell.legal-extra:hover { background: rgba(255, 209, 102, 0.28); }

/* Preview highlights for the hover feature: "this card COULD go here if you
   took it", as opposed to .legal / .legal-extra which mean "your held card can
   go here". They read as provisional - dashed and dimmer than the committed
   styles - and are deliberately never animated, so they do not compete with
   the .legal-extra pulse. */
.cascade-cell.preview {
    border: 1px dashed rgba(255, 255, 255, 0.45);
    background: rgba(255, 255, 255, 0.05);
}
/* Provisional go-again: keeps the gold hue so the extra-turn meaning survives,
   but stays flat - dashed border, faint wash, no outline, no pulse. */
.cascade-cell.preview-extra {
    border: 1px dashed var(--accent);
    background: rgba(255, 209, 102, 0.08);
}

/* Arrangement step: the four main-diagonal cells are the drop targets. */
.cascade-cell.diag-target {
    border: 1px dashed rgba(255, 209, 102, 0.55);
    background: rgba(255, 209, 102, 0.08);
    cursor: pointer;
}
.cascade-cell.diag-target:hover { background: rgba(255, 209, 102, 0.16); }

/* --- draw pile ---------------------------------------------------------- */

/* The take action is the game's primary affordance, so #draw-pile is a full
   card-back-sized target rather than the small text chip the siblings use. It
   keeps its id and stays a single element updated in place (the animator flies
   an acquired card FROM it, so it must not be rebuilt), but reads as a
   pressable card back: the page's 64:89 card footprint, card radius, and the
   blue back gradient/hatch already used by the .pile-chip mini back. The app
   writes the label ("Draw N - tap to take") as its text content. */
.cascade-draw {
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    flex: none;
    width: calc(var(--cascade-tray-cw) * 1.2);
    height: var(--cascade-tray-h);
    padding: 4px;
    border-radius: 8px;
    border: 1px solid rgba(255, 255, 255, 0.45);
    background:
        repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.09) 0 6px, transparent 6px 12px),
        linear-gradient(135deg, #5c6bc0, #3949ab);
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    line-height: 1.25;
    cursor: pointer;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
    transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
    user-select: none;
    -webkit-user-select: none;
}
.cascade-draw:hover {
    transform: translateY(-4px);
    box-shadow: 0 7px 16px rgba(0, 0, 0, 0.45);
    filter: brightness(1.08);
}
.cascade-draw:active {
    transform: translateY(-1px);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
    filter: brightness(0.96);
}
.cascade-draw:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

/* Not takeable (empty pile, or off the player's turn): flat, dimmed, and
   unpressable. The app toggles .inert; the hover/press lift is suppressed so
   nothing invites a click the engine would reject. */
.cascade-draw.inert {
    cursor: default;
    opacity: 0.7;
    filter: saturate(0.5) brightness(0.72);
    box-shadow: none;
}
.cascade-draw.inert:hover,
.cascade-draw.inert:active {
    transform: none;
    box-shadow: none;
    filter: saturate(0.5) brightness(0.72);
}
body.motion-off .cascade-draw { transition: none; }
body.motion-off .cascade-draw:hover,
body.motion-off .cascade-draw:active { transform: none; }

/* --- the play-phase tray (draw + held + discard) ------------------------ */

/* THE height reservation (Bug 4). The tray is a fixed-height horizontal row;
   during the play phase nothing it contains may change its height, so the two
   grids below never move when a card is drawn, held, discarded, or when the
   discard pile grows.

   --cascade-tray-h is the HELD BLOCK's height, its one-line title (14.4px),
   the 4px gap, and a card at --cascade-tray-cw, and the draw pile is made
   exactly that tall, so the tray is one height through the whole play phase:

     held block  = 14.4 + 4 + tray-cw * 89/64      50px -> 87.9   38px -> 71.2
     draw pile   = the same, by definition
     discard row = discard-cw * 89/64 + 12px gutter
                   (0.92 * tray-cw)                 46px -> 76.0   35px -> 60.6

   Every card here is sized off --cascade-tray-cw, not --cw or the grid cell, so
   the tray is the same short row at 4x4 and 5x5 and the height the grids get
   (THE HEIGHT CAP at the top of this file) is the same at both. Scoped to
   Cascade: the shared .pile-line nine other games use must gain neither a
   min-height nor nowrap.

   flex-wrap: nowrap (overriding the shared .pile-line's wrap) is the other half
   of the fix: a wrapped tray would be two rows tall. The discard block absorbs
   any width shortfall by scrolling inside itself (flex: 1; min-width: 0), so the
   row cannot wrap or overflow the page however narrow the viewport gets.

   justify-content: flex-start (the shared row centers its contents): the draw
   pile stays at the board's left edge in every phase, whether it is alone, has
   the pile choice or the arrangement beside it, or the held card and the
   discard pile. Centered, it jumped 418px between the start of the match and
   the first turn, and the held card 332px. */
.cascade-pile {
    --cascade-tray-h: calc(14.4px + 4px + var(--cascade-tray-cw) * 89 / 64);
    align-items: flex-start;
    flex-wrap: nowrap;
    justify-content: flex-start;
    min-height: var(--cascade-tray-h);
}

/* --- held card (inline, beside the draw pile) --------------------------- */

/* The held card as a compact inline block beside the draw pile: a one-line
   title on top, then the card face with the Discard button BESIDE it. NOT a
   full-width .panel (its border + margin-bottom was what pushed the board
   down), and deliberately only two stacked rows tall - title over card - so it
   fits the tray height budget above. Emptied, not merely hidden, by renderHeld
   behind the gate (R13.2).

   RESERVED WIDTH (Bug 3): the slot keeps this fixed footprint for the whole
   play phase, holding a card or not, so the discard pile beside it never
   slides when a card is taken or resolved. renderHeld leaves the block in the
   tray flow through play (emptied when nothing is held), and flex: none plus a
   fixed width mean the empty slot is exactly as wide as a full one, pinning the
   discard pile's left edge. The width is --cw + 90px, not a bare --cw multiple:
   the widest ordinary shape is the card face (--cw, fluid) beside the Discard
   button (~67px, FIXED text width that does not scale with --cw) plus the 8px
   gap, so the 90px covers the button and gap at every breakpoint while the
   --cw term tracks the card. */
.cascade-held {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    flex: none;
    width: calc(var(--cascade-tray-cw) + 90px);
}
/* The held card at the tray size, under the reservation above. Scoped through
   the tray's own class (never a bare .card rule). */
.cascade-pile .cascade-held .card { --cw: var(--cascade-tray-cw); }
/* One clipped line: the block must not grow a second title line (that would
   break the height budget) nor stretch the tray wide enough to force a wrap on
   a phone (that would break it the other way). max-width keeps a long name
   ("Player 2's card - from the discard pile") from doing either; the meaning
   "your card" survives the ellipsis. Capped at the slot's reserved width
   (100%) so a long name never stretches the block past its fixed footprint. */
.cascade-held-title {
    margin: 0;
    font-size: 12px;
    line-height: 1.2;
    font-weight: 600;
    opacity: 0.9;
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* The card and its Discard button share ONE row, so the button adds width, not
   a third stacked row - that third row was what tipped the held block past the
   draw-pile height (Bug 4). The row's height is the card's, which is under the
   budget at both breakpoints (see .cascade-pile). */
.cascade-held-row {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 8px;
}

/* --- turn banner -------------------------------------------------------- */

/* Names the active player and the pending decision, so it is always on screen
   rather than only in the status line.

   min-height reserves TWO text lines (Bug 4, pure layout). The banner sits just
   above the board and its text changes length constantly: renderTurnBanner
   appends the chain clause (" Extra turn! xN - matched Jacks on the diagonal")
   whenever a placement chains, which wraps the single "whose turn - what to do"
   line onto a second. Without a reservation that wrap shoves the whole board
   down; with two lines always reserved the board cannot move when the clause
   comes and goes. 2.8em = two lines at the inherited 1.4 line-height; the +14px
   covers the 6px*2 padding and 1px*2 border (border-box). */
.cascade-turn {
    padding: 6px 12px;
    margin: 0 auto 10px;     /* auto: on the board's axis, at its width (top) */
    min-height: calc(2.8em + 14px);
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    font-weight: 600;
    /* Centered, like the status line above and the outcome line it turns into
       at the end: left-aligned, the sentence jumped from the left edge to the
       middle when the match ended. */
    text-align: center;
}
.cascade-turn.yours { color: var(--accent); border-color: var(--accent); }

/* --- progress line and the repetition warning --------------------------- */

/* #progress-line carries the fill counts and, while it applies, the
   repetition warning instead (renderProgress, R18). The class is added by the
   renderer, which is how a game scopes a rule to a shell-owned line. Its
   height is RESERVED at the warning's, about 105 characters: one line where
   the game column holds it at this size, two below the width in the media
   query, so the warning coming and going cannot move the board. */
.progress.cascade-progress {
    font-size: 13px;
    line-height: 18px;
    min-height: 18px;
}
@media (max-width: 1099px) {
    .progress.cascade-progress { min-height: 36px; }
    .cascade-board, .cascade-pile, .cascade-turn { --cascade-chrome: 422px; }
}
.progress.cascade-progress.cascade-warn {
    color: var(--accent);
    opacity: 1;
    font-weight: 600;
}

/* --- end-of-match outcome ---------------------------------------------- */

/* The winner and both turn counts, written near the status/banner area
   instead of the removed result box. Celebratory but restrained: gold text on
   a soft gold wash, sized to stay readable on a phone. */
.cascade-outcome {
    margin: 0 0 12px;
    padding: 10px 16px;
    border: 1px solid var(--accent);
    border-radius: 10px;
    background: rgba(255, 209, 102, 0.12);
    color: var(--accent);
    font-size: 18px;
    font-weight: 700;
    text-align: center;
}

/* --- discard pile and notes -------------------------------------------- */

/* The discard block lives in the tray now (complaints 1-3): a compact label
   beside a single scrolling row of cards, not a full-width .panel below the
   board. --cascade-discard-cw makes the cards clearly bigger than the old
   .mini (46px at --cw 72, 34px at --cw 56) while still fitting the tray height
   budget: 0.82 * --cw is 59px at 72 and 46px at 56, whose card heights (82px /
   64px) plus a 12px scrollbar gutter stay under the draw pile (see
   .cascade-pile). It scales from --cw so it tracks the 560px breakpoint on its
   own. flex: 1 + min-width: 0 lets the row absorb the tray's spare width and
   scroll, which is what keeps the whole tray from wrapping (Bug 4). */
.cascade-discard-panel {
    --cascade-discard-cw: calc(var(--cascade-tray-cw) * 0.92);
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 1 1 0;
    min-width: 0;
}
/* The label beside the row. Short on screen ("Discard - N"); the full
   "...any of them can be taken" sentence lives in its title/aria-label
   (set by renderDiscard) so the row keeps its width. One clipped line so it
   never adds height to the tray. */
.cascade-discard-title {
    margin: 0;
    flex: none;
    font-size: 12px;
    line-height: 1.2;
    font-weight: 600;
    opacity: 0.9;
    white-space: nowrap;
}
/* A SINGLE fixed-height row that scrolls sideways as the pile grows, rather
   than a wrapping grid (complaint 2 vs 4): bigger cards, but adding a card
   scrolls the row instead of growing a new line that would push the board
   down. The height is one card row plus a small gutter so a horizontal
   scrollbar, when the platform draws one in-flow, does not clip the faces; it
   is fixed, so the tray height never changes with the card count. renderDiscard
   keeps s.discardPile's left-to-right order (R3.6) and scrolls to the end after
   each render, so the newest card (far right) is always in view. */
.cascade-discard {
    display: flex;
    flex-wrap: nowrap;
    gap: 4px;
    align-items: flex-start;
    overflow-x: auto;
    overflow-y: hidden;
    height: calc(var(--cascade-discard-cw) * 89 / 64 + 12px);
}
/* Enlarge the discard cards past .mini by overriding --cw for this row only -
   the sanctioned Cascade-scoped technique (like .cascade-grid .card). The app
   still tags them .mini; sprites.ts measures the real rendered width to pick
   the atlas, so the larger size gets the right art with no code change. */
.cascade-discard .card { --cw: var(--cascade-discard-cw); }
/* A card that has JUST been discarded flashes briefly and fades (Bug 2),
   rather than the old permanent outline on "the newest card" (which stuck
   forever because it was derived every repaint). The app adds
   .just-discarded in animate() and removes it on a timer; the animation only
   fades the accent outline out, so once it is gone the card looks like any
   other. outline (not border/box-shadow) so the row never reflows. */
.cascade-discard .just-discarded {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
    animation: cascade-discard-flash 600ms ease-out;
}
@keyframes cascade-discard-flash {
    from { outline-color: var(--accent); }
    to { outline-color: transparent; }
}
body.motion-off .cascade-discard .just-discarded { animation: none; }

.cascade-note { font-size: 13px; opacity: 0.8; margin-top: 6px; }

/* --- arrangement box (in the tray, during the arrange phase) ------------ */

/* The arrangement box keeps its .panel "box" look (complaint 3 called it a
   box) but sits in the tray flex row beside the draw pile instead of as a
   full-width block below the board. flex + min-width: 0 let it take the tray's
   spare width and let its one-row card tray scroll sideways on a narrow phone
   rather than overflow the page or wrap to a second line.

   It is EXEMPT from the play-phase height budget on purpose: it only shows
   during the 'arrange' phase, when the held block and discard pile are empty,
   so the tray is [draw][arrange] and its height changing once at that phase
   boundary is the one-time transition the user does not mind - not the
   per-action jitter of Bug 4. margin-bottom: 0 drops the shared .panel's
   trailing margin, which has no place on a flex-row item. */
.cascade-arrange {
    flex: 1 1 auto;
    min-width: 0;
    margin-bottom: 0;
    /* Change 2 (the panel was ~249px at --cw 72, ~40% of it the tray cards at
       full --cw and the two stacked controls rows) trimmed the padding, the
       head, the note, and the controls below, and those trims stay. The tray
       cards were shrunk too, to fit the play-phase reservation, and are now
       full grid size again (see --cascade-arrange-cw). All heights track --cw
       and the 89/64 card aspect, never bare px
       (.kiro/steering/ui-conventions.md). */
    padding: 5px 10px;                       /* was the shared .panel 14px 16px */
    /* The dealt cards are the SAME SIZE as the card on the player's own grid,
       so a card looks the same in the tray as on the diagonal it goes to (the
       user chose card size over fitting the tray reservation; see below).
       --cascade-cw is not in scope here: it lives on .cascade-board, and this
       panel sits in #pile-line, outside the board. So it reads
       --cascade-own-cw, which .cascade-pile declares beside the board with the
       same column count (the size seam at the top of this file), and takes off
       the 2px the grid takes for the cell border (.cascade-grid .card). One
       declaration per breakpoint now serves both, so the two cannot drift.
       FULL art at every size (never .simple): these are cards the player drags.

       PANEL HEIGHT. The panel is now taller than .cascade-pile's play-phase
       reservation (--cw * 1.3 * 89/64), so the board drops once when setup
       starts and rises once when play begins: a one-time phase change, not
       per-action movement. .cascade-pile's min-height is NOT changed, because
       it pins the play phase. During setup the height is constant, because the
       tray is a fixed one-row height (below) however many cards it holds. */
    --cascade-arrange-cw: calc(var(--cascade-own-cw) - 2px);
}
/* Game-scoped, driven off the tray element so ONE property sizes both the
   cards and the row height. NEVER a bare `#arrange-tray` or `.card` rule:
   ui-conventions lists eight shipped leaks of exactly that shape. The
   `.cascade-arrange` ancestor keeps it off every other game's tray.

   ONE ROW, FIXED HEIGHT, so taking a card off the diagonal or putting one back
   cannot change the panel's height: the page's `wrap` class is overridden to
   nowrap, and the row scrolls sideways if the panel is too narrow for four
   (left-aligned, since a centered scrolling strip clips its left overflow).
   The height is one card (--cw * 89/64) plus 6px on top for the 5px hover
   lift, which overflow would otherwise clip, plus a 12px scrollbar gutter (the
   .cascade-discard allowance), so a classic scrollbar does not cover the
   faces. A fixed `height`, not `min-height`: the row is the same height with
   zero, one, or four cards in it, and with or without a scrollbar. */
.cascade-arrange #arrange-tray {
    --cw: var(--cascade-arrange-cw);
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    padding: 6px 0 0;
    height: calc(var(--cw) * 89 / 64 + 18px);
    min-height: 0;
}
/* Compact head: keep the sort button (a real control), drop the panel-head's
   bottom margin, shrink the title, and never let it wrap - a wrapped title
   would add a line and move the panel as cards are assigned. */
.cascade-arrange .panel-head { margin: 0; }
.cascade-arrange .panel-head h3 { font-size: 13px; }
.cascade-arrange #hand-sort { padding: 2px 8px; }
.cascade-arrange #arrange-title {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* The advisory is RESERVED, never hidden (ui-conventions: a panel that appears
   mid-round should be an empty-state line, not `hidden`). One line held whether
   it shows or is empty, so the decreasing-diagonal warning cannot appear
   mid-drag and grow the panel. nowrap+ellipsis keeps the one short sentence to
   one line. */
.cascade-arrange #arrange-note {
    margin: 2px 0 0;
    font-size: 12px;
    min-height: 1.1em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* ONE controls row now (Fill beside Confirm), tight top margin - was two
   .controls rows at 12px 0 each. #arrange-fill lives inside #arrange-controls
   (renderArrange keeps the static Fill and rebuilds only .cascade-confirm), so
   this single row carries both. Confirm padding is trimmed to fit the budget. */
.cascade-arrange #arrange-controls { margin: 4px 0 0; }
.cascade-arrange #arrange-controls .cascade-confirm { padding: 4px 14px; }

/* --- pile choice box (in the tray, during the 'choose' phase) ----------- */

/* The "I cut, you choose" step (R2.9-R2.14, Decision 16). Like the arrangement
   box it sits in the tray beside the draw pile and is EXEMPT from the
   play-phase height budget: it only shows during the one-time 'choose' phase,
   when the held block and discard pile are empty, so the tray is [draw][choose]
   and its height changing once at that phase boundary is fine (the budget only
   pins the per-action play-phase height). It and the arrange box never coexist
   (choose precedes arrange). flex + min-width: 0 let it take the tray's spare
   width; margin-bottom: 0 drops the shared .panel margin on a flex-row item.

   --cascade-choose-cw sizes the two preview grids' cards, driven off this
   element so ONE property sizes them (never a bare `.card` or `#choose-options`
   rule - ui-conventions lists eight shipped leaks of exactly that shape). At
   0.42 * --cw it is ~30px at --cw 72 and ~24px at --cw 56, in the small-card
   band, which is why renderChoose draws these read-only previews from the
   simplified atlas. Re-derived for phones in the 560px block below. */
.cascade-choose {
    /* Scaled by 4 / --cascade-n, so a 5x5 preview keeps the 4x4 footprint:
       30px a cell at --cw 72 on 4x4, 24px on 5x5. Both are read-only and in
       the unconditional simplified tier. */
    --cascade-choose-cw: calc(var(--cascade-tray-cw) * 0.5 * 4 / var(--cascade-n));
    flex: 1 1 auto;
    min-width: 0;
    margin-bottom: 0;
    padding: 5px 10px;
}
.cascade-choose #choose-title {
    margin: 0 0 6px;
    font-size: 13px;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* The two piles side by side, centered (ui-conventions: the board axis is
   centered). They wrap to stack on a viewport too narrow to hold both, rather
   than overflow the tray. */
.cascade-choose-options {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px;
}
/* One pile: a pressable card-like block around its 4x4 preview grid. Keyboard
   and pointer both commit the take (renderChoose wires role/tabindex/onclick).
   The .hinted ring is the shared hint treatment, scoped to this option. */
.cascade-choose-option {
    padding: 6px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.04);
    cursor: pointer;
}
.cascade-choose-option:hover {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.1);
}
.cascade-choose-option:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.cascade-choose-option.hinted {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}
/* The n x n preview: only the main diagonal is filled, the rest are empty
   placeholders, so a pile reads as it will land on the diagonal. */
.cascade-choose-grid {
    display: grid;
    grid-template-columns: repeat(var(--cascade-n), var(--cascade-choose-cw));
    gap: 3px;
}
.cascade-choose-grid .card { --cw: var(--cascade-choose-cw); }
.cascade-choose-cell {
    width: var(--cascade-choose-cw);
    height: calc(var(--cascade-choose-cw) * 89 / 64);
    border-radius: 5px;
}
/* Empty (off-diagonal) cells: the same faint dashed placeholder the board
   uses, so the diagonal cards stand out against them. */
.cascade-choose-cell.empty {
    background: rgba(255, 255, 255, 0.05);
    border: 1px dashed rgba(255, 255, 255, 0.2);
}
body.motion-off .cascade-choose-option.hinted { animation: none; }

/* --- motion ------------------------------------------------------------- */

/* The acting seat changed: a short gold ripple on the turn banner and on the
   incoming player's grid. */
.cascade-turn.turn-flip, .cascade-seat.turn-flip {
    animation: cascade-turnflip 600ms ease-out;
}
@keyframes cascade-turnflip {
    from { box-shadow: 0 0 0 0 rgba(255, 209, 102, 0.85); }
    to { box-shadow: 0 0 0 12px rgba(255, 209, 102, 0); }
}
body.motion-off .cascade-turn.turn-flip,
body.motion-off .cascade-seat.turn-flip { animation: none; }
/* The gold border and wash still distinguish the extra-turn cells. */
body.motion-off .cascade-cell.legal-extra { animation: none; }

/* --- narrow layouts ----------------------------------------------------- */

/* Below 950px #screen-game collapses to one column and the log drops BELOW the
   board (platform.css), so the board gets the full page width. The two seats
   stay SIDE BY SIDE here rather than stacking - the point of this layout, from
   the complaint "I can't see all of my opponent's grid". Your own grid keeps
   its --cw size; the opponent's read-only grid shrinks to whatever width is
   left, which also drops it into the small-card band so renderBoard draws it
   from the simplified atlas (keyed on this SAME 950px query via
   NARROW_LAYOUT_QUERY, so the CSS size and the JS atlas cannot disagree). */
@media (max-width: 950px) {
    .cascade-board {
        /* align-items: flex-start (base) top-aligns the two seats. flex-wrap +
           max-width are the sub-350px release valve - see the opponent floor. */
        flex-wrap: wrap;
        max-width: 100%;
    }
    /* The own cell stays --cw; the viewport ceiling never binds at these
       widths, so the own grid is not shrunk there. The ceiling is one grid's
       n columns: 82px of chrome plus its n - 1 in-grid gaps, so at n = 4 it is
       (100vw - 100px) / 4, the formula this replaced. Declared on the board and
       the tray alike, so the arrange tray card tracks it (.cascade-arrange). */
    .cascade-board, .cascade-pile, .cascade-turn {
        --cascade-chrome: 420px;
        /* THE PAIR WIDTH: one own column plus one opponent column, after the
           chrome the opponent rule below names (96px, and both grids' gaps).
           The own cell takes what the pair leaves after the opponent's floor,
           so the opponent always fits beside it rather than wrapping below,
           which doubled the board's height on a phone at 5x5. */
        --cascade-pair-w: calc((100vw - 96px - 2 * (var(--cascade-n) - 1) * var(--cascade-gap))
            / var(--cascade-n));
        --cascade-own-cw: max(var(--cascade-min-cw), min(var(--cw), var(--cascade-fit-cw),
            calc((100vw - 82px - (var(--cascade-n) - 1) * var(--cascade-gap))
                / var(--cascade-n)),
            calc(var(--cascade-pair-w) - var(--cascade-opp-floor))));
        /* The opponent's cell is its own clamp here, so the board's width is
           not a formula of the own cell any more: the tray and banner span the
           column, which the board nearly fills at these widths. */
        --cascade-board-w: 100%;
    }
    /* A 5x5 opponent squeezed under 34px would be a smear even on the
       simplified art, and it leaves the side-by-side layout at that size anyway,
       so its floor is the top of the unconditional simplified tier. The floor
       is also the size it wraps to (see below). 4x4 keeps 24px, as before. */
    .cascade-board.cascade-size-5 { --cascade-opp-floor: 34px; }
    /* The tray and the banner do not read the opponent floor, but the own
       cell they size from does (above), so they carry it too. */
    .cascade-pile, .cascade-turn { --cascade-opp-floor: 24px; }
    .cascade-pile.cascade-size-5, .cascade-turn.cascade-size-5 { --cascade-opp-floor: 34px; }
    /* THE OPPONENT'S CELL. Width left for its n columns after: page padding
       (16px each side = 32), both seats' padding+border (2*(2*10+2*1) = 44),
       the 14px between-seat gap, and ~6px slack for scrollbar/sub-pixel
       rounding (96px together), plus both grids' in-grid gaps, 2(n - 1) of
       --cascade-gap. That is 96 + 36 = 132px at n = 4 (the constant this
       replaced) and 96 + 48 = 144px at n = 5. Divided by n, minus the own grid.
       That width, divided by n, is --cascade-pair-w (declared with the own
       cell above), and the opponent takes what the own cell leaves of it.
       Capped at min(own cell, 46px) so the opponent is never the larger of the
       two AND never leaves the small-card band (ui-conventions: <=46px
       read-only wants the simplified atlas, which renderBoard then applies).
       Floored at --cascade-opp-floor, which the own cell already left room
       for, so the two stay side by side; flex-wrap is only the release valve
       for a window narrower than both floors. */
    .cascade-seat:not(.own) {
        --cascade-cw: clamp(var(--cascade-opp-floor),
            calc(var(--cascade-pair-w) - var(--cascade-own-cw)),
            min(var(--cascade-own-cw), 46px));
    }
    /* The opponent head must not stretch its seat wider than its narrow grid
       (that would move the board). Cap it at the grid width and ellipsize the
       name - nowrap, never a second line (ui-conventions) - with smaller text. */
    .cascade-seat:not(.own) .cascade-seat-head {
        max-width: calc(var(--cascade-n) * var(--cascade-cw)
            + (var(--cascade-n) - 1) * var(--cascade-gap));
        gap: 4px;
        font-size: 11px;
        overflow: hidden;
    }
    .cascade-seat:not(.own) .cascade-seat-name {
        min-width: 0;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }
    .cascade-seat:not(.own) .cascade-seat-detail {
        font-size: 11px;
        white-space: nowrap;
    }
}
/* Phones: tighter gaps and trimmed seat chrome, so the side-by-side grids keep
   as much card width as possible. The opponent cell is re-derived for the
   phone chrome (ui-conventions: re-derive phone figures separately). */
@media (max-width: 560px) {
    .cascade-board {
        gap: 8px;       /* tighter between-seat gap (was 14px); folded in below */
    }
    /* Re-derived for the phone (ui-conventions): the own ceiling is one grid's
       n columns after 71px of chrome and n - 1 gaps of 3px, which is
       (100vw - 80px) / 4 at n = 4, the formula this replaced. The ceiling does
       not bind at phone widths (--cw is 40-56px here), so the own grid stays at
       --cw on both sizes: 45px at 390px wide, 44px at 360px. */
    .cascade-board, .cascade-pile, .cascade-turn {
        --cascade-gap: 3px;
        --cascade-tray-cw: 38px;
        --cascade-chrome: 406px;
        --cascade-min-cw: 28px;
        --cascade-pair-w: calc((100vw - 62px - 2 * (var(--cascade-n) - 1) * var(--cascade-gap))
            / var(--cascade-n));
        --cascade-own-cw: max(var(--cascade-min-cw), min(var(--cw), var(--cascade-fit-cw),
            calc((100vw - 71px - (var(--cascade-n) - 1) * var(--cascade-gap))
                / var(--cascade-n)),
            calc(var(--cascade-pair-w) - var(--cascade-opp-floor))));
    }
    /* A 5x5 opponent beside the own grid at 34px left the own cell 27px on a
       390px phone, so on a phone the 5x5 floor is 24px, simplified art (the
       unconditional tier), and the own grid gets the rest of the pair. */
    .cascade-board.cascade-size-5, .cascade-pile.cascade-size-5,
    .cascade-turn.cascade-size-5 { --cascade-opp-floor: 24px; }
    .cascade-draw { font-size: 11px; }
    /* Trim the seat chrome on phones to buy grid width. */
    .cascade-seat { padding: 6px; }
    /* Re-derived for the phone chrome: page padding 10px each side (20), both
       seats' padding+border 2*(2*6+2*1) = 28, the 8px between-seat gap, plus
       ~6px slack (62px together), plus both grids' in-grid gaps, 2(n - 1) of
       3px. That is 62 + 18 = 80px at n = 4 (the constant this replaced) and
       62 + 24 = 86px at n = 5. Same cap and floor as the 950 block, through
       this block's --cascade-pair-w, so both grids stay side by side on a
       phone at either size (a 5x5 opponent used to wrap below 526px wide and
       double the board's height). */
    .cascade-seat:not(.own) {
        --cascade-cw: clamp(var(--cascade-opp-floor),
            calc(var(--cascade-pair-w) - var(--cascade-own-cw)),
            min(var(--cascade-own-cw), 46px));
    }
    .progress.cascade-progress { font-size: 12px; line-height: 16px; min-height: 32px; }
    /* 14px holds the longest turn sentence (a take with the extra-turn clause)
       in the two reserved lines down to a 360px phone; at the inherited size
       it wrapped to three there and moved the board 22px. */
    .cascade-turn { font-size: 14px; }
    .cascade-outcome { font-size: 16px; padding: 8px 12px; }
    /* Re-derived for the phone (ui-conventions): the own cell's phone formula
       above, less the cell border. Phone --cw is 40-56px and the viewport
       ceiling does not bind at phone widths, so this is --cw - 2px: 43px at
       390px wide, 42px at 360px. Still full art, since the player drags it. */
    /* The arrange tray card needs no phone rule of its own any more: it reads
       --cascade-own-cw, which the .cascade-pile declaration above re-derives. */
    /* Re-derived, not the desktop 0.42 (ui-conventions: re-derive phone figures
       separately). Phone --cw is 40-56px; the previews are read-only, so they
       may go smaller than the draggable arrange cards. Floored at 26px so two
       4x4 grids still fit side by side or wrap cleanly, and simple-atlas art
       stays legible. Scaled by 4 / n like the desktop size, so a 5x5 preview
       keeps the 4x4 footprint: 26px a cell becomes 20.8px. */
    .cascade-choose {
        --cascade-choose-cw: calc(var(--cascade-tray-cw) * 0.6 * 4 / var(--cascade-n));
    }
}

/* =========================================================================
 * Evolution Poker
 *
 * Prefix policy: every Evolution-specific class is `evo-`. The shared layout
 * classes (.card, .cardrow, .panel, .opponent*, .result-*, .pile-chip, ...)
 * are reused as-is, so only what is genuinely new lives here.
 * ========================================================================= */
/* The round's plays: one labeled cell per seat (face-down backs while picks
   are secret; the revealed queue in resolution order during a choice). */
.evo-play { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.evo-play-name {
    font-size: 11px; opacity: 0.75;
    max-width: calc(var(--cw) + 14px);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.evo-play.current .evo-play-name { color: var(--accent); opacity: 1; font-weight: 700; }
.evo-play.resolved { opacity: 0.6; }
/* A resolved play's card has physically moved on (to the center or a
   capturer's pile), so its cell shows an id-less text record instead. */
.evo-play-record {
    width: var(--cw);
    height: calc(var(--cw) * 89 / 64);
    display: flex; align-items: center; justify-content: center;
    border: 1px dashed rgba(255, 255, 255, 0.3);
    border-radius: 6%;
    background: rgba(255, 255, 255, 0.04);
    font-size: 15px;
}
/* Two lines' worth of status, always. The playback narrates each beat
   ("Bot 1's Q♠ captures 2 cards: 5♠, Q♥.") which wraps to two lines, while the
   prompts take one - and the shared .status reserves only one, so the board and
   the hand stepped down 24px and back on every wrapping beat. The
   crisps-status / truc-status recipe. */
.status.evo-status { min-height: 2.9em; }

@media (max-width: 620px) {
    /* Narrower column, so the same narration takes a line more. */
    .status.evo-status { min-height: 4.4em; }
}

/* Reserve the center row's height so it NEVER collapses.
   At each round's end the center is discarded and sits empty for one playback
   beat before the next deal refills it. Without a floor the row went 100px ->
   0 -> 100px, so the hand panel below it jumped up and back down every round
   (measured: 18 of 19 hand jumps landed exactly on centerCards === 0) - and
   because animateTransitions diffs card POSITIONS, that shift made every card
   in the hand read as "moved", so the whole hand was hidden and re-flown
   twice per round (the "public cards blinking in and out" report).
   One card row is also the row's MAXIMUM: the center starts a round at its
   deal size and every resolution removes at least one card while returning
   exactly one, so it can only shrink from there. Uses the same metrics as
   .card, so it follows --cw through the narrow-viewport breakpoint. */
#evo-center { min-height: calc(var(--cw) * 89 / 64); }
/* One axis (Cuttle treatment): both public rows and the hand center. The
   plays row gets the same one-card floor as the center row above, and for the
   same reason: it empties at the top of every round, and without a floor the
   panels below it would jump twice a round. */
#evo-plays { justify-content: center; min-height: calc(var(--cw) * 89 / 64); }
#evo-center { justify-content: center; }

/* Opponent panels: the ✓ once a seat's secret pick is in. */
.evo-committed { color: var(--accent); font-weight: 800; }
/* Help text beside the Commit button. */
.evo-help { align-self: center; font-size: 13px; opacity: 0.8; }
/* Evolution opponent panels: quiet held-count beside a centered name, then ONE
   compact card row (face-down backs, then the public acquired minis). */
.evo-opponent { display: flex; flex-direction: column; align-items: center; text-align: center; }
.evo-opponent .opponent-name { width: 100%; text-align: center; }
.opponent-count { font-weight: 400; font-size: 12px; opacity: 0.7; }
.evo-opp-cards { justify-content: center; margin-top: 6px; flex-wrap: wrap; }
.evo-opp-cards .card { --cw: 30px; }
/* Card reference: static under the hand, so examining discards or unknown
   cards never moves anything the player is choosing. Both strips reserve one
   tiny-card row and scroll sideways when full. Discards are face-up; unknown
   cards are anonymous backs, never identities from the deck or a rival's
   secret hand. */
.evo-card-reference { margin-top: 0; }
.evo-card-reference > summary { cursor: pointer; font-weight: 700; text-align: center; }
.evo-reference-section + .evo-reference-section {
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px solid rgba(255, 255, 255, 0.12);
}
.evo-reference-section > h3 { margin: 4px 0 5px; font-size: 13px; text-align: center; }
.evo-reference-cards {
    flex-wrap: nowrap;
    gap: 3px;
    min-height: calc(28px * 89 / 64);
    overflow-x: auto;
    overflow-y: hidden;
    padding: 1px 0 4px;
}
.evo-reference-cards .card { --cw: 28px; flex: none; }
.evo-reference-empty { align-self: center; font-size: 13px; opacity: 0.7; }
.evo-unseen-card { pointer-events: none; }

/* Scored-hand log entries (collapsible): one player per line inside the
   <details> body, indented under the disclosure marker. Only the summary is
   bold, so nothing in the body inherits a weight (the wrapper is not a
   `.section`; see details.loggroup in platform.css for why). */
.evo-logscore { padding: 5px 0; }
.evo-logscore > summary { font-weight: 700; cursor: pointer; }
.evo-logscore-line { padding: 2px 0 2px 16px; }
.evo-logscore-line.totals { opacity: 0.75; margin-top: 2px; }
/* The viewer's acquired cards are public knowledge - a corner eye says so.
   The badge is the SHARED `.eye-badge` (webplay.css); `.evo-eye` is only this
   game's hook, named in ghostStripClasses, and carries no geometry, so the
   mark cannot drift from the one every other game uses. */
.evo-badged { position: relative; }
/* Results: ONE best hand per player (no three-hand table like the siblings).
   Five cards stay on one row. The lifted Evolution dialog gets its own wider
   cap below; a phone uses smaller cards rather than horizontal overflow. */
#result-panel.evo-result-panel.result-overlay { width: min(96vw, 880px); }
.evo-result-table .result-row {
    grid-template-columns: 44px minmax(84px, 140px) minmax(0, 1fr);
}
.evo-final-hand { flex-wrap: nowrap; gap: 4px; margin-top: 4px; min-width: 0; }
.evo-final-hand .card { --cw: clamp(28px, 6vw, 60px); flex: none; }
@media (max-width: 560px) {
    .evo-result-table .result-row { grid-template-columns: 32px 68px minmax(0, 1fr); gap: 6px; }
    .evo-final-hand { gap: 3px; }
    .evo-final-hand .card { --cw: clamp(27px, 7vw, 42px); }
}
/* Match scoreboard on the progress line (the Split Court recipe): the label
   "Hand N of 4 — Round R of 7" plus one running-total chip per player. The
   shared .progress sets opacity 0.7 and a child cannot climb back out of an
   inherited opacity, so the scoreboard styling hangs off this extra class on
   the line itself. Chip chrome (.progress-score, .mine, .leading) is shared. */
.progress.evo-scoreboard {
    opacity: 1;
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    justify-content: center;
    min-height: 34px;
}
.evo-scorename { font-size: 13px; opacity: 0.75; }
.evo-scorenum {
    font-size: 18px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.progress-score.leading .evo-scorenum { color: var(--accent); }
/* Per-player deal points + running total under the hand name in the
   showdown table (dealDone), points-only at 'done'. */
.evo-result-points { font-size: 13px; opacity: 0.8; margin-top: 2px; }
/* Final match standings: total big, the per-deal breakdown muted beside it. */
.evo-standings-table { margin-bottom: 14px; }
.evo-standings-table .result-row {
    grid-template-columns: 44px minmax(84px, 140px) minmax(0, 1fr);
}
.evo-standings-total { font-size: 22px; font-weight: 800; }
.evo-standings-breakdown { font-size: 13px; opacity: 0.75; }
/* Subhead between the standings and the last deal's showdown at 'done'. */
.evo-result-subhead { margin: 12px 0 6px; font-size: 15px; opacity: 0.85; }
/* Jokers-variant marker on the progress line, so mid-match (and on resume
   or online) everyone can see the variant is on. */
.evo-jokers-badge {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: 2px 8px;
    border-radius: 10px;
    border: 1px solid rgba(255, 215, 106, 0.5);
    color: #ffd76a;
    background: rgba(255, 215, 106, 0.08);
}

/* The hand rankings table inside the Cheat_Sheet (pages/evolutionpoker.mjs).
   The shared note under it states Grid Poker's Jokers, which count as 0 and
   pair with each other; this game's Jokers never pair and rank above an Ace,
   so the note is hidden here and the sheet's Variants section states them. */
.evo-rankings .rankings-note { display: none; }
/* =========================================================================
 * Dracula
 *
 * Prefix policy: every Dracula-specific class is `dracula-` (matching
 * `cascade-`, `duel-` and `ring-`). Shared classes are reused as-is and get
 * no rules here: hand cards select with `.card.sel`, the stock chip is a
 * `.pile-chip`, opponent backs are `.card.simple.back` in a `.cardrow`, the
 * bot-level control reuses `.segmented`/`.seg`, Continue/close are `.btn`,
 * and the result panel keeps the shared banner, note, and actions rows
 * around its own `table.dracula-result` ledger. Only what is genuinely new
 * lives here.
 * ========================================================================= */

/* --- board block: 3x3 coffin + score chips ------------------------------ */

/* Composite 2x2 grid, the gridpoker .board-grid technique: the 3x3 cell grid
   top-left, the three per-row score chips in a column to its RIGHT (Queen's
   lines), the three per-column score chips in a row BELOW it (King's lines).
   --dracula-cw caps the cell width against the viewport so the three card
   columns plus the row-chip column always fit; the subtracted constant
   covers everything beside the cards on the wide layout
   (page padding, the 300px log column, gaps, the ~110px chip column,
   scrollbar slack).

   THE CELL GRID IS THE CENTERED THING, AND IT DOES NOT MOVE (user feedback:
   "the coffin shouldn't move in the UI when I'm hovering over spots - center
   the card slots themselves, don't include the numbers in the centering
   logic"). It used to be `grid-template-columns: auto auto` on a
   `width: fit-content` box with `margin: auto` sides, so the chips fed the
   centering twice over: a preview relabelling a chip `19` -> `19 -> 95` grew
   the auto chip column, grew the fit-content board, and the auto margins
   re-centered it - dragging all nine cells sideways under the pointer, ~16px
   per relabel as measured. The same applied vertically, because a longer
   projected label WRAPS in the card-width column chips and grew the auto
   second row.

   Both feedback paths are cut by making every track's size independent of
   chip content:

     column 1  1fr                        empty gutter, mirrors the chips
     column 2  auto                       the cell grid: 3 * cw + 2 * gap
     column 3  minmax(chip reserve, 1fr)  the row chips, space RESERVED
     row 1     auto                       cells (fixed-size boxes)
     row 2     the column-chip reserve    fixed, so a wrap cannot grow it

   `width: 100%` (not fit-content) makes the box width definite, so the fr
   tracks divide LEFTOVER space rather than responding to content. Columns 1
   and 3 are both 1fr and get an equal share, which puts column 2 - the cell
   grid, and only it - dead center; the chips live outside the centering
   entirely. When the viewport gets too tight for an equal share, column 3
   floors at its reserve and column 1 gives way to 0: the grid then sits left
   of center, but still without ever MOVING, and nothing overflows. */
.dracula-board {
    --dracula-gap: 8px;      /* between cells, and between chips */
    --dracula-cw: min(var(--cw), calc((100vw - 560px) / 3));
    /* Reserved space for the chips, sized for the WIDEST label each track can
       ever be asked to show - measured in Chromium at this font size, not
       guessed. Row chips are nowrap, so the reserve is a width: the widest
       settled label is `30 x5 = 150` (92px, the engine's ceiling - raw 30 at
       the x5 same-suit multiplier) and the widest projected one is
       `150 -> 0 (Joker)` (112px; the reachable worst is `40 -> 0 (Joker)` at
       105px, since a previewable line is partial, but the extra 7px is free
       insurance). Column chips wrap inside one card column, so their reserve
       is a height: `40 -> 0 (Joker)` and `30 x5 = 150` both take two lines at
       40px, and 48px carries them plus slack. Should a label ever exceed a
       reserve it simply overflows into the board's own bottom margin - the
       cells still do not move, which is the property being defended. */
    --dracula-chipw: 112px;
    --dracula-chiph: 48px;
    display: grid;
    grid-template-columns: 1fr auto minmax(var(--dracula-chipw), 1fr);
    grid-template-rows: auto var(--dracula-chiph);
    gap: 10px;
    width: 100%;
    margin: 10px 0 16px;
    /* The containing block of the unseen panel, which sits out of the flow
       in the gutter's grid area (`.dracula-board > .dracula-unseen` below). */
    position: relative;
}

/* The 3x3 coffin grid itself (row-major cells 0-8, center = the Nail).
   Also reused at .mini size inside the deal-pause summary and the snapshot
   viewer, where it renders a completed coffin read-only. */
.dracula-grid {
    grid-column: 2;          /* between the empty gutter and the row chips */
    grid-row: 1;
    display: grid;
    grid-template-columns: repeat(3, var(--dracula-cw));
    gap: var(--dracula-gap);
}
.dracula-grid.mini { --dracula-cw: 46px; --dracula-gap: 5px; }
/* Coffin-view board (deal-pause summary + snapshot dialog): the SAME
   composite layout - mini grid, row chips to its right, column chips below -
   by re-declaring the two custom properties at the .dracula-grid.mini scale
   on the WRAPPER, so the chip containers pick up the mini geometry too.
   Tighter outer gap and margin: it sits inside an already-padded framed
   block. The .mini specificity also outranks the narrow-layout --dracula-cw
   overrides below, so the mini scale is viewport-independent. */
.dracula-board.mini {
    --dracula-cw: 46px;
    --dracula-gap: 5px;
    /* Read-only: the coffin view has no placement preview, so no chip is ever
       relabelled after it is painted and the row cannot be made to grow while
       anyone is looking at it. The reserve is therefore `auto` - content
       sizing, exactly the vertical layout this view has always had. Pinning it
       instead would mean reserving the mini worst case (`30 x5 = 150` wraps to
       FOUR lines at 71px in a 46px column) as permanent blank space under
       every deal summary, to defend against a change that cannot happen here.
       The horizontal reserve is inherited, and matters only as an fr floor. */
    --dracula-chiph: auto;
    gap: 8px;
    margin: 4px 0 8px;
}
/* Less the cell's 1px border on each side, so a placed card fills its cell
   exactly instead of spilling into the gap (the cascade-grid recipe). */
.dracula-grid .card { --cw: calc(var(--dracula-cw) - 2px); }

/* One fixed-size cell box per coffin position; holds a card face, or nothing.
   The border is transparent by default so the .lit highlight never shifts
   layout. */
.dracula-cell {
    width: var(--dracula-cw);
    height: calc(var(--dracula-cw) * 89 / 64);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 8px;
    border: 1px solid transparent;
}
/* A vacant cell stays visibly empty: the shared .card.empty dashed-placeholder
   recipe on the cell box (the cascade-cell.empty precedent). R9.1. */
.dracula-cell.empty {
    background: rgba(255, 255, 255, 0.05);
    border: 1px dashed rgba(255, 255, 255, 0.25);
}
/* Legal placement target while a hand card / the Joker is selected or
   dragged: the platform's `.lit` vocabulary, styled for cell boxes (the
   shared .card.lit rule only covers card elements). Gold = actionable, the
   duel-side.legal convention; the drag module additionally toggles
   .dropzone-active on the hovered zone, same as the other games. R9.5. */
.dracula-cell.lit {
    border: 1px solid var(--accent);
    background: rgba(255, 209, 102, 0.1);
    cursor: pointer;
}
.dracula-cell.lit:hover { background: rgba(255, 209, 102, 0.2); }
/* An offered cell is a focusable role="button" while a card is armed, so
   placing never needs a pointer. A div gets none of the shared
   `button:focus-visible` treatment, so it states its own - same accent, one
   step brighter than the resting .lit border, and an OUTSET outline so it
   reads distinctly from the inset .best ring. */
.dracula-cell.lit:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    background: rgba(255, 209, 102, 0.2);
}

/* Every cell of a tied-argmax row or column in a completed-coffin view: the
   deal-pause summary and the snapshot viewer ring their winning lines (user
   feedback: "circle the rows/columns"). Driven by the same boldSets() pass
   that bolds the line chips, so it reuses their `.best` modifier name. An
   INSET outline, not a border: .dracula-cell already spends its border on the
   .empty/.lit states, and outline-offset keeps the ring inside the cell box so
   it never shifts layout - a cell on both a best row and a best column simply
   gets the one ring. Live grids never carry the class. */
.dracula-cell.best {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

/* The most recently placed coffin card (state-derived from lastPlaced, at
   most one card ever carries it - R9.10). Cool blue, the duel-poker
   .last-played / Split Court set-a hue: gold already means selected/legal
   (.sel, .lit), so the last-placed marker gets its own color. Scoped to the
   live grid; snapshot/pause minis never render the class. */
.dracula-grid .card.last-placed {
    outline: 3px solid #6aa9e0;
    outline-offset: 1px;
    box-shadow: 0 0 10px rgba(106, 169, 224, 0.65);
}

/* --- score chips --------------------------------------------------------- */

/* Row chips: a 3-row grid whose rows are exactly the cell height + shared
   gap, so each chip centers on its coffin row. Column chips: the mirror
   below, one chip centered under each coffin column. */
.dracula-rowchips {
    grid-column: 3;
    grid-row: 1;
    display: grid;
    grid-template-rows: repeat(3, calc(var(--dracula-cw) * 89 / 64));
    gap: var(--dracula-gap);
    align-items: center;
    justify-items: start;
    /* Own height fixed by its own template rows (the cell height, three
       times), and align-self: start keeps row 1 sized by the cell grid alone,
       so a chip that outgrows its track overflows instead of pushing the
       cells. min-width: 0 lets the reserved track keep its size when a label
       is wider than it - the label overflows, the grid stays put. */
    align-self: start;
    min-width: 0;
}
.dracula-colchips {
    grid-column: 2;
    grid-row: 2;
    display: grid;
    grid-template-columns: repeat(3, var(--dracula-cw));
    gap: var(--dracula-gap);
    align-items: start;
    justify-items: center;
    /* Sized by the reserved row, not by its wrapped content (see
       --dracula-chiph): a projected label taking a second or third line
       overflows downward into the board's bottom margin and cannot shift the
       cell grid above it. */
    align-self: start;
    min-height: 0;
}

/* One running-score chip per line, rendering `raw xmult = total` verbatim
   from runningScores(), plus a plain-text `(Joker)` annotation on complete
   zeroed lines (R8.2, R8.4). Pill-shaped like .pile-chip but text-first. */
.dracula-chip {
    font-size: 12px;
    line-height: 1.3;
    padding: 3px 9px;
    border-radius: 8px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    opacity: 0.9;
    white-space: nowrap;
}
/* The chip's <button type="button"> form (hover title + click-to-explain):
   drop the native control chrome so it renders pixel-identical to the div
   form - the .card button-reset rationale (appearance kills the ButtonFace
   composite under the translucent pill background; the UA control font and
   text color give way to the inherited ones). font-family rather than the
   font shorthand on purpose: the shorthand's higher specificity here would
   clobber the pill's own 12px/11px sizing above and in the phone query.
   cursor signals the click affordance; keyboard focus is already covered by
   the shared button:focus-visible accent outline. .dracula-chip.best is
   class-only (higher specificity), so the emphasis survives unchanged. */
button.dracula-chip {
    appearance: none;
    -webkit-appearance: none;
    font-family: inherit;
    color: inherit;
    text-align: inherit;
    cursor: pointer;
}
/* Column chips sit under a card-width column, so let their text wrap and
   center rather than overflow (`(Joker)` takes the second line). chipText
   joins `raw xmult` and `= total` with no-break spaces, so a breakdown wraps
   only before its `=`: `15 x2` over `= 30`. */
.dracula-colchips .dracula-chip { white-space: normal; text-align: center; }
/* A coffin view's column chips (deal-pause summary, snapshot viewer) share a
   46px card column with a 5px gap, and the joined head has to fit inside it
   (user feedback: "try to fit the x2 in the same row as the number in the
   round summary"). Measured in Chromium at 11px: the widest head, `27 x15`,
   is 35.6px and `(Joker)` 34.1px, so 3px sides leave a 38px text box in a
   46px chip. At the 12px / 9px-side default the text box was 26px, which
   parted every multiplier from its number and made `0 (Joker)` a 57px chip
   in its 46px column, overlapping its neighbor. Viewport-independent, like
   the rest of the mini scale. */
.dracula-board.mini .dracula-colchips .dracula-chip {
    font-size: 11px;
    padding: 2px 3px;
}
/* Tied argmax line(s) by running total, driven by boldSets(): the R8.6
   bold-emphasis state. Accent + bold, the .result-handname.best precedent. */
.dracula-chip.best {
    color: var(--accent);
    border-color: var(--accent);
    font-weight: 800;
    opacity: 1;
}
/* PROJECTED, not settled: the chip is showing `current -> projected` for the
   placement the pointer is hovering or dragging over, which has not happened
   yet (user feedback: "show what the row/column's score would update to").
   Provisional-looking on purpose, and in a color that is neither the gold of
   selected/legal nor the blue of last-placed: a dashed cool-violet pill, so a
   preview can never be mistaken for a score that is already banked. Class-only
   like .best, so a chip that is both keeps both. */
.dracula-chip.preview {
    color: #cbb6ff;
    border: 1px dashed #b18cff;
    background: rgba(177, 140, 255, 0.12);
    font-weight: 700;
    opacity: 1;
}

/* --- explanation popover ------------------------------------------------- */

/* Score_Explanation panel: the .dracula-pause accent framing at a smaller,
   left-aligned scale - a bold title row, one line per body row, and a Close
   .btn. In-flow with NO backdrop (the live scope paints it into
   #dracula-explain right after the board; the pause/snapshot scopes append
   the same recipe inside their own containers), so grid, hand, and Joker
   interactions stay operable while it is open. The hidden state is the
   anchor's [hidden] attribute; text-align resets the centered coffin-view
   containers. */
.dracula-explain {
    margin: 0 0 14px;
    padding: 10px 14px;
    border: 1px solid var(--accent);
    border-radius: 10px;
    background: rgba(255, 209, 102, 0.08);
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    font-size: 13px;
    text-align: left;
}
.dracula-explain-title { font-weight: 700; }

/* --- small labels -------------------------------------------------------- */

/* Tiny uppercase caption (the .opponent-taken-label recipe) used above the
   joker areas, the opponent strip, and anywhere the markup needs a quiet
   heading inside the Dracula screen. */
.dracula-label {
    font-size: 11px;
    opacity: 0.65;
    text-transform: uppercase;
    letter-spacing: 0.4px;
}

/* --- jokers: the right-edge slot in the hand strip ----------------------- */

/* THE HAND STRIP: the cards centered on the hand box's own axis, and the Joker
   slot out of the flow at its right edge (user feedback: "Center the cards in
   your hand", after "keep the Joker tied to the right on my hand box, it keeps
   moving").

   ROOT CAUSE of the uncentered hand this replaces: the slot was an in-flow
   flex item with `margin-left: auto`. That pinned the Joker right, but an auto
   margin takes ALL of a flex row's free space before justify-content sees any,
   so platform.css's `#hand-cards { justify-content: center }` had nothing left
   to distribute and the hand cards packed to the LEFT. Once the viewer's Joker
   was played the slot went away and the cards jumped to the center mid-match.

   Now:
     - #hand-cards is the slot's containing block, and the slot sits at its
       top right, out of the flow;
     - the strip's inline padding is the slot's width ON BOTH SIDES, so its
       content box is centered on the box's own axis, the platform's
       justify-content centers the cards in it, and a centered card can never
       run under the slot;
     - the slot is there for the whole match (renderJokerSlot), so none of that
       changes when the Joker is played;
     - padding-bottom reserves the opponent's Joker indicator stacked under the
       own Joker, because an out-of-flow slot adds no height. The in-flow
       content is always one card tall (the hand, or .dracula-handempty), so
       the strip is always a card plus the indicator tall and the slot always
       fits inside it.
   src/shared/ui/handspace.ts reads the strip's offsetHeight, which includes
   this padding, and writes it back as min-height, which `* { box-sizing:
   border-box }` applies to the same border box, so the reservation it keeps is
   exactly this box. No min-height here, and no justify-content restated.

   Scoped through the page's .dracula-handpanel class: #hand-cards is on every
   page (ui-conventions, "Shared ids need a game class"). */
.dracula-handpanel #hand-cards {
    /* The divider's breathing room, and the slot: one card, the room, the rule. */
    --drc-slotgap: 13px;
    --drc-slotw: calc(var(--cw) + var(--drc-slotgap) + 1px);
    /* The opponent's mini Joker: 16px, the simplified atlas. */
    --drc-mini: 16px;
    --drc-indh: calc(var(--drc-mini) * 89 / 64);
    --drc-indgap: 5px;
    position: relative;
    padding-inline: var(--drc-slotw);
    padding-bottom: calc(var(--drc-indgap) + var(--drc-indh));
}
.dracula-handpanel .dracula-jokerslot {
    position: absolute;
    top: 0;
    right: 0;
    width: var(--drc-slotw);
    padding-left: var(--drc-slotgap);
    border-left: 1px solid rgba(255, 255, 255, 0.22);
    display: flex;
    flex-direction: column;
    gap: var(--drc-indgap);
}
/* An empty hand stays one card tall, so the strip keeps its height and the
   out-of-flow slot still fits inside it. */
.dracula-handpanel .dracula-handempty {
    min-height: calc(var(--cw) * 89 / 64);
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
}

/* The viewed seat's own unplayed Joker, the face in the slot. It behaves
   exactly like a hand card, so it is flagged rather than restyled: a violet
   ring, in a hue that is neither the gold of selected/legal nor the blue of
   last-placed, and the slot's divider. It carries no text label (user
   feedback: "No need to label the Joker with 'Joker'"): the card's own art
   shows it is a Joker, and cardEl's title and aria-label still name it.

   The divider lives on the SLOT, never on .card: a FLIP ghost and a drag
   ghost are clones of the FACE in document.body, and any two-class .card
   rule out-specifies the single-class .card-fly and .drag-ghost, the trap
   that made the placement flight run at 120ms. A `position` on this rule
   was that trap too: an unscoped `position: relative` (the old label's
   containing block) beat both ghosts' `position: fixed`, so dragging the
   Joker left its ghost in the body's flow 827px below the pointer and its
   placement flight ran off-screen (measured in Chromium). So nothing here
   sets a position, and the FLIP ghost also drops the class
   (ghostStripClasses in def.ts). */
.card.dracula-jokercard {
    outline: 2px solid #b18cff;
    outline-offset: 1px;
}
/* The own Joker once played: exactly the face's size, so nothing moves, and
   dashed, dim and captioned, so it can't be mistaken for a playable card
   (R4.7; the retired .dracula-joker-played recipe at hand size). */
.dracula-jokerspent {
    flex: none;
    width: var(--cw);
    height: calc(var(--cw) * 89 / 64);
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 4px;
    border: 1px dashed rgba(255, 255, 255, 0.3);
    border-radius: 6%;
    background: rgba(255, 255, 255, 0.04);
    font-size: 11px;
    line-height: 1.3;
    opacity: 0.75;
}
/* The opponent's Joker indicator, under the own Joker: the mini card, then
   the seat's name. Exactly --drc-indh tall, which is what the strip's
   padding-bottom reserves, and nowrap with an ellipsis, so a long seat name
   can't take a second line. Still to come, it's a mini Joker face; played,
   it's a dashed empty box of the same size. The state reads from the shape,
   and the tooltip says it in words. */
.dracula-oppjoker {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 3px;
    height: var(--drc-indh);
    min-width: 0;
    font-size: 11px;
    line-height: 1;
}
.dracula-oppjoker .card.dracula-oppjoker-card { --cw: var(--drc-mini); box-shadow: none; }
.dracula-oppjoker-gone {
    flex: none;
    width: var(--drc-mini);
    height: calc(var(--drc-mini) * 89 / 64);
    border: 1px dashed rgba(255, 255, 255, 0.45);
    border-radius: 6%;
}
.dracula-oppjoker-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    opacity: 0.85;
}
.dracula-oppjoker.spent .dracula-oppjoker-name { opacity: 0.55; }

/* --- unseen cards (renderUnseen) ---------------------------------------- */

/* The viewed seat's unseen cards by suit, or in deal 6 the opponent's hand.

   WHERE IT SITS. Above 950px it's beside the coffin, in the board's empty
   left gutter, so it's on screen without scrolling and adds nothing above
   the hand (at 1280x720 the hand's bottom is already below the fold). At
   950px and below the gutter can't hold it (about 118px on a 390px phone),
   so it goes under the hand panel, as a sheet the player opens (below,
   "AT 950px AND BELOW"). Those are two places in the document,
   and a stylesheet can't move an element between parents, so placeUnseen in
   def.ts moves the anchor at NARROW_QUERY, the breakpoint the log column
   already uses. These rules key on the PARENT, not on a media query, so the
   script and the stylesheet can't disagree about which layout is on. It's
   the simplest arrangement that does both: a grid over the whole play area
   would reflow every other block in it, and anchor positioning isn't in
   every browser yet.

   IN THE GUTTER it's absolutely positioned in column 1's grid area, both
   rows (the board is its containing block), so it takes no part in sizing
   any track. It can't widen the 1fr gutter or grow row 1, so the cell grid
   stays dead center and the coffin doesn't move. It hugs the cell grid's
   left edge, mirroring the row chips on the right, and is at most
   --drc-unseenw wide, less where the gutter is narrower (about 170px at a
   951px window). overflow: hidden is the last guard: whatever its content
   does, it can't spill onto the cells.

   ONE OUTER SIZE FOR THE MATCH. The heading is one nowrap line. The count is
   a sentence with a seat's name in it, so it may wrap, into a box fixed at
   two lines that a wrap can't grow. The body is fixed at four suit rows,
   which also holds deal 6's one row of up to four 34px cards. So 47 cards
   dwindling to a few, and deal 6's change of heading, never resize it. */
.dracula-unseen {
    --drc-unseenw: 240px;
    --drc-unseenline: 16px;
    --drc-unseenrow: 18px;
    --drc-unseengap: 2px;
    --drc-unseencw: 34px;
    width: min(100%, var(--drc-unseenw));
    margin: 12px auto 0;
    overflow: hidden;
    font-size: 12px;
    text-align: left;
}
/* Both lines are explicit. An absolutely positioned grid child takes an `auto`
   line as the board's padding edge, so `grid-column: 1` (that is, `1 / auto`)
   spanned the whole board, and `right: 0` put the panel at the board's right
   edge: over the row chips, and at a 951px window over coffin cells 2 and 5,
   where it took the taps meant for them (measured in Chromium). */
.dracula-board > .dracula-unseen {
    position: absolute;
    grid-column: 1 / 2;
    grid-row: 1 / 3;
    top: 0;
    right: 0;
    margin: 0;
}
/* The heading's line: the title, and in the narrow layout's sheet its Close
   beside it. Without the Close it's the title's 20px, as it always was. */
.dracula-unseen .dracula-unseenhead { display: flex; align-items: center; gap: 8px; }
.dracula-unseen .dracula-unseentitle {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0 0 2px;
    font-size: 13px;
    line-height: 18px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.dracula-unseen .dracula-unseencount {
    height: calc(2 * var(--drc-unseenline));
    line-height: var(--drc-unseenline);
    overflow: hidden;
    opacity: 0.75;
}
.dracula-unseen .dracula-unseenbody {
    height: calc(4 * var(--drc-unseenrow) + 3 * var(--drc-unseengap));
    display: flex;
    flex-direction: column;
    gap: var(--drc-unseengap);
}
/* A suit row is a .cardlabel, so it's bold, nowrap, and colored per suit by
   the platform's .cardlabel.red / .red.diamond / .club. */
.dracula-unseen .dracula-unseenrow {
    display: flex;
    /* A whole suit, `A K Q J 10 9 8 7 6 5 4 3 2` after its glyph, is about
       156px at this gap, inside the narrowest gutter's ~170px. */
    gap: 3px;
    line-height: var(--drc-unseenrow);
    overflow: hidden;
    font-variant-numeric: tabular-nums;
}
.dracula-unseen .dracula-unseensuit { flex: none; width: 14px; text-align: center; }
.dracula-unseen .dracula-unseennone { font-weight: 400; opacity: 0.55; }
.dracula-unseen .dracula-unseencards { display: flex; gap: 4px; }
.dracula-unseen .dracula-unseencards .card { --cw: var(--drc-unseencw); }

/* AT 950px AND BELOW the panel is a sheet behind a toggle (renderUnseen).
   The phone app layout zooms the play area to fit the window, and the panel
   in the flow under the hand made the whole board smaller for the match: the
   fit's zoom went from 1 to 0.90 at 390x844 and from 0.94 to 0.83 at 360x740.
   So the toggle is a head control in the hand panel, which the platform
   takes out of the flow at the right (`.panel-head > .btn.small`), so the
   head keeps its height, and the open panel carries `app-float`, which the
   frame floats over the foot of the board, outside the fit, with its Close.
   The label is capped in its own span, because overflow on the button would
   clip the touch target the platform grows with its ::after. */
/* 7em holds "Unseen cards" and "Player 2's hand" whole (74 and 82px at 12px)
   and keeps a 20-character online name's "<name>'s hand" clear of the
   centered "Your hand" at 360px; the button's title has the whole label. */
.dracula-handpanel .dracula-unseenbtnlabel {
    display: inline-block;
    max-width: 7em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
}
.dracula-handpanel .dracula-unseenbtn[aria-expanded="true"] {
    border-color: var(--accent);
    color: var(--accent);
}
/* Floating, the sheet is the frame's full width (platform.css), and the
   panel's 240px column is centered in it, its rows still left-aligned. */
@media (max-width: 950px) {
    body.app-phone .dracula-unseen.app-float { padding: 10px 14px 12px; }
    body.app-phone .dracula-unseen.app-float > * {
        width: min(100%, var(--drc-unseenw));
        margin-inline: auto;
    }
}

/* --- joker areas (RETIRED) ----------------------------------------------- */

/* Kept for the same reason the .dracula-opp rules below are: the
   #dracula-jokers anchor still exists in the page fragment, the app just
   never populates it any more (the Jokers moved into the hand panel above).
   Historic behavior: one framed area per seat, holding the owner's Joker
   face up while unplayed (R4.6) and the visibly distinct
   .dracula-joker-played placeholder after (R4.7). */
.dracula-jokers { display: flex; gap: 14px; flex-wrap: wrap; margin: 0 0 12px; }
.dracula-joker {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    padding: 8px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
}
.dracula-joker .card { --cw: 46px; }
/* "Joker played" placeholder: card-shaped, dashed and dim (the
   .evo-play-record recipe) so it cannot be mistaken for a playable card. */
.dracula-joker-played {
    width: 46px;
    height: calc(46px * 89 / 64);
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 3px;
    border: 1px dashed rgba(255, 255, 255, 0.3);
    border-radius: 6%;
    background: rgba(255, 255, 255, 0.04);
    font-size: 10px;
    line-height: 1.3;
    opacity: 0.75;
}

/* --- opponent strip ------------------------------------------------------ */

/* The opponent's concealed hand: a .dracula-label plus one .card.simple.back
   per held card (identities never rendered - R9.2), alongside the shared
   .pile-chip stock counter in the page's #pile-line. Only the row container
   is new. */
.dracula-opp {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 12px;
    flex-wrap: wrap;
}
.dracula-opp .card { --cw: 40px; }

/* --- deal-pause summary -------------------------------------------------- */

/* Painted INSTEAD of the live board while the pause flag is set (R10.1-10.5):
   an in-flow, full-width block with the completed coffin (a .dracula-grid.mini
   rebuilt from the snapshot), both seats' Deal_Scores, and a Continue .btn.
   Accent-framed like .cascade-outcome so the deal boundary reads as an event,
   not an error. */
.dracula-pause {
    margin: 10px 0 16px;
    padding: 16px 18px;
    border: 1px solid var(--accent);
    border-radius: 12px;
    background: rgba(255, 209, 102, 0.08);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    text-align: center;
}
.dracula-pause-title { font-size: 18px; font-weight: 800; }
/* Both seats' Deal_Scores, in the pause summary and the snapshot viewer. */
.dracula-pause-scores, .dracula-snapshot-scores {
    display: flex;
    gap: 20px;
    justify-content: center;
    flex-wrap: wrap;
    font-weight: 600;
}

/* --- snapshot viewer ----------------------------------------------------- */

/* Modal overlay for a past deal's recorded coffin (R10.7-10.9): opened from a
   dealScored log line, closed by its own control (and Escape), touching no
   game state. z-index 500 keeps it UNDER the pass-and-play handoff gate (600)
   - the gate's privacy cover must always win - and under the card-flight
   layers (900/1000). The backdrop may stay translucent: a snapshot is public
   information. */
.dracula-snapshot {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: rgba(0, 0, 0, 0.6);
}
/* The dialog itself: opaque felt (not the translucent --panel) so the live
   board never shows through the mini-grid it is being compared against. */
.dracula-snapshot-card {
    background: var(--felt-dark);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
    padding: 18px 22px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    max-width: calc(100vw - 32px);
}
/* Title + the explicit close control on one line: "Deal N" in the snapshot
   dialog, "Scoring help" in the cheat-sheet panel (one head-row recipe, two
   names - the pause/snapshot-scores precedent above). */
.dracula-snapshot-head, .dracula-cheat-head {
    align-self: stretch;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    font-weight: 700;
}

/* --- in-game bot-level control ------------------------------------------- */

/* Compact "Bot level" segmented control shown mid-match in vs-AI games only
   (R11.9). Reuses the shared .segmented/.seg machinery; this wrapper just
   lays the label out beside it and tightens the segments so the control
   reads as secondary chrome, not a setup screen.
   inline-flex, not flex, so the control shares ONE row with the adjacent
   .dracula-cheat anchor (user feedback: reclaim vertical space above the
   hand); vertical-align centers the two against each other, and the right
   margin is the gap between them. Hiding stays the anchor's [hidden]
   attribute in non-AI modes - its display:none is !important, so it wins
   over this display regardless of order. */
.dracula-level {
    display: inline-flex;
    vertical-align: middle;
    align-items: center;
    gap: 8px;
    margin: 0 14px 10px 0;
}
.dracula-level .seg { padding: 4px 10px; font-size: 12px; }

/* --- scoring-help cheat sheet -------------------------------------------- */

/* Toggle row beside the bot-level control, shown in every mode (R5.1); the
   .dracula-level row layout - inline-flex + vertical-align for the same
   shared-row reason, minus the right margin because this anchor ends the row
   - and the .btn inside tightens the same way .dracula-level .seg does, so
   the two read as one strip of secondary chrome. The open panel below is
   position:fixed, so it stays out of this inline flow. */
.dracula-cheat {
    display: inline-flex;
    vertical-align: middle;
    align-items: center;
    gap: 8px;
    margin: 0 0 10px;
}
.dracula-cheat .btn { padding: 4px 10px; font-size: 12px; }

/* The open sheet: one fixed, centered, self-scrolling card (inset + auto
   margins + fit-content = both-axis centering) holding all four sections.
   z-index 550 stacks it ABOVE the snapshot viewer (500) and UNDER the
   pass-and-play handoff gate (600) - the privacy cover must always win.
   Opaque felt like .dracula-snapshot-card: the game keeps rendering (and
   animating) underneath and must not bleed through the tables. No backdrop:
   play may proceed while the sheet is open. */
.dracula-cheat-panel {
    position: fixed;
    inset: 0;
    z-index: 550;
    margin: auto;
    width: fit-content;
    height: fit-content;
    max-width: min(620px, calc(100vw - 24px));
    max-height: calc(100vh - 32px);
    overflow-y: auto;
    background: var(--felt-dark);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
    padding: 16px 20px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* Value table (rank | in a row | in a column) and multiplier-tier table:
   quiet panel-border lines, no zebra - reference material, not chrome. */
.dracula-cheat-table {
    border-collapse: collapse;
    font-size: 13px;
}
.dracula-cheat-table th, .dracula-cheat-table td {
    border: 1px solid var(--panel-border);
    padding: 3px 10px;
    text-align: left;
}
/* Hearts and diamonds in the multiplier-tier Example cells; spades and clubs
   keep the inherited color. Reuses the platform's `red` modifier NAME and the
   shared --red variable (the .cardlabel.red precedent) but NOT .cardlabel
   itself, whose bold + nowrap would re-weight the reference table. Scoped to
   the table so the bare modifier cannot leak out of the cheat sheet. */
.dracula-cheat-table .red { color: var(--red); }
/* Per-suit refinement, matching the log below - see the .cardlabel block. */
.dracula-cheat-table .red.diamond { color: var(--red-diamond); }
.dracula-cheat-table .club { color: var(--blue-club); }
/* --- log suit colors ---------------------------------------------------- */
/* The engine's describeLogEvent returns PLAIN TEXT whose wording is pinned by
   dracula_engine_test.ts, so the suits are colored at render time instead:
   renderLog's appendSuitColored wraps each heart and diamond glyph in a
   `red`-modifier span and leaves spades, clubs, and all prose as bare text.
   Same convention as .dracula-cheat-table .red above - the shared --red of the
   `.cardlabel.red` precedent, the modifier NAME only, since .cardlabel's bold
   and nowrap would re-weight log prose. Scoped to #log-entries so the bare
   modifier cannot leak out of the log, and a descendant selector on purpose:
   the log is deal-grouped, so a completed deal's score line sits on a
   <summary>, and the glyphs there must color exactly like the body's. */
#log-entries .red { color: var(--red); }
/* Per-suit refinement. The id gives these (1,2,0) and (1,1,0), so they beat
   the rule above without needing !important - see the .cardlabel block for why
   `red` stays on diamonds. */
#log-entries .red.diamond { color: var(--red-diamond); }
#log-entries .club { color: var(--blue-club); }
/* --- multiplier glyph hints ---------------------------------------------- */
/* A complete line's multiplier with a compact glyph illustration of WHY it
   applies (three of a suit for x5, three color discs for x3, two of the
   paired suit for x2; nothing for x1, a Joker line, or an incomplete line).
   Lives in the Score_Explanation popover, NOT on the chips: the chips sit in
   the board's reserved --dracula-chipw / --dracula-chiph tracks, whose whole
   purpose is that chip content cannot move the coffin, and the popover has
   room to spare. The chip still surfaces the hint through its native title.
   The tier is always the engine's entry.multiplier - explain.ts's
   multiplierHint returns null rather than draw a hint the cards contradict -
   so nothing here can disagree with the score above it. */
.dracula-hint { display: inline-flex; align-items: center; gap: 6px; }
.dracula-hint-mult { font-weight: 700; }
/* The marks a touch larger than the body text and loosely tracked, so three
   discs or three pips read as a countable group at a glance. line-height 1
   keeps the taller glyphs from stretching the popover's row rhythm. */
.dracula-hint-marks {
    font-size: 15px;
    line-height: 1;
    letter-spacing: 2px;
}
.dracula-hint-marks.red { color: var(--red); }
/* Per-suit refinement (see the .cardlabel block), so a single-suit hint matches
   the log and the cheat sheet. A color-disc or rank hint carries neither
   modifier and keeps the inherited color. */
.dracula-hint-marks.red.diamond { color: var(--red-diamond); }
.dracula-hint-marks.club { color: var(--blue-club); }
/* The popover's hint row: quietly framed so it reads as an illustration of
   the multiplier sentence above it rather than another sentence. */
.dracula-explain-hint {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 2px 0 0;
}
/* --- result overview: the lines that scored, and standout scores --------- */
/* Each Deal row's cell carries the Deal_Score and the three cards of the line
   that earned it (awardedLines() in explain.ts - engine-resolved and
   cross-checked against the recorded score, so a cell either shows the true
   awarded line or no cards at all). */
/* The ledger is a REAL table, Dracula's own `table.dracula-result`, never the
   shared .result-table (ui-conventions "Score tables line up by column"; the
   .cbs-result example). That class is a grid whose rows each size their own
   tracks, and its `1fr` seat tracks stretched across the lifted dialog: the
   two seats' columns started 228px apart at 1280px (user feedback: "put the
   columns closer together and centered in the match summary"). Table layout
   sizes each column once for every row, and the table is only as wide as its
   content, so `margin: 0 auto` centers it in the dialog as a block. The dialog
   centers text and cells inherit it, so every th and td states its own
   alignment: labels and names left, numbers right. */
table.dracula-result {
    margin: 0 auto;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
}
.dracula-result th, .dracula-result td {
    padding: 3px 4px;
    vertical-align: middle;
    text-align: left;
}
.dracula-result thead th { font-weight: 700; padding-bottom: 6px; }
/* Each seat is a column pair, number then cards. The seat's name spans both
   and starts where its number column does. The 16px on the number column is
   the space between the two seats (and between the Deal label and the first
   seat), the only gap the ledger needs. */
.dracula-result .dracula-result-seathead,
.dracula-result td.dracula-result-numcell { padding-left: 12px; }
/* width: 1px keeps the number column at its content (the min-width below):
   a seat name wider than its two columns then widens the CARD column, out at
   the seat's right, instead of opening a gap in front of its numbers. */
.dracula-result td.dracula-result-numcell {
    width: 1px;
    text-align: right;
    white-space: nowrap;
}
.dracula-result td.dracula-result-cardcell { padding-left: 6px; }
.dracula-result .dracula-result-label { font-size: 13px; font-weight: 400; white-space: nowrap; }
.dracula-result tfoot th, .dracula-result tfoot td {
    border-top: 1px solid var(--panel-border);
    padding-top: 6px;
}
.dracula-result tfoot .dracula-result-label { opacity: 0.75; padding-left: 8px; }
/* A Deal_Score, and the totals under them. min-width 3ch holds three digits
   (tabular figures are all one width), so a match whose scores stay at two
   digits lays the ledger out exactly like one with an advanced 405, and the
   number column, shared by every row, keeps the card triples in one column
   either way. */
.dracula-result-num {
    display: inline-block;
    min-width: 3ch;
    font-size: 18px;
    text-align: right;
}
.dracula-result-num.dracula-result-total { font-size: 20px; font-weight: 800; }
/* Six deals x two seats = 36 faces, so the exhibit is deliberately small: the
   SIMPLIFIED atlas (.card.simple, flat rank-over-suit art) stays legible at
   this size where the full-art faces lose their pips, and a 26px triple plus
   2px gaps is 82px. One row, never wrapped, so every card column is exactly
   one triple wide. */
.dracula-result-line { gap: 2px; flex-wrap: nowrap; }
.dracula-result-line .card { --cw: 26px; box-shadow: none; }
/* Every Deal row opens its scoring explanation (user feedback: "make the rows
   in the match summary clickable as well"). The label is a real button, for
   the keyboard, stripped to read as the row's label; the whole row takes the
   pointer too. The shared button:focus-visible ring marks keyboard focus. */
.dracula-result tbody tr.dracula-result-dealrow { cursor: pointer; }
.dracula-result tbody tr.dracula-result-dealrow:hover > * {
    background: rgba(255, 255, 255, 0.05);
}
button.dracula-result-deal-btn {
    appearance: none;
    -webkit-appearance: none;
    background: none;
    border: 0;
    margin: 0;
    padding: 4px 6px 4px 2px;
    font-family: inherit;
    font-size: inherit;
    color: inherit;
    text-align: left;
    white-space: nowrap;
    border-radius: 6px;
    cursor: pointer;
    opacity: 0.8;
}
.dracula-result-caret { display: inline-block; width: 1.1em; opacity: 0.75; }
/* The open row, three ways at once so no single channel carries it: a gold
   tint across the row, the label in bold, and the caret turned down. */
.dracula-result tbody tr.dracula-result-dealrow.open > * {
    background: rgba(255, 209, 102, 0.12);
}
button.dracula-result-deal-btn.open { font-weight: 700; opacity: 1; }
/* The open Deal's explanation, under the ledger: the chip popovers'
   .dracula-explain panel, holding both seats' awarded lines side by side. */
.dracula-explain.dracula-result-explain { margin: 12px 0 0; }
.dracula-result-seats {
    align-self: stretch;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px 18px;
    margin: 2px 0 4px;
}
.dracula-result-seat {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
    min-width: 0;
}
/* In the lifted dialog, the summary scrolls and its actions do not. The
   dialog (platform.css .panel.result-overlay) is capped at min(80vh, 680px),
   and an open deal explanation is taller than what is left of that under the
   ledger, so on its own it would push Play again and Change setup below the
   dialog's fold (ui-conventions "Everything you play with is on screen").
   As a column flexbox the dialog gives the cap to the body box, which shrinks
   and scrolls, while the shell's head row and the actions row keep their
   height. Only when lifted: inline, the summary is part of the page. */
.panel.result-overlay.dracula-resultpanel {
    display: flex;
    flex-direction: column;
}
.panel.result-overlay.dracula-resultpanel > .dracula-result-body {
    flex: 0 1 auto;
    min-height: 0;
    overflow-y: auto;
}
.panel.result-overlay.dracula-resultpanel > .result-overlay-head,
.panel.result-overlay.dracula-resultpanel > .result-actions { flex: none; }
/* A top-3 Deal_Score (user feedback: "bold your highest 3 scores"): the
   RELATIVE, per-seat emphasis, now the primary one. It folds in the round-5
   absolute high-score badge - two treatments on one number read as clutter -
   so that badge is gone and the absolute fact rides the number's title
   instead (resultDealCell). Deliberately NOT the accent-gold .dracula-chip.best
   ("which line is ahead"); this is the violet standout hue, so the two can
   never be confused. Weight is not the only channel - the color and the
   number's title carry it too, for a reader who cannot resolve a font weight. */
.dracula-result-num.dracula-result-top3 {
    font-weight: 800;
    color: #dcc9ff;
}

/* --- the coffin picture in the log -------------------------------------- */

/* A scored deal's coffin under its log heading (coffinPicture in def.ts),
   with the awarded row and column ringed by logpic's `.logpic-won`. CENTERED
   in its log entry (user feedback: "Center the grid in the logs"): the
   picture is as wide as its three cards and its auto margins put it on the
   entry's axis. Only the picture moves. The log's lines and headings stay
   left-aligned (ui-conventions, "Anything whose rows have to line up").
   The rows' name and value columns are empty, so the grid's two column gaps
   sit either side of the cards and the cards are what centers.
   The cards are 4px apart instead of logpic's 2px and 3px, so a ringed cell's
   2px outline clears an unringed neighbor and two ringed neighbors' rings
   meet rather than overlap. Scoped to this game's hook, so no other game's
   picture changes. */
.logpic.dracula-logpic {
    width: fit-content;
    margin-inline: auto;
    row-gap: 4px;
}
.logpic.dracula-logpic .logpic-cards { gap: 4px; }

/* --- collapsible deal log ------------------------------------------------ */

/* The disclosure row on a scored deal is styled by the platform now, for every
   game: `details.loggroup > summary` in src/shared/styles/platform.css, built by
   the shared renderGroupedLogInto. Dracula carried that rule here for a while,
   keyed on the shared `logline` vocabulary, which made it global from a game's
   own sheet. */

/* --- narrow layouts ------------------------------------------------------ */

/* Single column: the log column is gone, so only page padding and the
   row-chip column sit beside the three card columns. */
@media (max-width: 950px) {
    .dracula-board { --dracula-cw: min(var(--cw), calc((100vw - 200px) / 3)); }
}
/* Phones: tighter gaps, smaller chips and tables, and the dialogs (snapshot
   viewer, cheat sheet) hug the screen. The chip reserves come down with the
   11px chip font, re-measured at that size: the widest row-chip label
   `150 -> 0 (Joker)` is 101px (`30 x5 = 150` 82px, `40 -> 0 (Joker)` 94px),
   and the widest column chip still takes two lines, at 35px. The gutter is the
   first thing to give way here - 1fr floors at 0 - so a narrow phone spends
   its width on cards, not on symmetry. */
@media (max-width: 560px) {
    .dracula-board {
        --dracula-gap: 5px;
        gap: 8px;
        --dracula-cw: min(var(--cw), calc((100vw - 150px) / 3));
        --dracula-chipw: 104px;
        --dracula-chiph: 42px;
    }
    .dracula-board.mini { --dracula-chiph: auto; }
    /* Four 44px cards and their gaps are 200px, and a 360px phone leaves the
       strip 306px, so each slot-width padding gets 53px at most: one card,
       7px of room, and the 1px rule make 52. */
    .dracula-handpanel #hand-cards {
        --drc-slotgap: 7px;
        /* The seat's name goes UNDER the mini card here, on a 10px line of
           its own: beside it, a 44px slot leaves 26px for the name, and
           hotseat's "Player 1's" needs 47px (measured). Stacked, it gets the
           slot's whole width. The reservation grows by that line, so the
           strip is still exactly a card plus the indicator tall. */
        --drc-indh: calc(var(--drc-mini) * 89 / 64 + 13px);
    }
    .dracula-oppjoker { flex-direction: column; gap: 2px; font-size: 10px; }
    .dracula-oppjoker-name { max-width: 100%; line-height: 11px; }
    .dracula-chip { font-size: 11px; padding: 2px 7px; }
    .dracula-snapshot { padding: 10px; }
    .dracula-snapshot-card { padding: 14px 14px; }
    .dracula-explain { padding: 8px 10px; font-size: 12px; }
    .dracula-cheat-panel {
        max-width: calc(100vw - 16px);
        max-height: calc(100vh - 16px);
        padding: 12px 12px;
    }
    .dracula-cheat-table { font-size: 12px; }
    .dracula-cheat-table th, .dracula-cheat-table td { padding: 2px 7px; }
    /* The live board's column chips are 45px wide here, so the joined head
       needs the coffin views' narrow sides too: `27 x15` at 11px plus 3px
       sides and the border is 43.6px. */
    .dracula-colchips .dracula-chip { padding: 2px 3px; }
    /* The result exhibit shrinks with the rest: 22px faces put a triple at
       70px, and the tighter seat gap keeps the Deal label and both seats on
       one line inside the 359px dialog, so the ledger keeps its two seat
       columns side by side instead of stacking them. */
    .dracula-result-line .card { --cw: 22px; }
    .dracula-result-num { font-size: 16px; }
    .dracula-result-num.dracula-result-total { font-size: 18px; }
    .dracula-result .dracula-result-seathead,
    .dracula-result td.dracula-result-numcell { padding-left: 8px; }
    .dracula-result td.dracula-result-cardcell { padding-left: 4px; }
    .dracula-result .dracula-result-label { font-size: 12px; }
    button.dracula-result-deal-btn { padding-right: 3px; }
    .dracula-result-caret { width: 1em; }
    .dracula-result thead th { font-size: 12px; }
    /* One seat's explanation above the other: two columns of ~150px would
       wrap every reason sentence to four lines. */
    .dracula-result-seats { grid-template-columns: minmax(0, 1fr); }
}
/* A 360px phone (best effort): the 390px ledger is 19px too wide for its
   331px dialog, so the faces and the seat gaps come down one more step. */
@media (max-width: 380px) {
    .dracula-result-line .card { --cw: 20px; }
    .dracula-result .dracula-result-seathead,
    .dracula-result td.dracula-result-numcell { padding-left: 6px; }
    .dracula-result td.dracula-result-cardcell { padding-left: 3px; }
    button.dracula-result-deal-btn { padding: 4px 2px 4px 0; }
    .dracula-hint-marks { font-size: 14px; letter-spacing: 1px; }
}


/* Examples: the coffin beside its score table, so the pair fills the text column (ui-conventions
   "Examples fill the text column"). The coffin is 3.54 card widths, 156px at the 44px card cap, and
   the table takes the rest of the line; a table cannot read the still's own width, so both read
   --drc-coffin. Should the coffin's script grow wider, the still shrinks its cards to stay inside
   156px rather than push the table down. A phone keeps the coffin centered over a full-width table. */
@media (min-width: 561px) {
    .cheatsheet:has(.sdg-still[data-sdg-still="drc-coffin"]) { --drc-coffin: 156px; }
    .cheatsheet .sdg-still[data-sdg-still="drc-coffin"] { float: left; width: var(--drc-coffin); margin: 10px 24px 6px 0; }
    .cheatsheet table.drc-extable { width: calc(100% - var(--drc-coffin) - 24px); margin: 10px 0 0; }
}
/* ---------- Agents of War board ----------
   Prefix policy: every Agents-of-War-specific class is aow- (matching the
   BGA client's aow- id prefix). Shared ids (hand-*, build-*, log-*, ...)
   keep their contract names. Cards are text tiles — the BGA game has no
   sprite atlas, so no image dependency. */

.aow-seat { margin: 8px 0; }
.aow-seat-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px; }
.aow-seat-name { font-weight: 700; }
.aow-chip {
    background: #123a24; border: 1px solid #2c6e49; border-radius: 999px;
    padding: 2px 10px; font-size: 0.85rem; white-space: nowrap;
}
.aow-influence { background: #1d4d31; font-weight: 700; }

.aow-zone-label { font-size: 0.8rem; opacity: 0.75; margin: 6px 0 2px; }
.aow-discard-strip, .aow-delayed { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }

.aow-board { margin: 10px 0; padding: 8px; border: 1px solid #2c6e49; border-radius: 12px; }
.aow-board-row { padding: 4px 0; }
.aow-board-own { border-top: 1px dashed #2c6e49; }
.aow-agents { display: flex; flex-wrap: wrap; gap: 8px; min-height: 64px; align-items: stretch; }
.aow-empty { opacity: 0.5; font-size: 0.9rem; padding: 18px 6px; }
/* Agents of War centers its rows: the board strips, their labels, the action
   bar and the hand all read as one column down the middle of the table rather
   than hugging the left edge. The hand rule is scoped to the panel class
   (added in pages/agentsofwar.mjs) because #hand-cards is shared with a dozen
   other games and an unscoped rule would reach every one of them. */
.aow-agents { justify-content: center; }
.aow-board-row .aow-zone-label { text-align: center; }
.aow-actions { justify-content: center; }

.aow-agent {
    display: flex; flex-direction: column; gap: 2px; text-align: left;
    background: #0f2f1d; color: inherit; border: 1px solid #2c6e49;
    border-radius: 10px; padding: 8px 10px; min-width: 118px; cursor: default;
    font: inherit; position: relative;
}
.aow-agent-name { font-weight: 700; font-size: 0.92rem; }
.aow-agent-head { display: flex; align-items: baseline; gap: 6px; }
.aow-agent-head .aow-agent-name { flex: 1 1 auto; }
.aow-power-badge {
    background: #1d4d31; border: 1px solid #7fd08f88; border-radius: 999px;
    min-width: 1.5em; text-align: center; padding: 0 5px;
    font-weight: 700; font-size: 0.9rem; flex: 0 0 auto;
}
.aow-stack-count { font-weight: 700; color: #ffd76d; font-size: 0.9rem; flex: 0 0 auto; }
.aow-agent-power { font-size: 0.85rem; }
.aow-agent-flags { font-size: 0.78rem; opacity: 0.9; display: flex; flex-wrap: wrap; gap: 4px; }
.aow-agent-flags:empty { display: none; }
.aow-flag-chip {
    background: #14381f; border: 1px solid #2c6e49; border-radius: 6px;
    padding: 0 5px; line-height: 1.4; white-space: nowrap;
}
/* Hover: the full card face floats above the in-play tile. */
.aow-peek {
    display: none; position: absolute; left: 0; bottom: calc(100% + 6px);
    z-index: 40; pointer-events: none; filter: drop-shadow(0 4px 12px #000a);
}
.aow-agent:hover .aow-peek, .aow-agent:focus-visible .aow-peek { display: block; }
/* Small card tiles (discard strips, Redacted zone) float the full face too. */
.aow-card { position: relative; }
.aow-card-small:hover > .aow-peek, .aow-card-small:focus-visible > .aow-peek { display: block; }
.aow-card-small .aow-peek .aow-card { width: 190px; font-size: 0.95rem; }
/* A preview is the whole card, so its text is never clipped. The hand tile caps
   its text at 10em to keep the hand row even; left in force here, that cap cut
   the last clause off the longest cards (Ambush's second mode). */
.aow-peek .aow-card-text { max-height: none; overflow: visible; }
/* Nor is a full-size card's: the hand and the deck-building pools. The cap cut
   the last bullet off Two Tools and Ambush in the hand, where there is no
   preview to fall back on. Only the small tiles (discard strips, the Redacted
   zone, the Replay tray) keep it, and each of those opens a preview. */
.aow-card:not(.aow-card-small) .aow-card-text { max-height: none; overflow: visible; }
/* The discard pile sits at the RIGHT edge of the seat panel, beside the log, so
   its preview opens leftward, over the panel, instead of over the log. */
.aow-discard-side .aow-peek { left: auto; right: 0; }
/* The opponent's panel is at the top of the page, where an upward preview ran
   off the top of the screen and hid the card's name and first lines. Every
   preview there opens downward, over the board: the discard pile, the Delayed
   cards and a revealed hand alike. */
#aow-opp .aow-peek { bottom: auto; top: calc(100% + 6px); }

/* A card NAME in the log: quiet prose that happens to be pressable, since hover
   does not exist on a touch screen. Italic and in the log's own color, with a
   faint dotted underline so it still reads as something you can open; a button's
   default background and padding made every name shout. The preview it opens is
   the card AS PRINTED, from the registry, with none of the damage or counters a
   board tile carries. */
.aow-log-card {
    display: inline; padding: 0; margin: 0; border: 0; border-radius: 0;
    background: none; box-shadow: none; color: inherit; font: inherit;
    font-style: italic; text-decoration: underline dotted rgba(255, 255, 255, 0.35);
    text-underline-offset: 2px; cursor: help;
}
.aow-log-card:hover, .aow-log-card:focus-visible { text-decoration-color: currentColor; }
.aow-log-card:hover > .aow-peek,
.aow-log-card:focus-visible > .aow-peek,
.aow-log-card.aow-peek-open > .aow-peek { display: block; }
.aow-log-card .aow-peek .aow-card { width: 200px; font-size: 0.92rem; font-style: normal; }
/* A chip's preview is placed against the sticky #log-panel, its nearest
   positioned ancestor. (The scrolling #log-entries must not become one, or it
   would clip the preview.) On a wide layout the log is a sidebar at the top
   right, where the upward opening ran off the screen, so there it opens to the
   LEFT of the panel, over the board. Below 950px the log sits under the play
   area and opening upward is right. */
@media (min-width: 951px) {
    .aow-log-card > .aow-peek { left: auto; right: calc(100% + 8px); bottom: auto; top: 0; }
}

/* One combat = one block: the declaration, anything played inside its windows,
   and the resolution, read together rather than as separate lines with the rest
   of the turn in between. */
.aow-log-combat { border-left: 3px solid #d8a03d; padding-left: 8px; }
.aow-log-combat-line + .aow-log-combat-line { padding-left: 10px; opacity: 0.92; }
/* A card whose interactive session is not built yet (Mass Looting). */
.aow-disabled-card { opacity: 0.45; filter: grayscale(0.6); }
/* The tilt is the independent `rotate`, not `transform`, so the board
   animation's nudges (which move with `translate`) keep it. */
.aow-exhausted { rotate: 3deg; opacity: 0.72; }
.aow-attacking { border-color: #d8a03d; box-shadow: 0 0 0 2px #d8a03d55; }
/* A committed blocker, paired with .aow-attacking so a live combat reads off the
   board: gold swung, blue stepped in front. */
.aow-blocking { border-color: #6fa8dc; box-shadow: 0 0 0 2px #6fa8dc55; }
.aow-has-ability { cursor: pointer; border-style: dashed; }
.aow-lit { cursor: pointer; border-color: #7fd08f; box-shadow: 0 0 0 2px #7fd08f44; }
.aow-sel { border-color: #ffd76d; box-shadow: 0 0 0 3px #ffd76d66; }
/* Deck-build picker: the card tiles ARE the control, so clicking one selects
   it. The chosen cards get the same green as an eligible/selected board tile. */
.aow-pickable { cursor: pointer; }
.aow-picked { border-color: #7fd08f; box-shadow: 0 0 0 3px #7fd08faa; background: #14301f; }
/* Partial Arbiter's two piles: the splitter moves cards between them, then the
   caster clicks one to take it. Boxed so "which pile" is unambiguous. */
.aow-pile {
    display: flex; flex-direction: column; gap: 4px;
    border: 1px dashed #2c6e49; border-radius: 10px; padding: 6px 8px;
}
.aow-pile-label { font-weight: 700; font-size: 0.85rem; }
.aow-pile-cards { display: flex; flex-wrap: wrap; gap: 6px; min-height: 28px; align-items: center; }
.aow-pile.aow-pickable:hover { border-style: solid; border-color: #7fd08f; background: #14301f; }
/* An Agent placed in play while its Warcry choice is still open. The rules
   resolve a Warcry with the Agent already in play, so it belongs on the board
   here even though the play has not committed yet. Dashed and translucent
   because it is not settled: canceling the choice takes it back to hand. */
.aow-entering {
    position: relative; opacity: 0.7;
    border-style: dashed; border-color: #ffd76d;
}
.aow-entering::after {
    content: 'entering play'; position: absolute; top: -8px; left: 6px;
    background: #ffd76d; color: #1a1205; border-radius: 6px;
    padding: 0 5px; font-size: 0.7rem; font-weight: 700;
}
/* Misinformer: the card the target is about to DRAW is already a legal pick, so
   it is shown alongside their hand and labeled. Without the label a drawn card
   that duplicates one already held is indistinguishable from it. */
.aow-just-drawn { position: relative; border-color: #ffd76d; box-shadow: 0 0 0 2px #ffd76d55; }
.aow-just-drawn::after {
    content: 'just drawn'; position: absolute; top: -8px; right: 6px;
    background: #ffd76d; color: #1a1205; border-radius: 6px;
    padding: 0 5px; font-size: 0.7rem; font-weight: 700;
}

.aow-card {
    display: flex; flex-direction: column; gap: 4px; text-align: left;
    background: #10231a; color: inherit; border: 1px solid #2c6e49;
    border-radius: 10px; padding: 8px 10px; width: 190px; font: inherit;
}
/* Card TYPE at a glance: an Agent stays on the board and an Order resolves and
   leaves, which is the first thing you sort a hand by; Covert (playable in a
   response window) is the second. Border and fill only, so the state rings
   (.aow-lit, .aow-sel, .aow-just-drawn) still read on top. */
.aow-type-agent { border-color: #2c6e49; background: #10231a; }
.aow-type-order { border-color: #4a5b8c; background: #161b2e; }
.aow-type-order .aow-card-power { background: #2c3a63; }
/* A darker wash, as a background IMAGE so it composes with the fill below and
   leaves box-shadow to the state rings. It used to be an inset box-shadow, which
   replaced any ring: a picked or selected Covert card showed nothing at all. */
.aow-covert { background-image: linear-gradient(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.18)); }
.aow-covert .aow-card-type { font-style: italic; }
/* The state rings restated on CARDS, after the card's own border and fill and
   the type colors above. The earlier .aow-lit/.aow-sel/.aow-picked rules have
   the same specificity and come first in this file, so .aow-card and the type
   colors silently overrode their border and fill. */
.aow-card.aow-lit { border-color: #7fd08f; box-shadow: 0 0 0 2px #7fd08f44; }
.aow-card.aow-sel { border-color: #ffd76d; box-shadow: 0 0 0 3px #ffd76d66; }
.aow-card.aow-picked { border-color: #7fd08f; box-shadow: 0 0 0 3px #7fd08faa; background-color: #14301f; }
.aow-card.aow-just-drawn { border-color: #ffd76d; box-shadow: 0 0 0 2px #ffd76d55; }
button.aow-card { cursor: pointer; }
button.aow-card:disabled { opacity: 0.55; cursor: default; }

/* Deck building: labeled rows, so the cards going INTO the deck sit apart from
   the cards still to choose from. Each row takes the action bar's full width. */
.aow-build-row { flex: 1 1 100%; text-align: center; }
.aow-build-cards {
    display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; align-items: stretch;
}
/* An open slot in a deck, sized like a small card so the row does not jump. */
.aow-build-slot {
    width: 168px; min-height: 64px; border: 1px dashed #2c6e4999; border-radius: 10px;
    display: flex; align-items: center; justify-content: center;
    font-size: 0.8rem; opacity: 0.6;
}
/* Pai Gow: the three decks side by side. The deck new picks go to is ringed. */
.aow-build-decks { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
.aow-build-decks .aow-pile { min-width: 150px; cursor: pointer; }
.aow-build-decks .aow-pile-cards { justify-content: center; }
.aow-pile.aow-pile-target { border-style: solid; border-color: #ffd76d; }
.aow-card-small { width: 132px; padding: 6px 8px; font-size: 0.85rem; }
/* Deck-building tiles print the whole card, a little smaller than the hand's. */
.aow-card-build { width: 168px; padding: 6px 8px; font-size: 0.85rem; }
.aow-card-build .aow-card-text { font-size: 0.76rem; max-height: none; overflow: visible; }
.aow-card-head { display: flex; justify-content: space-between; gap: 6px; align-items: baseline; }
.aow-card-name { font-weight: 700; }
.aow-card-power {
    background: #1d4d31; border-radius: 6px; padding: 0 6px; font-weight: 700;
}
.aow-card-type { font-size: 0.78rem; opacity: 0.8; }
.aow-card-text { font-size: 0.8rem; opacity: 0.92; max-height: 10em; overflow: hidden; }
/* One node per printed clause, so a multi-clause card reads as a list instead
   of a single run-on paragraph. */
.aow-card-line + .aow-card-line { margin-top: 0.3em; }
/* Replayable cards are not IN the hand, they are played out of the discard pile,
   so they sit in their own labeled tray under it rather than among the hand
   tiles. */
.aow-replay-tray {
    flex: 1 1 100%; margin-top: 10px; padding-top: 8px;
    border-top: 1px dashed #2c6e49; text-align: center;
}
.aow-replay-cards { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.aow-replayable { border-style: dashed; opacity: 0.92; }
.aow-replayable:hover { opacity: 1; }
.aow-delay-count { font-size: 0.8rem; font-weight: 700; color: #ffd76d; }

.aow-actions { margin: 10px 0; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.aow-hint { opacity: 0.8; font-size: 0.9rem; }
/* The laid-out bar (layoutActionBar): the controls row, then a prompt row that
   keeps one line of height when empty. The controls row is four fixed tracks,
   alternatives | main | choices | Cancel, with equal outer columns, so the
   main control (End your turn, Pass, Take the hit) sits at the same centered
   spot at the top of the bar whatever the prompt says, and the answers to a
   question (modes, targets, cards) join it in the center of the same row
   rather than adding a row below it (round 19 playtest: a choice "moved
   everything in the UI down"). The tracks have no column gap of their own: an
   empty region takes no room, and the margins below space the ones in use. The
   row keeps one button's height when empty. */
.aow-actions.aow-bar { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; align-items: stretch; }
.aow-bar-prompt {
    min-height: 1.4em; display: flex; flex-wrap: wrap; justify-content: center;
    align-items: center; gap: 4px 12px; text-align: center;
}
/* The outer tracks never shrink below their widest button: at minmax(0, 1fr)
   a wide list of choices took the row and Cancel spilled out of its 0-wide
   track over the discard pile beside the bar. */
.aow-bar-controls {
    display: grid; grid-template-columns: minmax(min-content, 1fr) auto auto minmax(min-content, 1fr);
    align-items: start; gap: 8px 0; min-height: 2.4em;
}
.aow-bar-alt, .aow-bar-main, .aow-bar-back, .aow-bar-choices { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.aow-bar-alt { grid-column: 1; justify-content: flex-end; margin-right: 8px; }
.aow-bar-main { grid-column: 2; justify-content: center; }
.aow-bar-choices { grid-column: 3; justify-content: center; }
.aow-bar-main:not(:empty) + .aow-bar-choices:not(:empty) { margin-left: 8px; }
.aow-bar-back { grid-column: 4; justify-content: flex-start; margin-left: 8px; }
.aow-kept-deck { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
@media (max-width: 560px) {
    .aow-bar-controls { grid-template-columns: minmax(0, 1fr); }
    .aow-bar-alt, .aow-bar-main, .aow-bar-choices, .aow-bar-back { grid-column: 1; margin: 0; }
    .aow-bar-main:not(:empty) + .aow-bar-choices:not(:empty) { margin: 0; }
    .aow-bar-main { order: -1; }
    .aow-bar-alt, .aow-bar-back { justify-content: center; }
}

/* The damage division: one line per recipient, laid out as a grid with FIXED
   tracks so every line's steppers, value and outcome sit in the same columns
   (each .aow-split-row is display: contents, so its cells are grid items of
   the one grid). Every control is always rendered, disabled when it does not
   apply, so nothing shifts under the pointer between clicks. */
.aow-split {
    flex: 1 1 100%; display: flex; flex-direction: column; align-items: center; gap: 8px;
    padding: 8px 10px; border: 1px solid #2c6e49; border-radius: 10px; background: #0f2f1d;
}
.aow-split-head { display: flex; gap: 12px; align-items: baseline; }
.aow-split-total { font-weight: 700; }
.aow-split-left { color: #ffd76d; font-variant-numeric: tabular-nums; white-space: nowrap; }
.aow-split-left.aow-split-done { color: #7fd08f; }
.aow-split-rows {
    display: grid; align-items: center; gap: 6px 8px;
    grid-template-columns: 11em 2.2em 2.4em 2em 2.4em 7.2em 6.4em;
}
.aow-split-row { display: contents; }
.aow-split-name { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.aow-split-prevented .aow-split-name { opacity: 0.6; }
.aow-split-value { text-align: center; font-weight: 700; font-variant-numeric: tabular-nums; }
.aow-split-step { padding: 2px 0; min-width: 0; width: 100%; }
.aow-split-kill { white-space: nowrap; width: 100%; }
.aow-split-outcome { white-space: nowrap; font-size: 0.9rem; opacity: 0.85; }
.aow-split-outcome.aow-split-dies { color: #e88b8b; font-weight: 700; opacity: 1; }
.aow-split-controls { display: flex; gap: 8px; justify-content: center; }
@media (max-width: 560px) {
    .aow-split-rows { grid-template-columns: minmax(6em, 1fr) 2em 2.2em 1.8em 2.2em 6.4em; }
    .aow-split-outcome { grid-column: 1 / -1; margin: -4px 0 4px; text-align: center; }
}
/* Board chips for a division being assigned, and Tactician's restrictions. */
.aow-flag-chip.aow-chip-assigned { border-color: #d8a03d; color: #ffd76d; }
.aow-flag-chip.aow-chip-lethal { border-color: #e88b8b; color: #ffb3b3; }
.aow-flag-chip.aow-chip-restricted { border-color: #6fa8dc; color: #b9d7f2; }
/* The verdict on #status-line. Scoped through a class the renderer adds, since
   #status-line is on every page. Large and colored, because who won is the one
   line nobody should have to hunt for. */
.aow-outcome { font-size: 1.6rem; font-weight: 800; line-height: 1.25; padding: 6px 0; }
.aow-outcome-win { color: #7fd08f; }
.aow-outcome-lose { color: #e88b8b; }
.aow-redacted { margin-top: 10px; padding-top: 6px; border-top: 1px dashed #2c6e49; }
/* The finished game's panel (the shell lifts it into a modal, and appends its
   own Review game button to the actions row). */
.aow-result-note { text-align: center; margin: 6px 0 0; opacity: 0.9; }

/* Seat panel: the discard pile rides on the RIGHT, faded and collapsed to
   its top card so it never competes with the board for attention. */
.aow-seat-flex { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.aow-seat-main { flex: 1 1 auto; min-width: 0; }
.aow-discard-side { flex: 0 0 auto; max-width: 300px; text-align: right; }
.aow-discard-side .aow-discard-strip { justify-content: flex-end; }
/* The action bar and the viewer's discard pile share one row. The bar takes
   the room left of the pile; once that is under 240px (a phone) the pile
   wraps onto its own line, still at the right. The row's height is the taller
   of the two, so a bar that changes between phases mostly does not move the
   hand below it. */
.aow-action-row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px 12px; margin: 10px 0; }
.aow-action-row .aow-actions { flex: 1 1 240px; min-width: 0; margin: 0; }
.aow-action-row .aow-discard-side { margin-left: auto; }
/* An empty pile keeps the height of one card, so the first discard does not
   grow the panel. */
.aow-zone-toggle.aow-zone-empty { display: inline-block; cursor: default; opacity: 0.5; }
.aow-discard-empty { min-height: 58px; }
.aow-faded { opacity: 0.55; transition: opacity 0.15s ease; }
/* The faded opacity makes the strip its own stacking context, which trapped the
   card preview's z-index inside it: the opponent's preview opens down over the
   board, and the board's Agents, later in the page, painted over it. While in
   use the strip is lifted above the board and fully opaque, so the preview's
   text reads cleanly. */
.aow-faded:hover, .aow-faded:focus-within { opacity: 1; position: relative; z-index: 5; }
.aow-zone-toggle {
    background: none; border: 1px solid #2c6e49; border-radius: 6px;
    color: inherit; font: inherit; font-size: 0.8rem; padding: 2px 8px;
    cursor: pointer; opacity: 0.8;
}
.aow-zone-toggle:hover { opacity: 1; }
.aow-clickable { cursor: pointer; }
.aow-limited {
    align-self: flex-start; font-size: 0.72rem; color: #d8a03d;
    border: 1px solid #d8a03d66; border-radius: 6px; padding: 0 5px;
    cursor: help;
}

/* Board animation (fx.ts): what an ability or a card was aimed at, who fought
   whom, and what it cost. Rings and nudges are classes on the repainted tile.
   Floating numbers and the ghost of an Agent that died are overlays on
   <body>, so the next repaint does not cut them off. Nudges and the ghost's
   shrink use the independent `translate` and `scale` properties, so an
   Exhausted tile keeps its tilt. */
.aow-agent.aow-fx-source { animation: aow-fx-source 900ms ease-out; }
.aow-agent.aow-fx-target, .aow-seat-name.aow-fx-target { animation: aow-fx-target 900ms ease-out; }
.aow-seat-name.aow-fx-target { border-radius: 6px; }
@keyframes aow-fx-source {
    0% { box-shadow: 0 0 0 0 #7fd0ffcc; }
    60% { box-shadow: 0 0 0 8px #7fd0ff00; }
    100% { box-shadow: 0 0 0 0 #7fd0ff00; }
}
@keyframes aow-fx-target {
    0%, 35% { box-shadow: 0 0 0 3px #ff6a4d; }
    100% { box-shadow: 0 0 0 12px #ff6a4d00; }
}
.aow-agent.aow-fx-lunge-up { animation: aow-fx-lunge-up 520ms ease-out; }
.aow-agent.aow-fx-lunge-down { animation: aow-fx-lunge-down 520ms ease-out; }
@keyframes aow-fx-lunge-up { 0%, 100% { translate: 0 0; } 40% { translate: 0 -14px; } }
@keyframes aow-fx-lunge-down { 0%, 100% { translate: 0 0; } 40% { translate: 0 14px; } }
.aow-agent.aow-fx-clash, .aow-agent.aow-fx-hit { animation: aow-fx-shake 520ms ease-in-out; }
@keyframes aow-fx-shake {
    0%, 100% { translate: 0 0; }
    20% { translate: -5px 0; }
    40% { translate: 5px 0; }
    60% { translate: -3px 0; }
    80% { translate: 3px 0; }
}
.aow-influence.aow-fx-loss { animation: aow-fx-loss 900ms ease-out; }
@keyframes aow-fx-loss { 0%, 40% { background: #a3322a; } 100% { background: #1d4d31; } }
.aow-fx-float, .aow-fx-ghost { position: absolute; pointer-events: none; z-index: 60; }
.aow-fx-float { display: flex; justify-content: center; align-items: flex-start; height: 0 !important; }
.aow-fx-float > span {
    font-weight: 700; font-size: 1rem; padding: 0 6px; border-radius: 6px;
    background: #0b1f14e6; white-space: nowrap;
    animation: aow-fx-float 1300ms ease-out forwards;
}
.aow-fx-float-damage > span, .aow-fx-float-loss > span { color: #ff8a73; }
.aow-fx-float-gain > span { color: #7fd08f; }
/* The overlay is a zero-height line at the top edge of what it labels, and the
   number rises from just above it, so it never covers the chip or the tile. */
@keyframes aow-fx-float {
    0% { translate: 0 -100%; opacity: 0; }
    15% { opacity: 1; }
    100% { translate: 0 calc(-100% - 28px); opacity: 0; }
}
.aow-fx-ghost { animation: aow-fx-ghost 800ms ease-in forwards; }
.aow-fx-ghost > .aow-agent { width: 100%; height: 100%; box-sizing: border-box; margin: 0; }
.aow-fx-ghost.aow-fx-died > .aow-agent { border-color: #ff6a4d; background: #3a1410; }
.aow-fx-ghost .aow-peek { display: none; }
@keyframes aow-fx-ghost { 0% { opacity: 1; scale: 1; } 100% { opacity: 0; scale: 0.8; } }
body.motion-off .aow-fx-source, body.motion-off .aow-fx-target, body.motion-off .aow-fx-lunge-up,
body.motion-off .aow-fx-lunge-down, body.motion-off .aow-fx-clash, body.motion-off .aow-fx-hit,
body.motion-off .aow-fx-loss { animation: none; }
body.motion-off .aow-fx-float, body.motion-off .aow-fx-ghost { display: none; }
/* The opponent's Order, shown over the middle of the board, then sent to their
   discard pile (--fx-dx / --fx-dy, set by fx.ts; unset, it fades in place). */
.aow-fx-played {
    position: absolute; pointer-events: none; z-index: 61;
    display: flex; justify-content: center; align-items: center;
    animation: aow-fx-played 2000ms ease-in-out forwards;
}
.aow-fx-played > .aow-card {
    width: 220px; font-size: 1rem; box-shadow: 0 10px 28px #000a; border-width: 2px;
}
@keyframes aow-fx-played {
    0% { opacity: 0; scale: 0.85; translate: 0 0; }
    12% { opacity: 1; scale: 1; translate: 0 0; }
    70% { opacity: 1; scale: 1; translate: 0 0; }
    100% { opacity: 0; scale: 0.3; translate: var(--fx-dx, 0) var(--fx-dy, 0); }
}
body.motion-off .aow-fx-played { display: none; }
/* Duel Poker: the leading side's caption marker. */
.duel-side-lead { color: var(--accent); font-weight: 700; }

/* Touch targets: on coarse pointers every routinely tapped control gets a
   modest minimum via padding, without inflating the desktop density. The
   chrome buttons used to demand 44px and the segmented options 40px, which
   read as oversized next to 20px titles and 13px labels ("the About and
   Contact buttons are too tall"); 38/34 keeps a comfortable tap while
   letting the chrome sit flat. */
@media (pointer: coarse) {
    .topbar .btn { min-height: 38px; }
    .btn.small, #hand-sort { min-height: 36px; padding-top: 5px; padding-bottom: 5px; }
    .linebtn { min-width: 44px; min-height: 44px; }
    .pairbtn { min-width: 44px; min-height: 44px; }
    .seg { min-height: 34px; }
}

/* Duel Poker on short viewports (laptops at ~900px): tighten the board so
   the hand row clears the fold - slightly smaller cards, tighter gaps. */
@media (max-height: 900px) {
    .duel-board {
        --duel-gap: 4px;
        --duel-center-gap: 8px;
        gap: 6px;
        margin: 6px auto 10px;
    }
}

/* The card width, from the width of the board's own container.
 *
 * This used to be four viewport formulas, `(100vw - 444px) / 9` and variants
 * for <=950px, <=560px and short screens, each reconstructing "everything that
 * is not a card" from outside: page padding, the 16px gap, the 300px log
 * column, a guess at scrollbar slack. Each was a separate chance to be wrong,
 * and the short-screen one was: it carried a desktop constant, sat later in the
 * sheet than the width breakpoints, and so won at every width, which drew cards
 * 0px wide on a phone under 900px tall. Container queries replaced them. The
 * formulas then stayed on as a fallback for browsers without container
 * queries, which by now means Safari 15 and older, and on those the fallback
 * brought the 0px bug back with it. So there is no fallback.
 *
 * 100cqw is the width available to the board, already accounting for the
 * one-column reflow, the page cap, and the log column. What is left to subtract
 * is what the board itself adds, which the sheet knows exactly: 6 within-side
 * gaps (3 per side), the 2 gaps flanking the shared center column, and 16px of
 * .duel-side chrome (2 sides x (3px padding + 1px border) x 2 edges). Because
 * it reads the gap variables rather than their values, the phone and
 * short-screen gap changes feed through on their own; only the short-screen 0.9
 * cap needs restating.
 *
 * The container is #board, which carries no styling of its own. It cannot be
 * .duel-board itself: that is `width: fit-content`, and inline-size containment
 * on a shrink-to-fit box collapses it.
 *
 * THIS RULE REACHES ALL TEN PAGES WITH A #board, deliberately, and it is the one
 * shared-id rule in this file that is allowed to. It has been unscoped since it
 * was written, and every one of those boards is laid out with it: layout
 * containment stops child margins collapsing through #board. Scoping it to
 * `#board:has(> .duel-board)` was tried and measured (October 2026, Chromium,
 * 390px and 1280px): Bonanza, Cascade, Evolution Poker, Exchange Poker, Grid
 * Poker, Ring Poker, and Whiskey Poker each changed, the board moving up 6px to
 * 10px or #board losing 14px to 26px of height. Only Polarite and Saint Poker
 * were unaffected. So scoping it is a spacing change to seven games and needs
 * deciding per game, with eyes on a browser, not as a side effect of this one.
 * The containment also makes #board the containing block for position:fixed
 * descendants. None of the ten pages renders one inside it (checked in play,
 * same pass); a game that adds one has to put it outside #board.
 * The same sizing is available to cascade and polarite, which also sit in
 * #board; dracula, sono and englishman would each need a wrapper first. */
#board { container-type: inline-size; }
.duel-board {
    --duel-slack: calc(6 * var(--duel-gap) + 2 * var(--duel-center-gap) + 16px);
    --duel-cw: min(var(--cw), calc((100cqw - var(--duel-slack)) / 9));
}
@media (max-height: 900px) {
    .duel-board {
        --duel-cw: min(calc(var(--cw) * 0.9), calc((100cqw - var(--duel-slack)) / 9));
    }
}
/* One step more at 760px and under, which is the app layout's 1280x720 frame:
   five rows of cards plus the hand are 23px past the play area at its 0.8
   zoom floor with the 0.9 step alone. */
@media (max-height: 760px) {
    .duel-board {
        --duel-cw: min(calc(var(--cw) * 0.85), calc((100cqw - var(--duel-slack)) / 9));
    }
}
/* Whole-pixel snap, for the atlas reason and with the @supports guard given on
   --cw in platform.css's :root. */
@supports (width: round(down, 1.5px, 1px)) {
    .duel-board {
        --duel-cw: round(down, min(var(--cw), (100cqw - var(--duel-slack)) / 9), 1px);
    }
    @media (max-height: 900px) {
        .duel-board {
            --duel-cw: round(down, min(var(--cw) * 0.9, (100cqw - var(--duel-slack)) / 9), 1px);
        }
    }
    @media (max-height: 760px) {
        .duel-board {
            --duel-cw: round(down, min(var(--cw) * 0.85, (100cqw - var(--duel-slack)) / 9), 1px);
        }
    }
}
/* =========================================================================
 * Durak and Transfer Durak
 *
 * Prefix policy: every class here is `durak-`, and BOTH titles use them - the
 * two games share one renderer (src/shared/durak/ui.ts), so they share one
 * stylesheet section too. Only their element ids are per-game, and ids carry no
 * styling.
 *
 * Shared classes are reused as-is and get no rules here: hand cards select with
 * `.card.sel`, opponent backs are `.card.simple.back` in a `.cardrow`, the
 * out-of-play counter is a `.pile-chip`, the seat panels sit on `.panel`'s
 * palette variables, the controls are `.btn`, the result panel is the shared
 * `.result-*` family, and the bot-level control is the shell's own
 * `.segmented`/`.seg`. Only what is genuinely new lives here.
 * ========================================================================= */

/* --- talon: the face-up trump crosswise under the face-down pack ---------- */

.durak-talon {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 0 0 12px;
    flex-wrap: wrap;
}

/* The stack is a fixed box the two cards are positioned inside, so the
   sideways trump and the upright pack overlap the way they do on a table. The
   box is sized from the trump card's ROTATED footprint (a card is 64:89, so
   turning it sideways swaps the two). */
.durak-talon-stack {
    --durak-cw: 52px;
    position: relative;
    width: calc(var(--durak-cw) * 89 / 64);
    height: calc(var(--durak-cw) * 89 / 64);
    flex: none;
}
.durak-talon-stack .card { --cw: var(--durak-cw); position: absolute; }

/* The trump card, turned sideways and centered: its rank stays readable. */
.durak-trump-card {
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(90deg);
}

/* The pack, squared up on top of it and nudged up so the trump shows below. */
.durak-talon-back {
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
}

.durak-talon-label { font-size: 13px; opacity: 0.8; }

/* --- the other seats ------------------------------------------------------ */

.durak-table {
    display: flex;
    gap: 10px;
    margin: 0 0 12px;
    flex-wrap: wrap;
}

.durak-seat {
    padding: 8px 12px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    min-width: 132px;
}
/* The seat on turn, and the two roles of the bout in progress. The defender is
   the one under pressure, so it gets the warmer marker. */
.durak-seat-turn { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.durak-defender { background: rgba(255, 209, 102, 0.1); }
.durak-seat-out { opacity: 0.5; }

.durak-seat-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
}
.durak-seat-name { font-weight: 700; }
.durak-seat-role {
    font-size: 11px;
    opacity: 0.7;
    text-transform: uppercase;
    letter-spacing: 0.4px;
}
.durak-seat-count { font-size: 13px; opacity: 0.75; }
.durak-seat-empty { font-size: 13px; opacity: 0.6; }

/* Concealed hands: backs only, tight enough that six of them fit a seat panel
   at any table size. Identities are never rendered here. */
.durak-seat-cards { flex-wrap: wrap; gap: 3px; margin: 5px 0 3px; }
.durak-seat-cards .card { --cw: 26px; }

/* Public knowledge: the cards this seat was SEEN to pick up off the table. Not
   a leak - everybody watched them go - and the most interesting thing on the
   screen once a bout has been taken. */
.durak-seat-known { margin-top: 5px; }
.durak-seat-known-label {
    display: block;
    font-size: 11px;
    opacity: 0.65;
    text-transform: uppercase;
    letter-spacing: 0.4px;
}
.durak-seat-known .cardrow.mini { gap: 3px; margin-top: 3px; }
.durak-seat-known .card { --cw: 30px; }

/* --- the bout ------------------------------------------------------------- */

.durak-battle { margin: 0 0 12px; }
.durak-battle-caption { font-size: 13px; opacity: 0.85; margin-bottom: 6px; }
.durak-battle-empty { font-size: 13px; opacity: 0.65; }

.durak-battle-row {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    flex-wrap: wrap;
    min-height: calc(var(--cw) * 89 / 64 + 18px);
}

/* One column per attack card. The card that beat it is laid ACROSS it, offset
   so both ranks read - the same thing a player does with their hands, and the
   reason the cell is a positioned box a little larger than one card. */
.durak-pair {
    --durak-offset: 18px;
    position: relative;
    width: calc(var(--cw) + var(--durak-offset));
    height: calc(var(--cw) * 89 / 64 + var(--durak-offset));
    border-radius: 8px;
    flex: none;
}
.durak-pair .card { position: absolute; top: 0; left: 0; }
.durak-beat-card {
    top: var(--durak-offset);
    left: var(--durak-offset);
    transform: rotate(12deg);
}

/* An attack card still waiting for an answer. */
.durak-standing .durak-attack-card {
    outline: 2px solid rgba(255, 255, 255, 0.5);
    outline-offset: 1px;
}
/* A card is armed and this attack card is one it could beat: tap to pair them.
   The lit set and the accepted set are one array, computed once per repaint. */
.durak-pair.lit {
    cursor: pointer;
    outline: 3px solid var(--accent);
    outline-offset: 3px;
    background: rgba(255, 209, 102, 0.12);
}
.durak-pair.lit:hover { background: rgba(255, 209, 102, 0.2); }

/* --- contextual controls -------------------------------------------------- */

.durak-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 12px;
    flex-wrap: wrap;
}
.durak-actions-hint { font-size: 13px; opacity: 0.8; }

/* --- own hand ------------------------------------------------------------- */

/* Trumps read as a group at the right-hand end of the sorted hand, so the
   reserve you are saving for the endgame is obvious at a glance. */
.durak-trump { box-shadow: 0 0 0 2px #ffd166, 0 6px 14px rgba(0, 0, 0, 0.45); }
/* A card with nothing legal to do right now: visibly out of play, and it
   carries no handlers either. */
.durak-idle { opacity: 0.45; }

/* Narrow screens: shrink the bout and the seat strip before the hand row, which
   is the thing the player is actually reading. */
@media (max-width: 820px) {
    .durak-pair { --durak-offset: 12px; }
    .durak-pair .card { --cw: 54px; }
    .durak-talon-stack { --durak-cw: 44px; }
    .durak-seat { min-width: 112px; }
}

/* ---------- Casino board ----------
 *
 * Layout, top to bottom: score chips (+ scoring-help toggle), the
 * opponent strip, the last-move strip, the table (loose cards + builds),
 * the armed card's action bar, the shared hand panel, capture piles, and
 * the round-score / scoring-help overlays. Everything casino- prefixed;
 * the armed hand card reuses the global .card.sel; point badges ride on
 * .casino-hasbadge cards (position scoped here, not on the shared .card).
 */

.casino-score { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 4px 0; }
.casino-score-chip {
    display: inline-flex; align-items: center; gap: 7px; padding: 3px 11px;
    border-radius: 14px; background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.15);
}
.casino-score-chip.active { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.casino-score-name { font-weight: 600; }
.casino-score-pts { font-weight: 700; font-size: 17px; }
.casino-score-goal { font-size: 12px; opacity: 0.7; }

#casino-help { display: inline-block; margin: 2px 0 6px; }

.casino-opp { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-height: 44px; margin: 4px 0; }
.casino-opp-name { font-size: 13px; opacity: 0.85; }
.casino-opp-cards { display: flex; gap: 4px; }
.casino-opp-cards .card { --cw: 30px; }

/* The status line sits above the whole board, so its line count is reserved:
   one line on a laptop (the platform's own .status floor), two on a phone,
   where "7♠ picked: tap a lit table card, or 7♠ again to take 2 cards." wraps
   (the draftpoker-status precedent). */
@media (max-width: 560px) {
    .status.casino-status { min-height: 2.8em; }
}

/* The last play. Never hidden, always ONE row: an empty strip keeps the
   height (only its frame goes), and the row scrolls rather than wraps, so the
   table under it never moves. Top padding leaves room for the point badges,
   which sit 7px above a card and would otherwise be clipped by the scroll box.
   Cards here are 34px, so they use the simplified atlas (ui-conventions). */
.casino-lastmove {
    --casino-lm-cw: 34px;
    display: flex; align-items: center; gap: 6px; flex-wrap: nowrap;
    overflow-x: auto; overflow-y: hidden;
    padding: 9px 10px 5px; margin: 6px 0; font-size: 13px; border-radius: 10px;
    min-height: calc(var(--casino-lm-cw) * 89 / 64 + 16px);
    background: rgba(0, 0, 0, 0.25); border: 1px solid rgba(255, 255, 255, 0.12);
}
.casino-lastmove.empty { background: transparent; border-color: transparent; }
.casino-lastmove .card { --cw: var(--casino-lm-cw); flex: none; }
.casino-lastmove-text { opacity: 0.9; white-space: nowrap; }
/* The captured cards overlap like a fan: each card's index corner stays
   readable, and a big take still fits one row on a phone. */
.casino-lastmove-fan { display: flex; flex: none; padding-left: 14px; }
.casino-lastmove-fan .card { margin-left: -14px; }
.casino-sweepnote { color: var(--accent); font-weight: 700; white-space: nowrap; }

/* The table reserves one row on a laptop and two on a phone, where 5 cards
   fill a row. A build is a little taller than a card, so the row is sized
   for the build. The runtime mark in def.ts (reserveTallest) holds anything
   taller for the rest of the round. */
.casino-table {
    --casino-table-cw: 64px;
    display: flex; align-items: flex-start; justify-content: center;
    gap: 12px; flex-wrap: wrap;
    padding: 14px; margin: 6px 0; border-radius: 14px;
    min-height: calc(100px + 28px + 2px);
    background: rgba(0, 0, 0, 0.18); border: 1px solid rgba(255, 255, 255, 0.12);
}
.casino-table .card { --cw: var(--casino-table-cw); }
/* Lit: a tap on it captures with the picked card. Picked: the outlined take a
   second tap on the hand card makes (or a build pick, or the card tapped
   first). Outline and lift only, never a layout change. */
.casino-table .card.lit { outline: 2px dashed rgba(255, 209, 102, 0.85); outline-offset: 2px; }
.casino-table .card.picked {
    outline: 3px solid var(--accent); outline-offset: 1px;
    transform: translateY(-4px);
}
.casino-empty { opacity: 0.6; font-size: 14px; align-self: center; }

.casino-build {
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    padding: 10px 10px 6px; border-radius: 12px; position: relative;
    background: rgba(255, 255, 255, 0.06);
    border: 2px solid rgba(255, 255, 255, 0.28);
    appearance: none; -webkit-appearance: none; font: inherit; color: inherit;
}
button.casino-build { cursor: pointer; }
button.casino-build:hover { border-color: rgba(255, 255, 255, 0.55); }
.casino-build.own { border-color: #7bd88f; }
.casino-build.lit { border-style: dashed; border-color: rgba(255, 209, 102, 0.85); }
.casino-build.picked { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
.casino-build-cards { display: flex; padding-left: 16px; }
.casino-build-cards .card { --cw: 40px; margin-left: -16px; }
.casino-build-chip {
    position: absolute; top: -10px; right: -8px; width: 26px; height: 26px;
    display: flex; align-items: center; justify-content: center;
    background: var(--accent); color: #3a2c00; font-weight: 800;
    font-size: 13px; border-radius: 50%; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}
.casino-build-owner { font-size: 11px; opacity: 0.75; }

/* The picked card's plays, directly above the hand. NEVER hidden and never
   resized by a pick: it reserves one row of buttons on a laptop and two on a
   phone (where Capture + Trail + the cancel already wrap), and an idle bar
   only drops its frame. Without this the hand dropped ~90px under the finger
   on every pick. A button is its line (1.4em) plus 7px padding and a 1px
   border each side; the bar adds 8px padding and a 1px border each side. The
   runtime mark (reserveTallest) holds any taller row for the match. */
.casino-actions {
    --casino-btn-h: calc(1.4em + 16px);
    display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
    justify-content: center; align-content: flex-start;
    padding: 8px 10px; margin: 6px 0; border-radius: 12px;
    min-height: calc(var(--casino-btn-h) + 18px);
    background: rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 209, 102, 0.35);
}
.casino-actions.idle { background: transparent; border-color: transparent; }

.casino-dead { opacity: 0.35; filter: grayscale(0.6); }

.casino-piles { display: flex; gap: 12px; flex-wrap: wrap; margin: 8px 0; }
.casino-pile {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    font-size: 13px; padding: 6px 10px; border-radius: 10px;
    background: rgba(255, 255, 255, 0.06);
}
.casino-pile-sweeps { display: flex; gap: 3px; }
.casino-pile-sweeps .card { --cw: 26px; }
.casino-pile-note { font-size: 11px; opacity: 0.7; }

/* Point badge on top of a card face (aces/casinos; every penalty card in
   Krypkasino, styled as a warning there).
   NOT ON A FLIGHT GHOST. The shared animator flies a CLONE of the card with
   `.card-fly { position: fixed }`, and this rule has the same specificity and
   comes later in the built stylesheet, so it used to win: every badged card's
   ghost (point cards, and every hand card wearing a capture or build icon)
   rendered in the page flow at the foot of the body instead of over the board,
   while the real card sat hidden for the flight. That was the blinking. */
.casino-hasbadge:not(.card-fly) { position: relative; }
.casino-pts {
    position: absolute; top: -7px; left: 50%; transform: translateX(-50%);
    background: #ffd166; color: #4a3600; font-size: 11px; font-weight: 800;
    line-height: 1; padding: 2px 5px; border-radius: 8px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); pointer-events: none; z-index: 2;
}
.casino-pts.bad { background: #e15b64; color: #fff; }

.casino-overlay {
    position: fixed; inset: 0; z-index: 70; padding: 16px;
    background: rgba(0, 0, 0, 0.65);
    display: flex; align-items: center; justify-content: center;
}
.casino-overlay-card {
    background: #20301f; border: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: 14px; padding: 18px 20px; width: 100%; max-width: 480px;
    max-height: 85vh; overflow: auto; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
}

.casino-scorelines { display: flex; flex-direction: column; gap: 2px; margin: 10px 0; }
.casino-scoreline {
    display: flex; gap: 10px; align-items: baseline;
    padding: 3px 6px; border-radius: 6px; font-size: 14px;
}
.casino-scoreline:nth-child(odd) { background: rgba(255, 255, 255, 0.04); }
.casino-scoreline-label { flex: 1; }
.casino-scoreline-who { min-width: 86px; opacity: 0.9; }
.casino-scoreline-pts { min-width: 44px; text-align: right; font-weight: 700; }
.casino-scoreline-pts.neg { color: #7bd88f; } /* negative is GOOD in Krypkasino */
.casino-scoreline.decided { background: rgba(255, 209, 102, 0.16); }
.casino-scoreline-mark { color: var(--accent); font-weight: 700; font-size: 12px; }
.casino-scoreline.totals {
    border-top: 1px solid rgba(255, 255, 255, 0.2);
    margin-top: 4px; padding-top: 6px; font-weight: 600;
}
.casino-help-text { font-size: 13px; opacity: 0.85; margin: 8px 0; }
.casino-error { color: #ff8a80; }
.casino-result-totals { font-size: 16px; font-weight: 600; margin: 6px 0; }
.casino-result-history {
    display: flex; flex-direction: column; gap: 2px;
    font-size: 13px; opacity: 0.8; margin: 8px 0;
}

@media (max-width: 540px) {
    .casino-table {
        --casino-table-cw: 52px;
        gap: 8px; padding: 10px;
        min-height: calc(2 * var(--casino-table-cw) * 89 / 64 + 8px + 20px + 2px);
    }
    .casino-build-cards .card { --cw: 34px; }
    .casino-opp-cards .card { --cw: 24px; }
}
@media (max-width: 560px) {
    .casino-actions { min-height: calc(2 * var(--casino-btn-h) + 8px + 18px); }
}

/* Casino log: the collapsible round itself is styled by the platform now, for
   every game (`details.loggroup` / `.loggroup-body` in
   src/shared/styles/platform.css, built by the shared renderGroupedLogInto).
   What is left here is the heading's own contents, which carry the totals and
   the Scores button that reopens that round's table: inline-flex so the pair
   sits beside the platform's caret and wraps together on a narrow panel. */
.casino-log-scored {
    display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap;
}

/* Hand-card play icons: ↓ = this card can capture (kryp: must), ☰ = it
   can build / add to a build. A card wearing neither trails on one tap. */
.casino-act {
    position: absolute; top: -7px; width: 15px; height: 15px;
    display: flex; align-items: center; justify-content: center;
    font-size: 10px; font-weight: 800; line-height: 1; border-radius: 50%;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); pointer-events: none; z-index: 2;
}
.casino-act.cap { left: -5px; background: #7bd88f; color: #0c2a14; }
.casino-act.build { right: -5px; background: #8ab4f8; color: #10233f; }

/* Two-column score table cells (one column per player). */
.casino-scorecell { min-width: 56px; text-align: right; font-weight: 700; }
.casino-scorecell.neg { color: #7bd88f; } /* negative is GOOD in Krypkasino */
.casino-scoreline.head .casino-scorecell {
    font-weight: 600; opacity: 0.85; font-size: 12px;
    text-transform: uppercase; letter-spacing: 0.4px;
}
/* =========================================================================
   Cupid
   A two-player trick game, so the board is small: the draw piles on one side,
   the trick itself on the other, and the hand panel below. The scoreboard is
   the shared .progress-score widget above (Cupid opts in with
   .cupid-scoreboard) because the race to 121 is the thing being watched.
   ========================================================================= */

/* Centered, and the columns take only the width they need (see .cupid-trick,
   which no longer grows). The board is the thing being looked at, so it sits in
   the middle of the page rather than being pushed against the left edge with the
   slack piling up on the right. Centering a wrapping row is safe here because
   every column's width is now content-driven and constant through a round: a
   column that changed width would slide its neighbors sideways. */
.cupid-board {
    display: flex;
    gap: 18px;
    align-items: flex-start;
    justify-content: center;
    flex-wrap: wrap;
    margin-bottom: 14px;
}

/* --- the draw piles: stock, turn-up, and the face-down prize trick -------- */

/* min-height is reserved so the row cannot change height over a round: the
   turn-up's caption goes "Turn-up" -> "None", and the prize swaps a chip for a
   pile, and neither may nudge the trick area beside it. */
.cupid-piles {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    flex: none;
    min-height: 100px;
    /* The padding, the border and the offsetting margin are here rather than on
       .cupid-mine below, so that the "it is your draw" ring only changes a
       COLOR. Carried by .cupid-mine they added 22px the moment a draw was owed,
       which is 3px past what min-height absorbs on a phone: the trick area and
       the hand stepped down every time the viewer took a trick. */
    padding: 10px;
    border: 1px solid transparent;
    border-radius: 14px;
    margin: -1px;
}
.cupid-pile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
}
.cupid-pile .card { --cw: 58px; }
/* The stock: the count rides on the back rather than sitting under it. */
.cupid-stock { position: relative; display: flex; }
.cupid-stock-count {
    position: absolute;
    right: -5px;
    bottom: -5px;
    min-width: 26px;
    padding: 1px 5px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    text-align: center;
    color: var(--accent-ink);
    background: var(--accent);
    border: 1px solid rgba(0, 0, 0, 0.35);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
    pointer-events: none;
}
.cupid-pile-label {
    font-size: 12px;
    opacity: 0.7;
    text-align: center;
    max-width: 108px;
    line-height: 1.3;
}
/* The stock's back is a real choice during the winner's draw, so it needs the
   same affordances a card face gets. */
.cupid-stock-back { cursor: default; }
button.cupid-stock-back { cursor: pointer; }
/* The revealed prize: two cards side by side, worth the room only once there is
   something on them to read. */
.cupid-prize .cardrow { gap: 4px; }
.cupid-prize .card { --cw: 44px; }
/* Face down it is a chip instead - see renderPiles. The ::before draws the one
   card's worth of hint the pile used to spend 90px on. */
.cupid-prize-chip {
    align-self: center;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 10px;
    border-radius: 8px;
    font-size: 12px;
    opacity: 0.7;
    cursor: help;
    background: var(--panel);
    border: 1px dashed var(--panel-border);
}
.cupid-prize-chip::before {
    content: '';
    width: 11px;
    height: 15px;
    border-radius: 2px;
    flex: none;
    background: linear-gradient(135deg, #5c6bc0, #3949ab);
    border: 1px solid rgba(255, 255, 255, 0.4);
}

/* --- the trick ----------------------------------------------------------- */

/* One slot per seat plus a caption below, so the caption gets a whole row of
   its own (flex-basis 100%) rather than a grid cell of its own. */
/* Wide enough for its two slots, and no wider. It used to be `flex: 1 1 300px`,
   which made it eat every spare pixel of the board: measured at 479px on a
   1280px viewport for a pair of cards that wanted 311px, which pushed the piles
   to the far left and the taken trick to the far right with a lake of felt in the
   middle. Now it takes a fixed share and .cupid-board centers the three columns.
   The basis is DERIVED FROM THE CARD SIZE and not from the content. `flex: 0 1
   auto` was tried first and reintroduced the movement this game keeps being bled
   by: a flex item's hypothetical width is its max-content, the widest thing in
   here is the CAPTION sentence, and that caption gains a clause once the stock
   runs out. The board wrapped the trick onto a second row when it did, moving
   the hand 232px mid-round. A width that only tracks --cw cannot do that.
   Two 52px-wide slot bodies either side of the cards, the 10px gap between the
   slots and 28px of the area's own padding come to the 170px. */
.cupid-trick {
    flex: 0 1 calc(2 * var(--cw) + 170px);
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: flex-start;
    justify-content: center;
    padding: 12px 14px;
    min-height: 172px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
/* Whose turn it is, level 1: the area the viewer owes something to. Applied to
   the trick area when a card is owed and to the piles when a draw is. */
.cupid-trick.cupid-mine,
.cupid-piles.cupid-mine {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}
.cupid-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 6px 12px 10px;
    border-radius: 12px;
    border: 1px solid transparent;
}
/* Whose turn it is, level 2: the seat that must act. The empty slot waiting for
   a card is the most spatially obvious place to say so, and it breathes so the
   eye catches it without the board becoming loud. */
.cupid-slot.cupid-turn {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.1);
    animation: cupid-turnpulse 1.6s ease-in-out infinite alternate;
}
@keyframes cupid-turnpulse {
    from { box-shadow: 0 0 0 0 rgba(255, 209, 102, 0); }
    to { box-shadow: 0 0 0 3px rgba(255, 209, 102, 0.28); }
}
body.motion-off .cupid-slot.cupid-turn {
    animation: none;
    box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.3);
}
/* The empty slot of the seat to act reads as a waiting space, not a blank. */
.cupid-slot.cupid-turn .card.empty {
    border-color: rgba(255, 209, 102, 0.6);
    background: rgba(255, 209, 102, 0.07);
}
.cupid-slot-head {
    display: flex;
    align-items: baseline;
    gap: 6px;
    font-size: 13px;
    min-height: 18px;
}
/* Whatever the seat is called, on one line and within the slot: an online
   display name long enough to wrap would widen the slot past the width the trick
   area reserves for it, and the two slots would then wrap onto separate rows. */
.cupid-slot-name {
    font-weight: 700;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.cupid-slot-role {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.65;
}
/* Whose turn it is, level 3: the chip says WHICH act is owed - lead, answer, or
   draw - so the marker carries information rather than only attention. */
.cupid-slot-role.cupid-now {
    opacity: 1;
    font-weight: 700;
    color: var(--accent-ink);
    background: var(--accent);
    border-radius: 999px;
    padding: 1px 7px;
}
/* The card that actually captured. Which one it is is the whole trick, and it
   is not always the higher-looking card - see the Anima over Cupid. */
.card.cupid-capturing { outline: 3px solid var(--accent); outline-offset: 2px; }
/* Two lines' worth reserved: the caption gains a "the stock is gone" clause for
   the last tricks of a round, and it must not grow the trick area when it does. */
.cupid-trick-caption {
    flex: 1 0 100%;
    align-self: flex-end;
    font-size: 13px;
    opacity: 0.85;
    line-height: 1.4;
    min-height: 37px;
}
/* The Monster's lead flip is easy to miss and changes whose turn it is. */
.cupid-flip { color: var(--accent); }

/* --- the trick just taken ------------------------------------------------ */

/* Set aside beside the trick area rather than inside it, and deliberately
   quieter: mini cards, a muted panel, no ring. It is reference, not the live
   position - but it stays legible, because "what did that just cost me" is the
   question a player asks straight after a trick is collected. */
.cupid-last {
    flex: 0 1 168px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 10px 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.1);
    border: 1px dashed var(--panel-border);
}
.cupid-last-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    /* One line of the points figure, reserved. Before the first trick the points
       span is empty, and an empty span contributes no line box: the head came out
       15px instead of 22px and the panel changed height on the first collection. */
    min-height: 1.5em;
}
.cupid-last-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.6;
}
.cupid-last-points { font-weight: 800; font-variant-numeric: tabular-nums; }
/* Two lines reserved: the Monster's lead flip is appended to this note rather
   than given a line of its own, so the panel keeps the same three children in
   every state and cannot grow when a Monster takes a trick. */
.cupid-last-note {
    font-size: 12px;
    opacity: 0.75;
    line-height: 1.35;
    /* THREE lines, not two. The panel is a 168px column here, and measurement put
       "AI took it, only the 5 of hearts counts" at two lines on its own and three
       once the Monster's lead flip is appended. Two was reserved and the panel
       grew by 16px whenever a Monster took a trick. The phone override below
       drops it back: the note is a full-width row there, so one line is enough
       and the second is the reservation. */
    min-height: calc(3 * 1.35em);
}
/* Before the first trick the panel holds its place rather than vanishing - it is
   a board column, and appearing later would resize the trick area beside it. */
.cupid-last-empty { opacity: 0.6; font-style: italic; }
.cupid-last .cupid-flip { font-size: 12px; line-height: 1.35; }
/* The capturing card, at mini size: a thinner ring than the full-size one so it
   does not swamp a 46px card. */
.cupid-last .card.cupid-capturing { outline-width: 2px; outline-offset: 1px; }

/* --- the prize trick, revealed in the round summary ----------------------- */

/* Full-size cards, because this is the one moment they are worth looking at: they
   were face down for the whole round and they have just decided a chunk of the
   score. Everywhere else in the summary is text. */
.cupid-prize-reveal {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px solid var(--panel-border);
}
.cupid-prize-reveal-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.6;
}
.cupid-prize-reveal-cards { gap: 8px; }
.cupid-prize-reveal-note { font-size: 13px; opacity: 0.85; }

/* --- the round overview -------------------------------------------------- */

/* The shared .result-row asks for 70px plus minmax(180px, 1fr) per seat, which
   wants 450px for Cupid's two seats. Below that, auto-fit wraps the columns onto
   extra grid rows, so a single line of the overview arrives as three. Two narrow
   numeric columns sized in em fit any width, and every label is nowrap so a long
   online display name cannot reintroduce the same wrap. */
.result-table.cupid-result { gap: 2px; }
.result-table.cupid-result .result-row {
    grid-template-columns: minmax(0, 1fr) repeat(2, minmax(3.5em, auto));
    gap: 2px 12px;
}
.result-table.cupid-result .result-label {
    text-align: left;
    white-space: nowrap;
}
.result-table.cupid-result .result-cell {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    /* A peer can pick a 20-character display name; it shortens instead of
       widening the column back past the wrap point. */
    overflow: hidden;
    text-overflow: ellipsis;
}
.result-table.cupid-result .result-row.totals .result-cell {
    font-size: 20px;
    font-weight: 800;
}

/* --- what we know about the other hand ------------------------------------ */

/* The opponent's panel joins the piles' BOARD COLUMN rather than standing above
   the board in a full-width strip of its own, which cost 146px of height and
   pushed the trick and the hand down for a seat name and at most five small
   cards. The page emits #opponents inside .cupid-side for this, with
   .cupid-opponents on it to scope the shared #opponents rule out.
   Measured widths are what decided the arrangement (tools/cupid-probe-width.mjs):
   the piles want 251px, the trick 386px and the taken trick 168px, and the board
   has 868px at a 900px viewport. Putting the panel BESIDE the piles needs about
   371px for the pair, leaving 364px, which evicts the taken trick to a second
   board row and makes the board 389px tall instead of 218px. So above the phone
   breakpoint it goes UNDER the piles instead, at the piles' width, and is kept
   one row tall (name beside cards) so the whole column stays shorter than the
   trick next to it. The panel then costs nothing at all.
   Column, not wrap: with flex-wrap the panel jumped between beside and under as
   the known cards accumulated and the row's demand grew, moving the hand 188px
   mid-match. */
.cupid-side {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 8px;
    flex: 0 0 auto;
}
/* The shared rule is a centered, full-width, 12px-margined row, and in a board
   column it needs none of that. It is written `#opponents` though, so a bare
   `.cupid-opponents` loses to it on specificity and the margin quietly stayed:
   the id has to be in the selector to beat the id. */
#opponents.cupid-opponents {
    margin-bottom: 0;
    justify-content: flex-start;
}
.cupid-opponents .opponent {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 10px;
    flex: 1 1 auto;
    min-width: 0;
}
/* Whatever the seat is called, on one line: a name that wrapped would make the
   panel two rows tall and the column taller than the trick. */
.cupid-opponents .opponent-name { white-space: nowrap; }

/* Public information, so it sits openly in the opponent's panel rather than
   being tucked away: every one of these cards was the face-up turn-up when it
   was drawn. Ordered oldest-first, and each captioned with when it was seen. */
.cupid-known {
    --cupid-knowncw: 38px;
    flex: 1 1 auto;
    min-width: 0;
}
/* The height is RESERVED, empty or not: this panel is part of a board column, so
   a strip that grew when the first card became known would resize the board. One
   card, the 2px gap under it, and its caption line. Driven off the custom
   property above so the phone breakpoint's smaller card recomputes it.
   The padding is `.cardrow.scroll`'s, and it goes: that 8px is there to clear the
   5px lift of a selected card, and nothing in this strip is selectable. */
.cardrow.scroll.cupid-known-cards {
    align-items: flex-start;
    padding: 0;
    min-height: calc(var(--cupid-knowncw) * 1.39 + 16px);
}
.cupid-known-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    flex: 0 0 auto;
}
.cupid-known .card { --cw: var(--cupid-knowncw); }
/* The "when": small, monospaced digits so a row of them lines up and reads as a
   sequence rather than as labels. */
.cupid-known-when {
    font-size: 10px;
    opacity: 0.55;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
}

/* --- the hand, when it is the viewer's move ------------------------------- */

/* Whose turn it is, level 4: the cards themselves. The status line says it in
   words, but the panel holding the thing you have to do should not look the same
   whether or not it is your move. */
#hand-panel.cupid-active {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}
#hand-panel.cupid-active #hand-title { color: var(--accent); }

/* What you just drew. The stock is face down and the hand is sorted by rank, so
   without this a draw is a card silently appearing somewhere in the row.
   A ring in --ink rather than the gold accent: gold already means "your turn"
   here, and "this card is the hint" and "this is a named card" on a card face.
   Pulsing while the mark is live, like .card.hinted, rather than a one-shot fade:
   a repaint builds fresh elements, which would restart a one-shot animation and
   read as a stutter. def.ts takes the class off after DREW_MARK_MS.
   An outline, so nothing in the hand moves for it. */
.card.cupid-drawn {
    outline: 3px solid var(--ink);
    outline-offset: 2px;
    animation: cupid-drawnpulse 0.85s ease-in-out infinite alternate;
}
@keyframes cupid-drawnpulse {
    from { outline-color: var(--ink); }
    to { outline-color: rgba(242, 239, 233, 0.15); }
}
body.motion-off .card.cupid-drawn { animation: none; }

/* A card in your own hand the other seat has seen: the site's shared eye badge
   (.eye-badge above) in the corner. `position: relative` is a TWO-class rule on
   purpose, so it out-specifies `.card-fly { position: fixed }` on an animation
   ghost - a ghost that kept it would land in document flow and flicker. def.ts
   names cupid-badged in ghostStripClasses so no ghost carries it; do not remove
   that without removing this. */
.card.cupid-badged { position: relative; }

/* --- the four named cards ------------------------------------------------ */

/* The Anima, the Monster, Cupid, and the Zephyr do not follow the ranking, so
   they are ringed wherever they appear. The drop shadow from .card has to be
   restated: box-shadow does not cascade in pieces. */
.card.cupid-named {
    box-shadow: 0 0 0 2px var(--accent), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
/* The Zephyr is the odd one out - it never wins anything, it only denies - so
   it rings cold rather than gold. */
.card.cupid-zephyr {
    box-shadow: 0 0 0 2px rgba(190, 220, 255, 0.9),
                0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}

/* --- the ranking and scoring reference ----------------------------------- */

/* Suit symbols carry their real color here, the same --red the game log uses
   for card labels, so a suit reads the same wherever it appears. Slightly larger
   than the text around them: at 13px a pip is otherwise a smudge. */
.cupid-suit { font-size: 1.15em; line-height: 1; }
.cupid-suit.red { color: var(--red); }
/* Per-suit refinement (see the .cardlabel block). The modifiers are written by
   hand in pages/cupid.mjs, this reference being static HTML. */
.cupid-suit.red.diamond { color: var(--red-diamond); }
.cupid-suit.club { color: var(--blue-club); }
/* The four named cards, wherever they are referred to. */
.cupid-ref-name { color: var(--accent); }

.cupid-ranking-label,
.cupid-scoretable th {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.6;
    font-weight: 600;
}
.cupid-ranking-label { margin: 12px 0 4px; }
.cupid-ranking {
    font-size: 13px;
    line-height: 2;
    margin: 0;
}
/* Each step holds together while the line wraps, so a rank never breaks across
   two rows and the pairs stay readable as pairs. */
.cupid-rank-step { white-space: nowrap; }
/* The ascent, marked between steps rather than stated once in the label. */
.cupid-rank-sep { opacity: 0.35; margin: 0 5px; }
.cupid-scoretable {
    border-collapse: collapse;
    font-size: 13px;
    margin-top: 14px;
}
.cupid-scoretable th {
    text-align: left;
    padding: 0 14px 4px 0;
    border-bottom: 1px solid var(--panel-border);
}
.cupid-scoretable td {
    padding: 4px 14px 4px 0;
    border-top: 1px solid rgba(255, 255, 255, 0.12);
}
.cupid-scoretable td:first-child { font-weight: 600; white-space: nowrap; }
/* The value column: the numeric answers line up under each other, and the phrasal
   ones ("add the two values") sit in the same column rather than reading as part
   of the trick. */
.cupid-scoretable .cupid-score-value {
    opacity: 0.85;
    font-variant-numeric: tabular-nums;
    padding-right: 0;
}

/* --- narrow screens ------------------------------------------------------ */

/* --- the two shell-owned lines above the board ---------------------------- */

/* The status line and the scoreboard are centered by the platform now, for every
   game. Cupid is where the argument was first worked out and lost: centering the
   status moves its words horizontally as the sentence changes length. The height
   is reserved either way (below), so nothing below the line moves, and reading
   the sentence in the middle of the board it describes won out.

   The two SHELL-owned lines above the board are the only things left that move.
   Measured over a whole match with tools/cupid/cupid-measure.mjs: the board's own
   panels are stable to within 3px at every width, but at 390px the status line
   runs 26..50px and the scoreboard 34..61px, and those 51px cascade straight
   down onto the opponents' panel, the piles, the trick, the taken trick and the
   hand. Both are reserved below, and each is scoped through a class Cupid adds
   itself (renderStatus sets `status cupid-status`, renderProgress adds
   `cupid-scoreboard`) because #status-line and #progress-line are on every page. */

@media (max-width: 620px) {
    /* Two lines' worth of status, always. The sentence runs from "Your lead:
       play any card." to "AI led the Anima: answer with any card.", and the
       long one takes one line at 1280px, 900px and 620px but two at
       540px and below. Reserved from this breakpoint down rather than from the
       measured 540px threshold: the boundary depends on the wording and on how
       long an online display name is, and a line of held-open space is cheaper
       than the board stepping mid-trick. */
    .status.cupid-status { min-height: 2.8em; }

    /* The scoreboard is "ROUND n", a chip per seat, and "Trick 4 of 11", and
       around 400px the four of them stop fitting on one row. Which one folds
       then depends on the digits: a score reaching three figures was enough to
       send the round label to a second row, and a squeezed trick counter broke
       across two lines inside itself. Either way the line gained 27px, mid-play,
       and pushed the whole board down.
       So the trick counter takes a row of its own DELIBERATELY (flex-basis
       100%), which makes two rows the shape at every width below this
       breakpoint rather than a threshold that moves with the score. The
       min-height then pins it, because the counter is not rendered once a round
       is over: one row plus the gap plus one 14px line. */
    .progress.cupid-scoreboard {
        gap: 8px;
        min-height: calc(34px + 8px + 1.4em);
    }
    .progress.cupid-scoreboard .progress-round { white-space: nowrap; }
    /* nowrap on the chip, not on its three spans: the name, the digits and the
       "/121" are separate inline boxes, so a squeezed chip would otherwise break
       between them and grow taller than the row reserved for it. */
    .progress.cupid-scoreboard .progress-score {
        padding: 3px 9px;
        white-space: nowrap;
    }
    .cupid-progress-tricks { flex: 1 0 100%; white-space: nowrap; }

    .cupid-board { gap: 12px; }
    /* A smaller card on a small screen. The strip's reservation is derived from
       this, so it recomputes with it. */
    .cupid-known { --cupid-knowncw: 30px; }
    .cupid-piles { gap: 6px; }
    .cupid-pile .card { --cw: 44px; }
    .cupid-prize .card { --cw: 34px; }
    .cupid-pile-label { font-size: 11px; max-width: 84px; }
    .cupid-trick { min-height: 150px; padding: 10px; gap: 6px; }
    .cupid-trick .card { --cw: 62px; }
    .cupid-slot { padding: 4px 6px 8px; }
    /* The taken trick lies down into a strip rather than taking a column of a
       screen this narrow. */
    .cupid-last {
        flex: 1 1 100%;
        flex-direction: row;
        align-items: center;
        flex-wrap: wrap;
        gap: 8px;
    }
    .cupid-last-head { flex: none; gap: 6px; }
    .cupid-last .card { --cw: 34px; }
    .cupid-last-note { flex: 1 1 100%; min-height: calc(2 * 1.35em); }
}

/* Below 560px the panel DOES go beside the piles, which is where it reads best.
   The switch is here rather than at 620px because of where the board breaks its
   rows: the piles column wants 256px and the trick 300px, so the two share a row
   down to about a 600px viewport, and beside the piles there is nothing to spare.
   Below that the trick takes a row of its own, the piles' row has 140px going
   begging, and a panel sitting in it costs no height at all: the piles are the
   taller of the two. The name goes back above the cards, the column being too
   narrow to give it a share of the width. */
@media (max-width: 560px) {
    .cupid-side { flex-direction: row; gap: 8px; flex: 1 1 auto; }
    #opponents.cupid-opponents { flex: 1 1 100px; min-width: 0; }
    .cupid-opponents .opponent { display: block; padding: 6px 8px; }
    .cupid-known { margin-top: 4px; }
}

/* =========================================================================
   Le Truc
   Three cards, three tricks, and a doubling cube. Two things drive the layout:
   the STAKE gets a chip of its own at the top, because it is what the game is
   about; and all three tricks sit side by side for the whole hand, because the
   tie-break is "whoever won the first trick" and a single clearing trick area
   could not show that. The scoreboard is the shared .progress-score widget
   above (Le Truc opts in with .truc-scoreboard) since the race to 12 is short
   enough to watch every point of.
   ========================================================================= */

/* --- a status line that does not resize ----------------------------------- */

/* Two lines' worth, always. The wording grows and shrinks ("Your lead - play any
   card" vs "AI led 8 - answer with any card, or ask to double first. Only an Ace
   beats an 8."), and the shared .status reserves only one line, so without this
   everything below - including the hand - would step up and down as the sentence
   changed. */
.status.truc-status { min-height: 2.9em; }

@media (max-width: 620px) {
    /* Narrower column, so the same sentences take a line more. */
    .status.truc-status { min-height: 4.4em; }
}

/* --- the stake, at the right-hand end of the scoreboard ------------------- */

/* margin-left: auto is what puts it top-right. It lives in the scoreboard line
   rather than in a row of its own precisely so that doubling cannot change the
   height of anything: a row that came and went would shove the trick board and
   the hand up and down the screen mid-hand. */
.truc-stake {
    margin-left: auto;
    display: inline-flex;
    align-items: baseline;
    gap: 7px;
    padding: 4px 14px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.28);
    border: 1px solid var(--panel-border);
}
/* Once the hand has been doubled the stake is the most important number on the
   screen, so it stops being furniture and lights up. */
.truc-stake.raised {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.14);
    box-shadow: 0 0 0 1px var(--accent);
}
/* An offer awaiting an answer pulses, because it is blocking the game. */
.truc-stake.pending {
    border-color: var(--accent);
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.truc-stake-label {
    font-size: 11px;
    opacity: 0.7;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
.truc-stake-value {
    font-size: 24px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.truc-stake.raised .truc-stake-value { color: var(--accent); }
.truc-stake-unit { font-size: 13px; opacity: 0.8; }
.truc-stake-offer { font-size: 15px; font-weight: 700; color: var(--accent); }

/* "Trick 2 of 3" - a quiet counter beside the scores, styled like the hand
   number at the other end of the line. */
.truc-progress-tricks {
    font-size: 13px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.6;
}

@media (max-width: 620px) {
    /* Too narrow to sit beside the scores, so it wraps to its own full-width
       line - still above the board, still never changing height. */
    .truc-stake { margin-left: 0; flex-basis: 100%; justify-content: center; }
}

/* --- the three tricks ---------------------------------------------------- */

/*
 * A GRID: a gutter of seat names down the left, then one column per trick.
 *
 * The gutter is the point. Naming the seat beside each CARD (an earlier version
 * did) means text of a different width in every state - "AI", "AI · led",
 * nothing at all on an empty slot - and that shoves the cards sideways every
 * time a trick progresses. Here every card sits in a grid cell whose width comes
 * from the column, so nothing beside it can move it. The seats are named once
 * each instead of six times, which reads better anyway.
 */
.truc-tricks {
    display: grid;
    grid-template-columns: auto repeat(3, minmax(0, 1fr));
    align-items: center;
    justify-items: center;
    column-gap: 10px;
    row-gap: 4px;
    padding: 12px 14px;
    margin-bottom: 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
/* The empty gutter cells on the head and caption rows. */
.truc-corner { width: 100%; }
/* One per seat, on that seat's row of cards. Also the anchor their played card
   flies in from. */
.truc-seat {
    justify-self: end;
    font-size: 13px;
    font-weight: 600;
    opacity: 0.7;
    white-space: nowrap;
    padding-right: 2px;
}
.truc-head {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
}
/* The trick being played now, marked on its number rather than by tinting a
   whole column - which with a gutter in the way would not read as a column. */
.truc-head.current { opacity: 1; color: var(--accent); }
.truc-cell { display: flex; align-items: center; justify-content: center; }
/* The played cards are the thing being read, so they are full size - the same
   as the cards in hand. Anything smaller and the rank is guesswork, which in a
   game decided by rank alone is the whole of the information. */
.truc-cell .card { --cw: 76px; flex: none; }
/* The card that took the trick. With no suits and no trumps this is the whole
   of what happened, so it is the one thing ringed. */
.card.truc-winning { outline: 3px solid var(--accent); outline-offset: 2px; }
/* Which card was led is worth knowing (the leader re-leads after a tie), so the
   two cards of a trick are staggered: the led card sits a little left, the
   answer a little right. Clearer at a glance than a label or a ring, and it
   costs no vertical space. The offset is on the cell, so an empty follower slot
   holds the same stagger before the answer lands. */
.truc-cell.truc-led { transform: translateX(-11px); }
.truc-cell.truc-follow { transform: translateX(11px); }
/* Two lines' worth of room, always. The caption changes as a trick is played
   ("To come" -> "In play" -> "You win - A takes the 8"), and without a reserved
   height the longer wording would reflow the board and move the hand. */
.truc-cap {
    font-size: 12px;
    opacity: 0.85;
    text-align: center;
    line-height: 1.3;
    min-height: 2.6em;
    align-self: start;
}
/* A tied trick counts for nobody, which has to look different from a lost one
   rather than merely unmarked - hence its own cold color. */
.truc-cap.tied { color: rgba(190, 220, 255, 0.95); opacity: 1; font-weight: 600; }

/* --- the hand, and the cube controls beside it ---------------------------- */

/* The hand panel MUST NOT MOVE: it is what the player aims at. Everything that
   could have shifted it is pinned - the stake lives in the scoreboard, the trick
   captions reserve their height, and the controls below hold their column
   whether or not they have anything in them. */
.truc-handrow {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    /* One row of cards, always - so the panel's height is fixed. */
    min-height: calc(var(--cw) * 89 / 64);
}
.truc-handrow #hand-cards { flex: 1 1 auto; }

.truc-actions {
    /* Fixed column, present even when empty: an offer appearing must not slide
       the cards sideways. */
    flex: 0 0 232px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 0;
    border-radius: 12px;
    border: 1px solid transparent;
}
/* The frame appears only when there is actually a decision in the box. */
.truc-actions.filled {
    padding: 9px 11px;
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 209, 102, 0.35);
}
.truc-actions .btn { width: 100%; }
.truc-actions-note { font-size: 12px; opacity: 0.8; line-height: 1.35; }

@media (max-width: 760px) {
    /* Not enough width for a column beside the cards: drop it underneath, still
       inside the panel. */
    .truc-handrow { flex-wrap: wrap; }
    .truc-actions { flex: 1 1 100%; }
}
/* The hint flash lands on a BUTTON here, not on a card, so it needs its own
   rule - but the same one hintpulse can drive: the keyframes animate
   outline-color, so an outline is what has to be there to pulse. */
.truc-actions .btn.hinted {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}

/* --- the hand summary ----------------------------------------------------- */

/*
 * Le Truc's summary is three rows of one- or two-digit numbers for two players.
 * The shared .result-row is built for the poker siblings, whose cells hold
 * five-card hands: it gives every seat column a 180px minimum and a 1fr share,
 * which stretches this table right across the panel with a couple of digits
 * adrift in each cell. Size the columns to their contents and let the table be
 * as narrow as it wants to be.
 */
/* The panel itself: a narrow card in the middle of the screen, not a full-width
   band. Everything in it centers, so the banner sits over the table and the
   buttons under it. */
.panel.truc-summary {
    max-width: 380px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
.panel.truc-summary .result-actions { justify-content: center; flex-wrap: wrap; }
.panel.truc-summary .result-note { text-align: center; }

.result-table.truc-result { width: fit-content; gap: 1px; margin: 0 auto; }
.result-table.truc-result .result-row {
    grid-template-columns: auto repeat(2, minmax(3.6em, auto));
    gap: 2px 22px;
    align-items: baseline;
}
/* Numbers right-aligned under the name they belong to, and tabular so the
   columns do not shuffle as the digits change. */
.result-table.truc-result .result-cell {
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.result-table.truc-result .result-label { text-align: left; white-space: nowrap; }
/* The winner's star: a badge above the right edge of the name, out of flow, so
   the name still right-aligns over its column of numbers. Appended into the
   name text it dragged the name off-center - the bug this fixes. */
.result-table.truc-result .result-cell.truc-won {
    position: relative;
    color: var(--accent);
}
.result-table.truc-result .result-cell.truc-won::after {
    content: '\2605';
    position: absolute;
    right: 0;
    top: -0.95em;
    font-size: 11px;
    line-height: 1;
    color: var(--accent);
}
.result-table.truc-result .result-row.totals .result-cell {
    font-size: 20px;
    font-weight: 800;
}
.result-table.truc-result .result-row.totals {
    margin-top: 4px;
    padding-top: 4px;
    border-top: 1px solid var(--panel-border);
}

/* --- log and reference --------------------------------------------------- */

/* Betting events are decisions, not card play, so they are picked out of the log. */
.truc-logbet { color: var(--accent); }
/* Card chips in the log carry a rank and no suit, so they are never colored
   red: two cards of the same number are the same card in this game. */
.truc-cardlabel { color: var(--ink); font-variant-numeric: tabular-nums; }
/* The ranking cycle, set apart from the sentence that introduces it. */
.truc-cycle {
    display: inline-block;
    font-weight: 700;
    white-space: nowrap;
    color: var(--accent);
}

/* --- narrow screens ------------------------------------------------------ */

@media (max-width: 620px) {
    .truc-tricks { column-gap: 6px; padding: 8px; }
    /* Still big enough to read the number at a glance, which is the point. */
    .truc-cell .card { --cw: 54px; }
    /* A smaller card wants a smaller stagger, or the two cards drift apart. */
    .truc-cell.truc-led { transform: translateX(-7px); }
    .truc-cell.truc-follow { transform: translateX(7px); }
    .truc-seat { font-size: 11px; }
    .truc-cap { font-size: 11px; min-height: 3.4em; }
}

/* =========================================================================
   Crisps
   A climbing-shedding race, and the layout follows from two facts. First, the
   round RATCHETS: what has already been played is as much of the position as
   what is on top, so the whole climb stays on the table, oldest first and
   dimmed, with the live combination ringed. Second, a move is a SET of cards,
   so the hand is a selection surface rather than a row of buttons - which makes
   it the thing the player aims at, and means NOTHING above it may change
   height. Every region above the hand therefore has a reserved height, and
   every control lives in a fixed column inside the hand panel.
   The scoreboard is the shared .progress-score widget above (Crisps opts in
   with .crisps-scoreboard), carrying both scores AND both hand counts: the race
   to shed is the number a player actually watches.
   ========================================================================= */
/* --- a status line that does not resize ----------------------------------- */
/* Two lines' worth, always. This line does the teaching in Crisps ("Stair 5-5-6-6
   - you must answer pair of 8s with a pair"), so its length swings widely, and
   the shared .status reserves only one line - without this the board and the hand
   would step up and down the screen as the wording changed. */
.status.crisps-status { min-height: 2.9em; }
@media (max-width: 620px) {
    /* Narrower column, so the same sentences take a line more. */
    .status.crisps-status { min-height: 4.4em; }
}
/* The hand counts, beside each score. In a shedding race this is the number that
   moves, so it sits in the scoreboard rather than on a panel of its own. */
.crisps-scorecards {
    font-size: 12px;
    opacity: 0.7;
    padding-left: 4px;
    border-left: 1px solid var(--panel-border);
    margin-left: 2px;
}
/* Two cards or fewer: they are one round from going out, and that has to shout. */
.crisps-scorecards.close { opacity: 1; color: var(--accent); font-weight: 700; }
.crisps-progress-round {
    margin-left: auto;
    font-size: 13px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.6;
}
/* --- the pile, the face-up card, and the four out of the deal -------------- */
/* Fixed height: the pile emptying and the face-up card vanishing must not move
   the board - or the hand - below them. */
/*
 * EVERY REGION ABOVE THE HAND HAS A HEIGHT THAT CANNOT CHANGE, and each does it
 * the same way: nothing wraps, and every line that comes and goes is reserved
 * whether or not it has anything in it. A min-height that the content can never
 * exceed is a CONSTANT height, which is the property that matters - a board that
 * grows by a row pushes the hand down under the player's finger.
 */
.crisps-piles {
    display: flex;
    align-items: flex-start;
    gap: 18px;
    /* No wrapping: a wrapped pile row is a taller pile row. */
    flex-wrap: nowrap;
    padding: 10px 12px;
    margin-bottom: 10px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
    /* Card + count + the reserved "no more draws" line. */
    min-height: calc(52px * 89 / 64 + 56px);
}
.crisps-pile,
.crisps-faceup {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.crisps-pile .card,
.crisps-faceup .card { --cw: 52px; }
.crisps-pile-count { font-size: 12px; opacity: 0.75; white-space: nowrap; }
/* "No more draws this deal" - a rule change big enough to say in words, since
   from here on a pass costs nothing but the lead. */
/* Reserved even while empty - see the .crisps-piles note above. */
.crisps-pile-note {
    font-size: 11px;
    line-height: 1.3;
    min-height: 1.3em;
    color: var(--accent);
    font-weight: 700;
    white-space: nowrap;
}
.crisps-faceup-label {
    font-size: 11px;
    opacity: 0.6;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
/* --- the opponent's hand, face down -------------------------------------- */
/* Small and quiet: it is not a scoreboard (the count is up in the scores), it is
   the place the opponent's cards fly out of. Fixed height so losing cards cannot
   move the board below it. */
.crisps-opp {
    display: flex;
    align-items: center;
    gap: 10px;
    /* Nowrap + a scrolling card strip: a wrapped row is a taller row, and this
       one grows a face-up card the moment the opponent takes one. */
    flex-wrap: nowrap;
    /* Load-bearing while the hotseat gate is up: renderOppHand paints nothing
       behind the gate rather than hiding the box, so this is what stops the
       table and the hand under it sliding 45px up and back at every handoff. */
    min-height: calc(44px * 89 / 64 + 4px);
    margin-bottom: 10px;
}
.crisps-opp-name { flex: none; }
.crisps-opp-name { font-size: 12px; opacity: 0.65; }
.crisps-opp-cards {
    display: flex;
    gap: 3px;
    flex-wrap: nowrap;
    align-items: center;
    /* Twelve cards will not fit on a phone, so the strip scrolls sideways rather
       than wrapping onto a second line. */
    overflow-x: auto;
    min-width: 0;
}
/* One size for backs and known cards alike: a known card appearing must not make
   the row taller. Both are drawn from the simplified atlas, which is what stays
   readable at 44px (and at 32px on a phone). */
.crisps-opp-cards .card { --cw: 44px; flex: none; }
/* A card they were SEEN to take: public information, so it is face up. Bigger
   than a back and ringed, because it is the one thing in this row worth reading -
   and it is the payoff for having declined the face-up card. */
.crisps-opp-cards .card.crisps-opp-known {
    outline: 2px solid rgba(255, 209, 102, 0.75);
    outline-offset: 1px;
    margin-right: 3px;
}
.crisps-opp-empty { font-size: 12px; opacity: 0.5; }
/* Reserved even while empty, and dropped entirely when there is no room. */
.crisps-opp-note { font-size: 11px; opacity: 0.6; flex: none; }
@media (max-width: 720px) { .crisps-opp-note { display: none; } }
/* --- the climb ----------------------------------------------------------- */
/* The whole round, oldest first. Reserves a row's height even when empty, so
   the first play of a round does not shove the hand down the screen. */
/*
 * THE ONE THAT WAS MOVING THE HAND. A round ratchets upwards, so the climb keeps
 * getting wider - and with flex-wrap it eventually took a second row, doubling
 * the board's height mid-round and shoving the hand down the screen. It scrolls
 * sideways instead, and renderTable keeps the newest combination in view.
 */
.crisps-table {
    display: flex;
    align-items: flex-end;
    gap: 12px;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    padding: 12px;
    margin-bottom: 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
    /* Cards + the who line + the reserved caption + both boxes' padding and
       border. Measured at 160.6px, so the reservation clears it by a pixel and
       is therefore the governing height in EVERY phase - an empty table, a
       nine-deep climb and the pass marker all come out the same. It was 76px
       here, three short, so the box was 157px while empty and 160px the moment
       anything was played, and the hand took the 3px with it. */
    min-height: calc(58px * 89 / 64 + 81px);
}
/* Armed: the table is a drop zone and a click target for the selection. */
.crisps-table.armed { cursor: pointer; border-color: rgba(255, 209, 102, 0.3); }
.crisps-table-empty { font-size: 13px; opacity: 0.7; align-self: center; }
.crisps-play {
    display: flex;
    flex-direction: column;
    gap: 5px;
    padding: 6px 8px 7px;
    border-radius: 10px;
    border: 1px solid transparent;
}
/* Beaten, and still worth seeing: which pairs are spent is part of the position.
   Dimmed rather than removed. */
.crisps-play.spent { opacity: 0.5; }
/* The combination that has to be beaten. The one thing on the board ringed. */
.crisps-play.live {
    background: rgba(255, 255, 255, 0.06);
    border-color: var(--accent);
}
/* A triple or quad closes the round to standard combinations entirely, so it is
   marked as a different kind of thing rather than just a bigger one. */
.crisps-play.special {
    background: rgba(190, 220, 255, 0.1);
    border-color: rgba(190, 220, 255, 0.45);
}
.crisps-play.live.special { border-color: var(--accent); }
/* The pass that ended the round, sitting at the end of the climb it ended.
   Deliberately loud: without it the draw screen is a pile of cards and two
   buttons, and there is no way to tell a beaten opponent from one who declined. */
.crisps-play.crisps-passed {
    background: rgba(255, 209, 102, 0.12);
    border-color: var(--accent);
    justify-content: center;
    align-items: center;
    min-width: 84px;
}
.crisps-passed-mark {
    /* Sized to stand level with the played cards beside it. */
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: calc(58px * 89 / 64);
    font-size: 17px;
    font-weight: 800;
    letter-spacing: 0.12em;
    color: var(--accent);
}
/* An empty table says who passed to make it empty, so a swept round still reads
   as a consequence rather than as a round that began out of nowhere. */
.crisps-table-empty { text-align: center; }
.crisps-play-who {
    font-size: 11px;
    line-height: 1.3;
    min-height: 1.3em;
    letter-spacing: 0.03em;
    opacity: 0.8;
    white-space: nowrap;
}
.crisps-play-cards { display: flex; gap: 4px; }
.crisps-play-cards .card { --cw: 58px; flex: none; }
/* Reserved even while empty: "To beat" comes and goes as the climb advances, and
   without a held line the whole board would step up and down with it.
   NOWRAP IS PART OF THE RESERVATION. "ROUND OVER" is 70px of uppercase at this
   letter-spacing and the pass marker is 84px wide with 16px of padding, so the
   caption had 68px to fit in and wrapped to a second line - which made that one
   group 149px against every other group's 134px, grew the table 15px, and moved
   the hand 18px down the screen every time a round ended. */
.crisps-play-caption {
    font-size: 11px;
    line-height: 1.3;
    min-height: 1.3em;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--accent);
    font-weight: 700;
    white-space: nowrap;
}
/* --- the hand, and the action bar beside it ------------------------------- */
/* The hand MUST NOT MOVE: it is what the player aims at, and in Crisps they are
   aiming several taps in a row. Everything that could shift it is pinned - the
   rows above reserve their heights, and the controls below hold their column
   whether or not they have anything in them. */
.crisps-handrow {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    /* One row of cards, always - so the panel's height is fixed. */
    min-height: calc(var(--cw) * 89 / 64);
}
.crisps-handrow #hand-cards { flex: 1 1 auto; }
.crisps-actions {
    /* Fixed column, present even when empty: a Pass button or a draw choice
       appearing must not slide the cards sideways. */
    flex: 0 0 250px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 0;
    border-radius: 12px;
    border: 1px solid transparent;
}
/* The frame appears only when there is actually a decision in the box. */
.crisps-actions.filled {
    padding: 9px 11px;
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 209, 102, 0.35);
}
.crisps-actions .btn { width: 100%; }
.crisps-actions .btn:disabled { opacity: 0.45; }
.crisps-actions-note { font-size: 12px; opacity: 0.8; line-height: 1.35; }
/* The selection read back as what it IS ("Stair 5-5-6-6"). This is where the
   combination rules get learned, so it is the one note that is not muted. */
.crisps-read { opacity: 1; font-weight: 700; }
/* A set of cards that is a combination but cannot be played: the message says
   which rule stopped it, so it reads as an explanation rather than an error. */
.crisps-read.bad { color: var(--red); font-weight: 600; }
/* A card of YOURS the opponent has seen: you took it face up, off the table, in
   front of them. Cold blue rather than the accent, so it never reads as the
   selection (accent outline) or a hint (pulsing accent outline) - it is a
   standing fact about the card, not something you just did. */
.card.crisps-seen {
    outline: 2px dashed rgba(190, 220, 255, 0.85);
    outline-offset: 1px;
}
.card.crisps-seen.sel { outline-style: solid; outline-color: var(--accent); }
.crisps-seen-note {
    font-size: 12px;
    font-weight: 400;
    opacity: 0.7;
    color: rgba(190, 220, 255, 0.95);
}
/* The hand's own heading, and the note above is appended to it. The note comes
   and goes as cards become known, so on a phone the heading takes two lines from
   the moment the opponent has seen one card - and #hand-cards sits directly
   below it. Reserved at both breakpoints. Scoped to the class the page puts on
   Crisps' panel, because #hand-title and .panel-head belong to every game. */
.crisps-handpanel .panel-head { min-height: calc(1.3em + 10px); }
@media (max-width: 620px) {
    .crisps-handpanel .panel-head { min-height: calc(2 * 1.3em + 10px); }
}
/* A card that appears in no legal play. Dimmed by RANK, never by card - two 7s
   are the same card in Crisps, so dimming one of them would be a lie. */
.card.crisps-dead { opacity: 0.4; filter: saturate(0.4); }
.card.crisps-dead:hover { transform: none; }
/* The hint flash can land on a BUTTON here (Pass, or one of the two draws), not
   only on a card, so it needs its own rule - but the same hintpulse can drive
   it: the keyframes animate outline-color, so an outline has to be there. */
.crisps-actions .btn.hinted {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}
@media (max-width: 820px) {
    /* Not enough width for a column beside the cards: drop it underneath, still
       inside the panel. */
    .crisps-handrow { flex-wrap: wrap; }
    .crisps-actions { flex: 1 1 100%; }
}
/* --- the deal summary ----------------------------------------------------- */
/*
 * Three rows of one-digit numbers for two players. The shared .result-row is
 * built for the poker siblings, whose cells hold five-card hands: it gives every
 * seat column a 180px minimum and a 1fr share, which stretches this table right
 * across the panel with a couple of digits adrift in each cell. Size the columns
 * to their contents and let the table be as narrow as it wants to be.
 */
.result-table.crisps-result { width: fit-content; gap: 1px; margin: 0 auto; }
.result-table.crisps-result .result-row {
    grid-template-columns: auto repeat(2, minmax(3.6em, auto));
    gap: 2px 22px;
    align-items: baseline;
}
/* Numbers right-aligned under the name they belong to, and tabular so the
   columns do not shuffle as the digits change. */
.result-table.crisps-result .result-cell {
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.result-table.crisps-result .result-label { text-align: left; white-space: nowrap; }
.result-table.crisps-result .result-row.totals .result-cell {
    font-size: 20px;
    font-weight: 800;
}
.result-table.crisps-result .result-row.totals {
    margin-top: 4px;
    padding-top: 4px;
    border-top: 1px solid var(--panel-border);
}
/* --- log and reference --------------------------------------------------- */
/* A pass is the decision that ends a round, so it is picked out of the log. */
.crisps-logpass { color: var(--accent); }
/* Card chips in the log carry a rank and no suit, so they are never colored
   red: two cards of the same number are the same card in this game. */
.crisps-cardlabel { color: var(--ink); font-variant-numeric: tabular-nums; }
/* The rank order in the Rules_Summary and on the Rules_Page alike (the rule is
   not scoped to a wrapper, so both places match). */
.crisps-cycle {
    display: inline-block;
    font-weight: 700;
    white-space: nowrap;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}
/* --- narrow screens ------------------------------------------------------ */
@media (max-width: 620px) {
    .crisps-piles { gap: 10px; padding: 8px; }
    /* THREE rows at this width, measured: "Deal 2", the two score chips (each
       carrying a name, a score, the target and a card count) and "Round 4" do
       not fit across 390px. The shared widget reserves ONE row, so without this
       the line grew 25px on the second row and another 13px on the third - in
       hotseat, where the seats are named "Player 1" and "Player 2" rather than
       "You" and "AI" - and carried the whole board, piles, opponent row, table
       and hand, down with it. Scoped to Crisps: the other fourteen games on that
       widget fit one row here and would just get 38px of dead space. */
    .progress.crisps-scoreboard { min-height: 72px; }
    .crisps-table {
        gap: 8px;
        padding: 8px;
        /* Clears the 125px a group measures here, on the same reasoning as the
           desktop reservation above. */
        min-height: calc(44px * 89 / 64 + 68px);
    }
    .crisps-opp { min-height: calc(32px * 89 / 64 + 4px); }
    /* Still big enough to read the rank at a glance, which in a game decided by
       rank alone is the whole of the information. */
    .crisps-play-cards .card { --cw: 44px; }
    .crisps-play { padding: 4px 5px 6px; gap: 3px; }
    /* NOT `white-space: normal`, which is what this said. A long combination
       name ("Sequence 4-5-6-7-8") then took two lines here and only here, so the
       table grew and the hand moved - at the width where it moves furthest. The
       table scrolls sideways, so a wide group costs nothing. */
    .crisps-play-who { font-size: 10px; }
    .crisps-passed-mark { min-height: calc(44px * 89 / 64); font-size: 14px; }
    .crisps-opp-cards .card { --cw: 32px; }
}

/* =========================================================================
   Hearts and Gong Zhu
   Two titles over one renderer (src/shared/hearts/ui.ts), so every rule here
   carries both prefixes rather than the styling being duplicated - the same
   arrangement the scoreboard widget above uses. Only the game-specific
   coloring at the end differs between them.

   The layout follows from three facts. First, this is a game of ACCUMULATION:
   what each player has captured so far IS the position, so every seat's point
   cards stay on screen face up beside their total, and nothing about the game
   is legible without them. Second, a trick has a FIXED SHAPE - one card per
   seat, in play order - so the trick area reserves a slot per seat and a card
   never appears to jump position as the trick fills. Third, three of the four
   phases put a decision in the HAND (choose three to pass, choose what to
   expose, choose what to play), so the hand is the surface the player aims at
   and nothing above it may change height as the phase changes.
   ========================================================================= */

/* --- the table: one grid --------------------------------------------------- */
/* THE TABLE AS A TABLE. One grid for the board: the other seats' panels sit West,
   North and East of the trick, and the trick is a cross whose slots sit on the
   same sides, so a card is read by where it lands. #hearts-table is
   `display: contents`, so its panels are cells of this grid; the viewer is South,
   below the board, where their hand is. The side columns are flexible and the
   middle one is the cross's own width, so the panels hug the trick rather than
   the page edges. The trick just taken sits beside North, in the corner, and
   with three seats (no North) under East.

   A panel is a line taller in a round when it can show a card in that hand
   (ui.ts: the seat you pass to, and a seat that exposed). Only the panels whose
   height moves the board RESERVE that line (--seat-full): North here, East at
   three seats, and all three once they sit in a row across the top. The others
   size to their contents, which moves nothing.

   The board used to be the three panels in a row ABOVE a trick area the width
   of the page: 525px tall at 1280px wide, most of it empty felt, with 54px cards
   in the middle of it. */
/* The board's size container. Nothing inside is position: fixed (the flying
   cards are appended to document.body), so containment costs nothing. */
.hearts-arena,
.gongzhu-arena { container: htable / inline-size; }
.hearts-board,
.gongzhu-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas:
        "taken up    last"
        "left  trick right";
    align-items: center;
    gap: 10px 18px;
    margin-bottom: 12px;
}
/* At three seats West and the viewer's point cards mirror East and the last
   trick: a seat over a pile on each side of the cross. */
.hearts-board.seats-3,
.gongzhu-board.seats-3 {
    grid-template-areas:
        "left  trick right"
        "taken trick last";
}
.hearts-board.seats-3 .hearts-seat.pos-left,
.gongzhu-board.seats-3 .gongzhu-seat.pos-left { align-self: end; min-height: var(--seat-full); }
.hearts-table,
.gongzhu-table { display: contents; }
.hearts-seat.pos-left,
.gongzhu-seat.pos-left { grid-area: left; justify-self: end; }
.hearts-seat.pos-up,
.gongzhu-seat.pos-up { grid-area: up; justify-self: center; min-height: var(--seat-full); }
.hearts-seat.pos-right,
.gongzhu-seat.pos-right { grid-area: right; justify-self: start; }
/* At three seats East sits on top of the last trick, so its height moves it. */
.hearts-board.seats-3 .hearts-seat.pos-right,
.gongzhu-board.seats-3 .gongzhu-seat.pos-right { align-self: end; min-height: var(--seat-full); }
/* A seat's panel. SMALL, and one fixed width, so a long name or a growing pile
   cannot resize the grid; capped at its column where the board is narrowest. */
.hearts-seat,
.gongzhu-seat {
    box-sizing: border-box;
    width: 172px;
    max-width: 100%;
    min-width: 0;
    padding: 6px 9px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default so the .active ring never shifts the layout. */
    outline: 2px solid transparent;
    /* The card sizes of the two rows, here rather than on the rows so the
       panel's own reservation can be derived from them: the head line, the
       known-cards row, and two rows of captured point cards, plus the padding
       and border. Overridden per breakpoint at this level, never on a row. */
    --kn-cw: 20px;
    --tk-cw: 17px;
    --seat-full: calc(20px + 4px + var(--kn-cw) * 1.39
        + 6px + 2 * var(--tk-cw) * 1.39 + 2px + 15px);
}
/* The seat on turn. Gold is the platform's "this is where the action is". */
.hearts-seat.active,
.gongzhu-seat.active { outline-color: var(--accent); }
/* One line: the name, this round's points (or Ready during the pass), and the
   match total. Every label nowrap, so a long name cannot make the head taller
   than its one reserved line. */
.hearts-seat-head,
.gongzhu-seat-head {
    display: flex;
    align-items: baseline;
    gap: 6px;
    min-height: 20px;
    white-space: nowrap;
}
.hearts-seat-name,
.gongzhu-seat-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: 700;
    font-size: 13px;
}
/* The match total, the number that decides the game. */
.hearts-seat-total,
.gongzhu-seat-total {
    font-size: 17px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
/* "Ready" / "Choosing..." during the two simultaneous phases. The only thing
   ever said about another seat's pass or exposure - the choice itself is
   private until every seat has committed. */
.hearts-seat-ready,
.gongzhu-seat-ready { color: var(--accent); font-weight: 700; font-size: 12px; }
/* Points taken THIS ROUND. Positive is bad in both games, so the two colors
   are the whole of the reading. */
.hearts-seat-points,
.gongzhu-seat-points { font-weight: 700; font-size: 13px; font-variant-numeric: tabular-nums; }
.hearts-seat-points.bad,
.gongzhu-seat-points.bad { color: #ff8a80; }
.hearts-seat-points.good,
.gongzhu-seat-points.good { color: #8bd88b; }
/* The cards in this hand the viewer may see (exposed, or passed here by the
   viewer). One RESERVED row for the whole round, empty or not, so the panel
   keeps its height as those cards are played (ui.ts says which rounds have it).
   No row of backs: every hand is the same size. */
.hearts-seat-known.cardrow,
.gongzhu-seat-known.cardrow {
    flex-wrap: nowrap;
    justify-content: flex-start;
    gap: 3px;
    margin-top: 4px;
    min-height: calc(var(--kn-cw) * 1.39);
}
.hearts-seat-known .card,
.gongzhu-seat-known .card { --cw: var(--kn-cw); }
/* The point cards captured so far: public, and the most interesting information
   on the table. Drawn from the SIMPLIFIED atlas (rank over suit) because at this
   size the full art is an unreadable smudge and counting the pile is the entire
   reason it is on screen.

   SMALL AND WRAPPING, both load-bearing. A seat can end a round holding all
   thirteen hearts plus the Q-spade, and at any comfortable reading size that row
   is wider than the panel - so it wraps, and the cards are sized so two rows of
   seven still fit inside the box rather than spilling out of it. */
/* `.cardrow.mini` above sets gap 4px and carries two classes, so the override
   has to match its specificity to win - hence `.hearts-taken.cardrow` rather
   than the bare class. Four extra pixels a card is the difference between one
   row and two for a full fourteen-card pile. */
/*
   RESERVED, NOT GROWN. The row is present from the first trick with nothing in
   it, and it holds TWO rows of card height from the start, so a seat's panel
   keeps one height for the whole round instead of stepping taller as the first
   point card arrives and again as the pile wraps. Two rows is the ordinary
   worst case: a full sixteen-card Gong Zhu pile is two rows of eight at this
   size. The size lives in --tk-cw so the reservation follows it per breakpoint.
*/
.hearts-taken.cardrow,
.gongzhu-taken.cardrow {
    --tk-rows: 2;
    margin-top: 6px;
    flex-wrap: wrap;
    align-content: flex-start;
    gap: 2px;
    max-width: 100%;
    min-height: calc(var(--tk-rows) * var(--tk-cw) * 1.39 + (var(--tk-rows) - 1) * 2px);
}
.hearts-taken .card,
.gongzhu-taken .card { --cw: var(--tk-cw); }
/* The viewer's own pile has the full page width, so it can afford to be legible,
   and at that width one row holds a whole round's point cards. */
.hearts-taken-panel .hearts-taken.cardrow,
.gongzhu-taken-panel .gongzhu-taken.cardrow { --tk-cw: 26px; --tk-rows: 1; }

/* Cards in another seat's hand that THIS viewer may see. Exposed cards are
   public and take the standard gold `.lit`; the three the viewer passed to that
   seat are known only to them, so they are faded to say "you know this, the
   table does not". */
.hearts-known,
.gongzhu-known {
    opacity: 0.55;
    outline: 2px dashed rgba(100, 181, 246, 0.8);
    outline-offset: 1px;
}

/* --- the viewer's own captured pile -------------------------------------- */
/* A cell of the board (pages/hearts.mjs): on a wide board the corner beside
   North, mirroring the last trick on the other side, so it costs no row of
   its own; on a narrow one a row under the board. Its height is constant (one
   reserved strip that scrolls), so it never resizes the row it sits in. */
.hearts-taken-panel,
.gongzhu-taken-panel {
    grid-area: taken;
    justify-self: end;
    align-self: end;
    box-sizing: border-box;
    width: 100%;
    max-width: 360px;
    min-width: 0;
    padding: 8px 10px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.hearts-board.seats-3 .hearts-taken-panel,
.gongzhu-board.seats-3 .gongzhu-taken-panel { align-self: start; }
/* A flex row with a real gap: without it the three readings run together into
   "Your point cards+42 tricks", which is how this was first shipped. */
.hearts-taken-head,
.gongzhu-taken-head {
    display: flex;
    align-items: baseline;
    gap: 12px;
    flex-wrap: wrap;
    font-size: 12px;
}
.hearts-taken-label,
.gongzhu-taken-label { font-weight: 700; font-size: 13px; }
.hearts-taken-points,
.gongzhu-taken-points {
    font-weight: 800;
    font-size: 16px;
    font-variant-numeric: tabular-nums;
}
.hearts-taken-points.bad,
.gongzhu-taken-points.bad { color: #ff8a80; }
.hearts-taken-points.good,
.gongzhu-taken-points.good { color: #8bd88b; }
/* The empty state takes exactly the strip's reserved height, so the panel does
   not move when the line is replaced by the first captured card. */
.hearts-taken-none,
.gongzhu-taken-none {
    --tk-cw: 26px;
    opacity: 0.6;
    font-size: 12px;
    margin-top: 6px;
    min-height: calc(var(--tk-cw) * 1.39);
    display: flex;
    align-items: center;
    white-space: nowrap;
}
.hearts-taken-head .hearts-taken-label,
.gongzhu-taken-head .gongzhu-taken-label { white-space: nowrap; }

/* THE STATUS LINE, reserved at two lines. It carries the phase prompt and, after
   a hint, the suggestion too, so its length swings; the shared .status reserves
   one line, and the board and the hand stepped down whenever it wrapped. The
   renderer adds the game class, since the shell owns the element. */
.status.hearts-status,
.status.gongzhu-status { min-height: 2.9em; }

/* A card that may be EXPOSED, during the exposure phase. Only four cards in the
   pack ever can be, and a player who does not already know which four cannot be
   expected to find them in a hand of thirteen. Deliberately lighter than `.sel`,
   which means "armed": this one only means "eligible". */
.hearts-exposable,
.gongzhu-exposable {
    outline: 2px dashed var(--accent);
    outline-offset: 1px;
}

/* --- the trick ------------------------------------------------------------ */
/* A CROSS: one fixed slot per seat, placed where that seat sits - left for the
   player on your left, top for the far seat, right for the player on your right,
   and your own card at the bottom nearest your hand. So a card appears beside the
   panel of the player who played it and never moves once it lands, which is what
   makes a trick readable without a caption under each slot.

   COMPACT: the side cards sit level with the GAP between the top and bottom
   cards, the way cards land on a real table, so the cross is two cards tall
   rather than three. The rows are half-card units in --pl-h, so the whole shape
   follows the card size per breakpoint. The 14px middle row is where the top
   card's out-of-flow "winning" caption hangs. */
.hearts-trick,
.gongzhu-trick {
    --pl-cw: 70px;
    --pl-h: calc(var(--pl-cw) * 1.39);
    grid-area: trick;
    display: grid;
    grid-template-columns: repeat(3, var(--pl-cw));
    grid-template-rows:
        calc(var(--pl-h) / 2) calc(var(--pl-h) / 2) 14px
        calc(var(--pl-h) / 2) calc(var(--pl-h) / 2);
    column-gap: 12px;
    align-items: center;
    justify-items: center;
    justify-self: center;
    /* The bottom card's caption hangs into this. */
    padding: 10px 14px 22px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.hearts-play.pos-up,
.gongzhu-play.pos-up { grid-column: 2; grid-row: 1 / 3; }
.hearts-play.pos-left,
.gongzhu-play.pos-left { grid-column: 1; grid-row: 2 / 5; }
.hearts-play.pos-right,
.gongzhu-play.pos-right { grid-column: 3; grid-row: 2 / 5; }
.hearts-play.pos-down,
.gongzhu-play.pos-down { grid-column: 2; grid-row: 4 / 6; }
/* Three seats: the two others side by side on top, yours below and between. */
.hearts-trick.seats-3,
.gongzhu-trick.seats-3 {
    grid-template-rows: repeat(3, calc(var(--pl-h) / 2));
}
.hearts-trick.seats-3 .hearts-play.pos-left,
.gongzhu-trick.seats-3 .gongzhu-play.pos-left { grid-row: 1 / 3; }
.hearts-trick.seats-3 .hearts-play.pos-right,
.gongzhu-trick.seats-3 .gongzhu-play.pos-right { grid-row: 1 / 3; }
.hearts-trick.seats-3 .hearts-play.pos-down,
.gongzhu-trick.seats-3 .gongzhu-play.pos-down { grid-row: 2 / 4; }
.hearts-play,
.gongzhu-play {
    display: flex;
    flex-direction: column;
    align-items: center;
    /* So the winning caption can hang out of flow below the card without adding
       height to the slot. See .hearts-play-flag. */
    position: relative;
}
.hearts-play .card,
.gongzhu-play .card { --cw: var(--pl-cw); }
/* WHO IS CURRENTLY WINNING THE TRICK. In a game about not taking tricks this is
   the most useful thing on the board, so it is called out on the card itself AND
   captioned underneath - color alone would leave it invisible to a color-blind
   player, and the caption also survives a grayscale screenshot. */
.hearts-winning,
.gongzhu-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
.hearts-play-flag,
.gongzhu-play-flag {
    font-size: 10px;
    line-height: 1;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--accent);
    /* OUT OF FLOW. The caption appears and disappears as the winning card changes
       hands mid-trick, and while it took layout height the whole cross stepped up
       and down under it, dragging the hand panel with it. Hung below the card
       instead, so every slot keeps one height whether or not it is winning. The
       space it hangs into is reserved by the grid (see .hearts-trick). */
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    margin-top: 3px;
    white-space: nowrap;
    pointer-events: none;
}
/* The seat on turn, so the eye knows where the next card is coming from. */
.hearts-play.onturn .card.empty,
.gongzhu-play.onturn .card.empty { border-color: var(--accent); }
/* A slot still owed a card: outlined, not blank, so it reads as "waiting". */
.hearts-play.empty .card.empty,
.gongzhu-play.empty .card.empty { border-style: dashed; }

/* --- the trick just taken ------------------------------------------------- */
/* Set aside out of the trick area, so an empty slot there always means a card
   is owed rather than that one was just collected. Under the East panel. */
.hearts-last,
.gongzhu-last {
    grid-area: last;
    justify-self: start;
    align-self: end;
    box-sizing: border-box;
    width: 172px;
    max-width: 100%;
    padding: 6px 9px 8px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
/* At three seats it sits under East rather than beside North. */
.hearts-board.seats-3 .hearts-last,
.gongzhu-board.seats-3 .gongzhu-last { align-self: start; }
.hearts-last-title,
.gongzhu-last-title {
    font-size: 11px;
    opacity: 0.7;
    margin-bottom: 5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.hearts-last .hearts-trickcross,
.gongzhu-last .gongzhu-trickcross { margin: 0 auto; }

/* --- a completed trick as a cross ---------------------------------------- */
/* The same W-N-E-S layout as the live trick, shrunk: the viewer's card at the
   bottom and the others where those players sit, so a finished trick reads the
   way it was played rather than as a row in play order. Used by the last-trick
   panel and by every trick line in the game log.

   A FIXED footprint whatever the trick holds: every column and row is a card
   width or height even when a cell is empty, so the last-trick panel never
   resizes between tricks (see ui-conventions, "containers must not move"). */
.hearts-trickcross,
.gongzhu-trickcross {
    --tx-cw: 26px;
    display: grid;
    grid-template-columns: repeat(3, var(--tx-cw));
    grid-template-rows: repeat(3, calc(var(--tx-cw) * 1.39));
    grid-template-areas: '. up .' 'left . right' '. down .';
    align-items: center;
    justify-items: center;
    gap: 1px 4px;
    /* Room for the lead caret and the win check, which hang 8px outside the
       outer cards. Without it they overflowed the box, and in the log that put
       a horizontal scrollbar under the whole panel, worst where the vertical
       scrollbar takes space. */
    padding: 0 9px;
}
.hearts-trickcross.seats-3,
.gongzhu-trickcross.seats-3 {
    grid-template-rows: repeat(2, calc(var(--tx-cw) * 1.39));
    grid-template-areas: 'left . right' '. down .';
}
.hearts-trickcross.log,
.gongzhu-trickcross.log { --tx-cw: 19px; }
.hearts-trickcross .card,
.gongzhu-trickcross .card { --cw: var(--tx-cw); }
.hearts-trickcross-cell.pos-up,
.gongzhu-trickcross-cell.pos-up { grid-area: up; }
.hearts-trickcross-cell.pos-left,
.gongzhu-trickcross-cell.pos-left { grid-area: left; }
.hearts-trickcross-cell.pos-right,
.gongzhu-trickcross-cell.pos-right { grid-area: right; }
.hearts-trickcross-cell.pos-down,
.gongzhu-trickcross-cell.pos-down { grid-area: down; }
.hearts-trickcross-cell,
.gongzhu-trickcross-cell { position: relative; line-height: 0; }
/* The lead caret and the win check sit on the card's corners, out of flow, so
   marking a card never changes the size of the cross. Symbols rather than color
   alone, for a color-blind reader and a grayscale screenshot. */
.hearts-trickcross-led,
.gongzhu-trickcross-led,
.hearts-trickcross-won,
.gongzhu-trickcross-won {
    position: absolute;
    font-size: 10px;
    line-height: 1;
    font-weight: 800;
    pointer-events: none;
}
.hearts-trickcross-led,
.gongzhu-trickcross-led { top: -1px; left: -8px; color: var(--ink); opacity: 0.85; }
.hearts-trickcross-won,
.gongzhu-trickcross-won { bottom: -1px; right: -8px; color: var(--accent); }

/* A trick line in the log: a one-line caption beside the cross, so a finished
   round skims as a column of small crosses. Carries a game class, so it cannot
   reach another game's #log-entries. */
#log-entries .hearts-logtrick,
#log-entries .gongzhu-logtrick {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 3px 0;
}
.hearts-logtrick-cap,
.gongzhu-logtrick-cap {
    font-size: 12px;
    min-width: 0;
    flex: 1 1 auto;
    opacity: 0.85;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The viewer's own side of a pass, under the public line: what they passed
   and what they were passed. Indented, so it reads as part of that line. */
#log-entries .hearts-logpass,
#log-entries .gongzhu-logpass { padding-left: 12px; opacity: 0.85; }

/* --- contextual actions -------------------------------------------------- */
/* One button row reserved in every phase (a .btn is its 1.4 line height plus
   7px padding and a 1px border, top and bottom). The row sits directly above
   the hand and holds Pass, then nothing for most of the round, then possibly
   End the round: sized to its contents it moved the hand at each of those. */
.hearts-actions,
.gongzhu-actions {
    align-items: center;
    min-height: calc(1.4em + 16px);
}
/* The exposure explainer. Long, because exposing is the one rule in Gong Zhu
   nobody guesses correctly, and it is read at most once per round. */
.hearts-action-note,
.gongzhu-action-note {
    font-size: 13px;
    opacity: 0.8;
    max-width: 46em;
    flex: 1 1 22em;
}

/* --- the hand ------------------------------------------------------------ */
/* No reservation here. Keeping the hand from shrinking and re-growing is done
   at runtime for every game by src/shared/ui/handspace.ts, which measures what
   the hand actually needs. A shared min-height was tried first and was wrong in
   both directions: too little for a 15-card hand on a phone, and 110px of dead
   space under Cribbage's 6. The comment on that module has the measurements. */
/* A card the follow-suit rule (or an exposure lock) forbids right now. Dimmed
   rather than removed: knowing what you CANNOT play is half of knowing what
   the position is. Only ever applied on the viewer's own turn, where there is
   a choice actually being denied. */
.hearts-mute,
.gongzhu-mute { opacity: 0.32; }
/* A card that was passed TO you this round. Outlined so the three cards you
   did not choose are findable at a glance, which is what the first trick after
   a pass is decided on. */
.hearts-passed-in,
.gongzhu-passed-in { box-shadow: 0 0 0 2px #64b5f6; }
/* The hint's suggestion, spoken beside the status line. */
.hearts-hintnote,
.gongzhu-hintnote { color: var(--accent); font-weight: 600; }

/* --- the exposed cards, in the scoreboard -------------------------------- */
/* Exposure is public and permanent for the round, and it changes what every
   card on the table is worth - so it lives in the scoreboard, not the log. */
.hearts-exposed-badge,
.gongzhu-exposed-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 10px;
    border-radius: 999px;
    background: var(--panel);
    border: 1px solid var(--accent);
}
.hearts-exposed-label,
.gongzhu-exposed-label {
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--accent);
    font-weight: 700;
}
.hearts-exposed-badge .card,
.gongzhu-exposed-badge .card { --cw: 22px; }

/* --- the round and match scoring panel ----------------------------------- */
.hearts-round-title,
.gongzhu-round-title { margin: 0 0 10px; font-size: 15px; opacity: 0.9; }
.hearts-scoretable,
.gongzhu-scoretable { width: 100%; }
/* One row per seat: who, what they took, what it cost, where that leaves them.
   A grid rather than a table so the four columns stay aligned across rows while
   the middle one takes all the slack. */
.hearts-scorerow,
.gongzhu-scorerow {
    display: grid;
    grid-template-columns: minmax(64px, 1fr) minmax(0, 3fr) 62px 62px;
    gap: 8px;
    align-items: baseline;
    padding: 5px 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.hearts-scorerow.head,
.gongzhu-scorerow.head {
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.55;
    border-bottom-style: solid;
}
/* THE SAME TABLE INSIDE THE LOG, where it gets about 260px (the 300px sidebar,
   less its padding and the scored round's 12px indent). The result panel's grid
   needs over 210px before the detail column gets anything, so at that width the
   rows overflowed and the log clipped them. In the log it takes the thin shape
   instead: name, round and match on one line, and what was taken on its own line
   underneath. Scoped through the game class, so no other game's log moves. */
#log-entries .hearts-scoretable .hearts-scorerow,
#log-entries .gongzhu-scoretable .gongzhu-scorerow {
    /* 48px, because "ROUND" in the heading is wider than the 40px it had and
       ran into "MATCH" beside it. */
    grid-template-columns: minmax(0, 1fr) 48px 48px;
    gap: 1px 6px;
    padding: 3px 0;
}
#log-entries .hearts-scoretable .hearts-scorerow.head,
#log-entries .gongzhu-scoretable .gongzhu-scorerow.head { letter-spacing: 0; }
#log-entries .hearts-scoretable .hearts-scorerow.head > div,
#log-entries .gongzhu-scoretable .gongzhu-scorerow.head > div {
    white-space: nowrap;
    overflow: hidden;
}
#log-entries .hearts-scoretable .hearts-scorerow-detail,
#log-entries .gongzhu-scoretable .gongzhu-scorerow-detail {
    grid-column: 1 / -1;
    grid-row: 2;
    font-size: 11.5px;
}
/* The column heading's detail cell would become an empty second line. */
#log-entries .hearts-scoretable .hearts-scorerow.head .hearts-scorerow-detail,
#log-entries .gongzhu-scoretable .gongzhu-scorerow.head .gongzhu-scorerow-detail {
    display: none;
}
#log-entries .hearts-scoretable .hearts-scorerow-name,
#log-entries .gongzhu-scoretable .gongzhu-scorerow-name {
    font-size: 12.5px;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
#log-entries .hearts-scoretable .hearts-scorerow-points,
#log-entries .gongzhu-scoretable .gongzhu-scorerow-points { font-size: 12.5px; }
#log-entries .hearts-scoretable .hearts-scorerow-total,
#log-entries .gongzhu-scoretable .gongzhu-scorerow-total { font-size: 13px; }
.hearts-scorerow-name,
.gongzhu-scorerow-name { font-weight: 700; }
.hearts-scorerow-detail,
.gongzhu-scorerow-detail { font-size: 13px; opacity: 0.8; }
.hearts-scorerow-points,
.gongzhu-scorerow-points,
.hearts-scorerow-total,
.gongzhu-scorerow-total {
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-weight: 700;
}
.hearts-scorerow-points.bad,
.gongzhu-scorerow-points.bad { color: #ff8a80; }
.hearts-scorerow-points.good,
.gongzhu-scorerow-points.good { color: #8bd88b; }
.hearts-scorerow-total,
.gongzhu-scorerow-total { font-size: 18px; }
/* The bare totals row, for a match that ended without a scored round. */
.hearts-totals,
.gongzhu-totals { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0; }
.hearts-total-chip,
.gongzhu-total-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    padding: 4px 12px;
    border-radius: 999px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.hearts-total-name,
.gongzhu-total-name { font-size: 13px; opacity: 0.75; }
.hearts-total-num,
.gongzhu-total-num {
    font-size: 20px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

/* --- the rules and scoring reference ------------------------------------- */
.hearts-suit,
.gongzhu-suit { font-weight: 700; }
.hearts-scoretable th,
.gongzhu-scoretable th,
.hearts-scoretable td,
.gongzhu-scoretable td {
    text-align: left;
    padding: 3px 10px 3px 0;
    vertical-align: baseline;
}
.hearts-scoretable th,
.gongzhu-scoretable th {
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.55;
    font-weight: 700;
}
.hearts-score-value,
.gongzhu-score-value {
    text-align: right;
    white-space: nowrap;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.hearts-score-note,
.gongzhu-score-note { font-size: 13px; opacity: 0.75; }
.hearts-reference table,
.gongzhu-reference table,
.rules.cheatsheet table.gongzhu-scoretable { margin: 6px 0 10px; }
.hearts-reference-note,
.gongzhu-reference-note { font-size: 13px; opacity: 0.85; margin: 6px 0 10px; }
/* The named cards - the Pig, the Goat, the Transformer - wherever they are
   spoken about. The names carry the game, so they read as names. */
.gongzhu-ref-name { color: var(--accent); white-space: nowrap; }

/* --- a narrow board ------------------------------------------------------ */
/* Below the width two panels and the cross need side by side (2 x 172 + 262 +
   the gaps), the board restacks: the other seats in a row ACROSS THE TOP, still
   West, North and East from left to right, and the cross under them beside the
   trick just taken. Keyed on the board's own width (the arena), so a 1024px
   window with the log beside it and a phone get the same treatment. Every
   reservation is re-derived here rather than left at its wide figure. */
@container htable (max-width: 650px) {
    .hearts-board,
    .gongzhu-board {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        grid-template-areas:
            "left  up    right"
            "trick trick last"
            "taken taken taken";
        gap: 8px 6px;
        align-items: start;
    }
    .hearts-board.seats-3,
    .gongzhu-board.seats-3 {
        grid-template-areas:
            "left  .     right"
            "trick trick last"
            "taken taken taken";
    }
    .hearts-taken-panel,
    .gongzhu-taken-panel,
    .hearts-board.seats-3 .hearts-taken-panel,
    .gongzhu-board.seats-3 .gongzhu-taken-panel { justify-self: stretch; align-self: start; max-width: none; }
    /* In a row across the top, any panel's height is the row's, so all three
       reserve the full shape. */
    .hearts-seat.pos-left,
    .gongzhu-seat.pos-left,
    .hearts-seat.pos-up,
    .gongzhu-seat.pos-up,
    .hearts-seat.pos-right,
    .gongzhu-seat.pos-right,
    .hearts-board.seats-3 .hearts-seat.pos-right,
    .gongzhu-board.seats-3 .gongzhu-seat.pos-right {
        width: auto;
        justify-self: stretch;
        align-self: stretch;
        padding: 5px 6px 6px;
        min-height: var(--seat-full);
    }
    .hearts-trick,
    .gongzhu-trick { --pl-cw: 60px; }
    .hearts-last,
    .gongzhu-last,
    .hearts-board.seats-3 .hearts-last,
    .gongzhu-board.seats-3 .gongzhu-last {
        width: auto;
        justify-self: stretch;
        align-self: start;
        padding: 5px 6px 7px;
    }
}
@container htable (max-width: 480px) {
    .hearts-trick,
    .gongzhu-trick { --pl-cw: 46px; column-gap: 8px; padding: 8px 10px 20px; }
    .hearts-seat-head,
    .gongzhu-seat-head { gap: 4px; }
    .hearts-seat-name,
    .gongzhu-seat-name { font-size: 12px; }
    .hearts-seat-total,
    .gongzhu-seat-total { font-size: 15px; }
    .hearts-seat-known.cardrow,
    .gongzhu-seat-known.cardrow { gap: 2px; }
    /* A third of a phone holds six of these a row, so two rows still hold the
       twelve point cards an ordinary round puts in one pile. */
    .hearts-seat,
    .gongzhu-seat { --kn-cw: 16px; --tk-cw: 14px; }
    /* The last trick's title wraps to the two lines reserved for it, and its
       cross shrinks to fit a third of the board. */
    .hearts-last-title,
    .gongzhu-last-title { white-space: normal; min-height: 2.6em; line-height: 1.3; }
    .hearts-last .hearts-trickcross,
    .gongzhu-last .gongzhu-trickcross { --tx-cw: 22px; }
}

/* --- narrow screens ------------------------------------------------------ */
@media (max-width: 620px) {
    /* The viewer's own strip takes two rows here, because at 21px on a phone a
       whole round no longer fits one. Re-derived through --tk-cw. */
    .hearts-taken-panel .hearts-taken.cardrow,
    .gongzhu-taken-panel .gongzhu-taken.cardrow { --tk-cw: 21px; --tk-rows: 2; }
    .hearts-taken-none,
    .gongzhu-taken-none {
        --tk-cw: 21px;
        min-height: calc(2 * var(--tk-cw) * 1.39 + 2px);
    }
    /* A narrow status column wraps the phase prompt to a third line. */
    .status.hearts-status,
    .status.gongzhu-status { min-height: 4.4em; }
    /* The detail column is the first thing to give: the round and match numbers
       are what a player is looking for on a small screen. */
    .hearts-scorerow,
    .gongzhu-scorerow { grid-template-columns: minmax(52px, 1fr) 54px 54px; }
    .hearts-scorerow-detail,
    .gongzhu-scorerow-detail { grid-column: 1 / -1; grid-row: 2; }
}

/* ---------- Dickory ---------- */
/*
 * Dickory's board leads with the RANK ORDER: the twelve ranks written out lowest
 * to highest, because the ordering rotates several times a game and every
 * legality question in the game is "which of these two ranks is higher".
 *
 * (An earlier version put a clock dial above it, with a mouse sweeping round to
 * the new high rank on a CSS transition. It was cut - the strip says everything
 * the dial did, in the form the rules are written in, and two views of one fact
 * is one view too many at the top of the screen.)
 *
 * Nothing above the hand may change height as play proceeds - the hand is what
 * the player is aiming at - so the action column inside the hand panel keeps its
 * width whether or not it holds a decision, and the status line reserves two
 * lines.
 */

.dkry-board {
    background: rgba(0, 0, 0, 0.18);
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    padding: 14px 16px;
    margin-bottom: 14px;
}

/* --- the opponent's hand --- */

/*
 * One row, never wrapping, one card high, so it cannot push the board down as
 * the opponent takes cards. Known cards sit apart on the left, unoverlapped so
 * their ranks read; the backs overlap, since all they say is how many.
 */
.dkry-opp {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    margin-bottom: 10px;
}
.dkry-opphand {
    --cw: 40px;
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: flex-start;
    gap: 12px;
    min-height: calc(var(--cw) * 89 / 64);
    max-width: 100%;
}
.dkry-oppknown { display: flex; gap: 4px; }
.dkry-oppbacks { display: flex; }
.dkry-oppbacks .card + .card { margin-left: calc(var(--cw) * -0.6); }

/* --- the low-to-high order strip --- */

.dkry-order { margin-bottom: 10px; }
.dkry-orderhead {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 4px;
}
/* The color key. Static markup (pages/aroundtheclock.mjs) using the same chip classes
   as the live strip, so it cannot drift out of step with what it describes. */
.dkry-legend {
    display: flex;
    gap: 12px;
    align-items: center;
    flex-wrap: wrap;
    font-size: 12px;
    opacity: 0.8;
}
.dkry-legenditem { display: flex; gap: 4px; align-items: center; white-space: nowrap; }
.dkry-legend .dkry-rank { min-width: 18px; font-size: 11px; padding: 1px 3px; opacity: 0.9; }
.dkry-orderrow {
    display: flex;
    gap: 3px;
    align-items: center;
    flex-wrap: wrap;
    justify-content: center;
}
.dkry-rank {
    min-width: 26px;
    padding: 2px 4px;
    text-align: center;
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    border-radius: 5px;
    background: rgba(255, 255, 255, 0.07);
    border: 1px solid transparent;
}
/*
 * TWO MARKS, ONE ACCENT. There used to be a red chip for the highest rank and an
 * amber one for a rank the viewer holds, which read as two unexplained colors
 * competing at the top of the screen. Red is gone: the highest rank is the site's
 * one accent, and "in your hand" is a plain lift out of the background. So there
 * is a single colored chip in the strip and everything else is a shade of the
 * panel.
 */
/* A rank the viewer holds, with how many copies when it is more than one. */
.dkry-rank.held {
    background: rgba(255, 255, 255, 0.2);
    border-color: rgba(255, 255, 255, 0.4);
    color: var(--ink);
}
/* The current highest rank - always the last chip in the strip. */
.dkry-rank.top {
    background: var(--accent);
    color: var(--accent-ink);
    border-color: var(--accent);
}
.dkry-rankcount { font-size: 10px; font-weight: 400; opacity: 0.8; }

/* --- the queue --- */

.dkry-queuehead {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 8px;
    margin-bottom: 4px;
    flex-wrap: wrap;
}
.dkry-queuetitle {
    font-size: 11px;
    opacity: 0.65;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    white-space: nowrap;
}
.dkry-queuenote { font-size: 12px; opacity: 0.8; }
/*
 * CENTERED BOX, LEFT-PINNED CARDS, and the two are not in conflict.
 *
 * Cards only ever LEAVE the queue from the right (a take, and the discard a pass
 * makes) and only ever ARRIVE at the left (the refill). If the row were centered
 * card-by-card, every take would slide all six cards sideways, which reads as the
 * whole queue having changed when only one card did. So the row is a FIXED-WIDTH
 * box, six card slots wide, centered in the board; the cards sit left-aligned
 * inside it and never move except when the queue itself shifts them. That also
 * makes the pass's shift-right a real animated movement rather than a reflow
 * (see def.ts's animate).
 */
.dkry-queue {
    --dkry-slots: 6;
    display: flex;
    gap: 6px;
    align-items: flex-start;
    /* Six slots and their gaps, so the box never resizes as cards leave. */
    width: calc(var(--dkry-slots) * var(--cw) + (var(--dkry-slots) - 1) * 6px);
    max-width: 100%;
    margin: 0 auto;
    /* The tallest thing it holds, and nothing more: the old 96px left a band of
       dead space between the queue and the melds. */
    min-height: calc(var(--cw) * 89 / 64);
}
.dkry-queue { --cw: 50px; }
/* The rightmost card: the one that sets the high rank. The site accent, not
   the old red. */
.dkry-queue .card.dkry-high { outline: 3px solid var(--accent); outline-offset: 2px; }
/* Any card but the leftmost can be tapped, and takes every card to its right
   with it, so hovering one lifts every card a tap would take: the hovered
   card by the site's own hover lift, the ones to its right here. */
.dkry-queue .card.dkry-takeable { cursor: pointer; }
.dkry-queue .card.dkry-takeable:hover ~ .card { translate: 0 -5px; }
/*
 * The card the viewer would have to take DOWN TO before they had a legal play.
 * Deliberately quieter than the ringed rightmost card: it is a suggestion about
 * a move, not a statement about the rules.
 */
.dkry-queue .card.dkry-unlock {
    outline: 2px dashed rgba(255, 209, 102, 0.75);
    outline-offset: 2px;
}

/* --- the meld on the table --- */

.dkry-table { margin-top: 6px; }
.dkry-tablerow {
    --cw: 56px;
    display: flex;
    gap: 6px;
    align-items: center;
    justify-content: center;
    /* Card height, so an empty table does not open a gap under the queue. */
    min-height: calc(var(--cw) * 89 / 64);
}
.dkry-tablelabel { font-size: 13px; margin-bottom: 3px; text-align: center; }
.dkry-tablelabel b { color: var(--accent); }
.dkry-empty { font-size: 13px; opacity: 0.6; font-style: italic; }

/* --- hand cards --- */

/*
 * Cards no legal play can use right now. A beat must match the SHAPE on the
 * table, so most of a hand is usually dead to the meld in front of it, and
 * working that out by eye is the tedious part of reading a Dickory position.
 * Grayed AND faded, so it does not rely on color alone; still clickable, since
 * the action note explains what is wrong and a queue take may bring the card
 * back into range.
 */
.card.dkry-dim { opacity: 0.4; filter: grayscale(0.7); }
.card.dkry-dim:hover { opacity: 0.62; }
/*
 * The same "no legal play uses this" mark, at half strength, for the position
 * where NOTHING is playable. The hand still matters there - it is what decides
 * which queue card is worth taking to get unstuck - so it must stay readable
 * rather than receding like a card that has simply been ruled out this turn.
 */
.card.dkry-dimsoft { opacity: 0.78; filter: grayscale(0.25); }
.card.dkry-dimsoft:hover { opacity: 1; }
/* Cards staged out of the queue and sitting provisionally in the hand. */
.card.dkry-staged { outline: 2px dashed var(--accent); outline-offset: 1px; }

/* --- hand panel and actions --- */

.panel.dkry-handpanel .dkry-handrow {
    display: flex;
    gap: 12px;
    align-items: flex-start;
}
.panel.dkry-handpanel #hand-cards { flex: 1 1 auto; }
/* Always present, so an action appearing or being answered never moves the
   hand underneath the pointer. */
.dkry-actions {
    flex: 0 0 190px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.dkry-actions .btn { width: 100%; }
.dkry-actionnote { font-size: 12px; opacity: 0.85; line-height: 1.35; }
.dkry-actionnote.bad { color: var(--red); }
.status.dkry-status { min-height: 2.9em; }

/* --- match scoreboard and result --- */

/* A flex row, so it centers with justify-content: the site's text-align
   centering on .progress cannot reach flex items. */
.dkry-score {
    display: flex;
    gap: 10px;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
}
.dkry-scorepill {
    display: flex;
    gap: 6px;
    align-items: baseline;
    padding: 3px 10px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.07);
    border: 1px solid transparent;
}
.dkry-scorepill.active { border-color: var(--accent); }
.dkry-scorename { font-weight: 700; font-size: 13px; }
/*
 * THE CARD COUNT IS THE FIGURE. In a race to empty a hand it is the score, and
 * the opponent's is the number a player checks most often, so it is set large
 * with its unit beside it rather than buried in a sentence.
 */
.dkry-scorecount {
    font-size: 21px;
    font-weight: 800;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}
.dkry-scorepill.them .dkry-scorecount { color: var(--accent); }
.dkry-scoreunit { font-size: 11px; opacity: 0.7; }
.dkry-scorelabel { font-size: 12px; opacity: 0.6; }
/* The discard count: a footnote, since nothing scores. */
.pile-chip.dkry-minorchip { opacity: 0.6; font-size: 12px; }
.dkry-scorewins { font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; }
.panel.dkry-summary { max-width: 520px; margin-left: auto; margin-right: auto; text-align: center; }
.panel.dkry-summary .result-actions { justify-content: center; flex-wrap: wrap; }
.dkry-summarynote { font-size: 13px; opacity: 0.85; margin-top: 6px; }

/* --- the Cheat_Sheet (Rules summary and the rules page) --- */

.dkry-cycle {
    font-variant-numeric: tabular-nums;
    background: rgba(255, 255, 255, 0.08);
    padding: 1px 6px;
    border-radius: 5px;
    white-space: nowrap;
}

@media (max-width: 720px) {
    .dkry-board { padding: 10px 12px; }
    .dkry-queue { --cw: 40px; }
    .dkry-opphand { --cw: 34px; }
    /*
     * Rows above the hand that wrapped on a phone and walked it down the
     * screen (measured at 390px: 43px of movement).
     *
     * The strip wrapped because a "x2" count widened its chip. Here the count
     * is a corner badge instead, out of the flow, so every chip is one width
     * and twelve of them fit one row down to 360px: nothing to reserve.
     *
     * The queue's head put its note beside the title when it fitted and under
     * it when it did not, so on a phone the note always goes under, and
     * reserves the two lines its longer sentences take ("Nothing answers yet,
     * but taking 2 cards would.").
     */
    .dkry-orderrow { flex-wrap: nowrap; }
    .dkry-orderrow .dkry-rank { position: relative; }
    .dkry-orderrow .dkry-rankcount {
        position: absolute;
        top: -7px;
        right: -5px;
        padding: 0 2px;
        border-radius: 4px;
        font-size: 9px;
        line-height: 12px;
        background: var(--panel-border);
        color: var(--ink);
    }
    .dkry-queuehead { flex-direction: column; align-items: center; gap: 0; }
    .dkry-queuenote { line-height: 1.4; min-height: 2.8em; text-align: center; }
    .dkry-tablerow { --cw: 46px; }
    .dkry-rank { min-width: 22px; font-size: 12px; }
    /* The action column gives up its own column before the hand does. */
    .panel.dkry-handpanel .dkry-handrow { flex-wrap: wrap; }
    .dkry-actions { flex: 1 1 100%; }
    .status.dkry-status { min-height: 4.4em; }
}
/* ==========================================================================
   Haggis
   ==========================================================================
   Two-player climbing game with public wild cards. Every shared class (.card,
   .cardrow, .panel, .pile-chip, .logline, .result-actions, .winner-banner, ...)
   is reused as-is, so only what is genuinely Haggis's own lives here - all of it
   prefixed `hgs-`.

   The layout has one job the siblings do not: THREE ZONES OF DIFFERENT PRIVACY
   have to be legible at a glance. The opponent's fourteen cards are secret
   (backs), their three wilds are public (faces), and the Haggis is secret to
   everybody (a count). So faces and backs sit in the same row and the wilds are
   marked with an accent outline, because "I can see their King" is a fact the
   rules deliberately give the player and the board must not bury. */

.hgs-board { margin-bottom: 12px; }

/* ---------- the scoreboard ---------- */
.hgs-score { display: flex; gap: 10px; align-items: stretch; flex-wrap: wrap; margin-bottom: 10px; }
.hgs-scorepill {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 6px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    min-width: 128px;
}
.hgs-scorepill.active { border-color: var(--accent); }
.hgs-scorename { font-weight: 700; font-size: 13px; }
.hgs-scorepoints {
    font-size: 22px;
    font-weight: 800;
    line-height: 1.1;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}
.hgs-scorenote { font-size: 12px; opacity: 0.75; }

/* ---------- the piles: everything in Haggis is worth points ---------- */
.hgs-piles .pile-chip::before { content: none; }
.hgs-haggischip { border-style: dashed; }

/* ---------- the opponent ---------- */
.hgs-opp {
    padding: 8px 10px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.14);
    margin-bottom: 12px;
}
.hgs-opphead { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; margin-bottom: 6px; }
.hgs-oppname { font-weight: 700; }
.hgs-oppcount { font-size: 13px; opacity: 0.8; }
.hgs-oppbet {
    font-size: 12px;
    font-weight: 700;
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-ink);
}
.hgs-opprow { flex-wrap: wrap; gap: 4px; align-items: center; }
/* The opponent's hidden hand: one narrow back per card. Overlapped, because a
   full-width fan of fourteen would dominate a phone screen and the only thing
   this row has to communicate is HOW MANY. */
.card.hgs-oppback { --cw: 30px; margin-right: -14px; }
.card.hgs-oppback:last-of-type { margin-right: 0; }
/* Their wilds are FACES, and outlined so the difference reads instantly. */
.hgs-opprow .card.hgs-wild { --cw: 44px; margin-left: 10px; }

/* A wild card is public information. Both seats' wilds carry the same mark. */
.card.hgs-wild { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---------- the table ---------- */
.hgs-table { margin-top: 4px; }
.hgs-tablelabel { font-size: 14px; margin-bottom: 6px; min-height: 2.6em; }
.hgs-tablelabel b { color: var(--accent); }
.hgs-tablerow { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; min-height: 104px; }
.hgs-play { display: flex; flex-direction: column; gap: 4px; opacity: 0.62; }
/* The last combination played is the one that has to be beaten, so it is the
   only one at full strength. */
.hgs-play.top { opacity: 1; }
.hgs-playwho { font-size: 12px; }
.hgs-play.top .hgs-playwho { font-weight: 700; color: var(--accent); }
.hgs-playcards { gap: 5px; }
.hgs-playcards .card { --cw: 52px; }
.hgs-trickworth { font-size: 12px; opacity: 0.85; align-self: flex-end; }
.hgs-empty { font-size: 13px; opacity: 0.6; font-style: italic; }

/* ---------- the hand ---------- */
.hgs-wilds {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-wrap: wrap;
    padding-bottom: 10px;
    margin-bottom: 10px;
    border-bottom: 1px dashed var(--panel-border);
}
.hgs-wildlabel { font-size: 12px; opacity: 0.8; margin-right: 4px; }
.hgs-wilds .card { --cw: 52px; }
.panel.hgs-handpanel .hgs-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
}
.panel.hgs-handpanel #hand-cards { flex: 1 1 auto; }

/* ---------- the action column ---------- */
.hgs-actions {
    flex: 0 0 232px;
    display: flex;
    flex-direction: column;
    gap: 7px;
}
.hgs-actions .btn { width: 100%; }
.hgs-actionnote { font-size: 12px; opacity: 0.85; line-height: 1.35; }
.hgs-actionnote.bad { color: var(--red); opacity: 1; }
.hgs-actionnote.hgs-hint { color: var(--accent); opacity: 1; }
.hgs-betrow {
    display: flex;
    flex-direction: column;
    gap: 5px;
    padding-top: 8px;
    border-top: 1px dashed var(--panel-border);
}
.hgs-betlabel { font-size: 12px; font-weight: 700; }
.hgs-betnote { font-size: 11px; opacity: 0.75; line-height: 1.3; }

/* ---------- the round and match summary ---------- */
.panel.hgs-summary { max-width: 560px; margin-left: auto; margin-right: auto; }
.panel.hgs-summary .result-actions { justify-content: center; flex-wrap: wrap; }
.hgs-scoretable { display: table; width: 100%; margin: 10px 0; font-size: 14px; }
.hgs-scorerow { display: table-row; }
.hgs-scorecell {
    display: table-cell;
    padding: 3px 8px;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.hgs-scorecell.label { text-align: left; width: 55%; opacity: 0.85; }
.hgs-scorerow.header .hgs-scorecell { font-size: 12px; font-weight: 700; opacity: 0.7; }
.hgs-scorerow.totals .hgs-scorecell { border-top: 1px solid var(--panel-border); font-weight: 700; }
.hgs-scorerow.running .hgs-scorecell { font-weight: 800; color: var(--accent); }
.hgs-scorehint { font-size: 11px; opacity: 0.65; font-weight: 400; }

/* ---------- the log ---------- */
.logline.hgs-logcapture { color: var(--accent); }

/* ---------- the Cheat_Sheet's card points table (Rules summary and the rules page) ---------- */
.hgs-pointtable { border-collapse: collapse; margin: 8px 0; font-size: 14px; }
.hgs-pointtable th,
.hgs-pointtable td { text-align: left; padding: 2px 14px 2px 0; vertical-align: top; }
.hgs-pointtable th { font-size: 12px; text-transform: uppercase; opacity: 0.7; letter-spacing: 0.4px; }

@media (max-width: 720px) {
    /* The action column gives up its own column before the hand does. */
    .panel.hgs-handpanel .hgs-handrow { flex-wrap: wrap; }
    .hgs-actions { flex: 1 1 100%; }
    .hgs-playcards .card { --cw: 44px; }
    .hgs-wilds .card { --cw: 46px; }
    .hgs-scorecell.label { width: 45%; }
}


/* ==========================================================================
   Less Than Three
   ==========================================================================
   Two-player ladder-climbing race over six rounds. Every shared class (.card,
   .cardrow, .panel, .pile-chip, .logline, .result-actions, .winner-banner, ...)
   is reused as-is, so only what is genuinely this game's own lives here - all of
   it prefixed `l3-`.

   The layout has one job the siblings do not: THE HAND COUNTS ARE THE SCORE. A
   knock pays the difference between the two hands and a shed-out pays the loser's
   whole hand plus five, so both counts sit in the scoreboard at nearly the weight
   of the score itself, and a count that has dropped below three is marked -
   because that is the moment its owner can end the round on their next play.

   EVERYTHING SITS ON ONE CENTERED AXIS (the Cuttle treatment): the scoreboard, the
   table label, the climb, the hand and the action column all center on the same
   line, so the eye tracks straight down the middle instead of along a ragged left
   edge. Every rule that does it is scoped through an `l3-` class - `#hand-cards`,
   `#status-line`, `#progress-line` and `.panel-head` are on all ~90 pages in this
   repo, so an unscoped rule here would center every one of them. See the leak
   table in .kiro/steering/ui-conventions.md. */

.l3-board { margin-bottom: 12px; }

/* ---------- the scoreboard ---------- */
.l3-score {
    display: flex;
    gap: 10px;
    align-items: stretch;
    flex-wrap: wrap;
    justify-content: center;
    margin-bottom: 10px;
}
.l3-scorepill {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 6px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    min-width: 140px;
}
.l3-scorepill.active { border-color: var(--accent); }
.l3-scorepill { align-items: center; text-align: center; }
.l3-scorename { font-weight: 700; font-size: 13px; }
.l3-scorepoints {
    font-size: 22px;
    font-weight: 800;
    line-height: 1.1;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}
.l3-scorecards { font-size: 13px; opacity: 0.8; font-variant-numeric: tabular-nums; }
/* Fewer than three cards: this seat may knock the instant it plays, which is the
   only way the round can end without warning. */
.l3-scorecards.low { opacity: 1; font-weight: 700; color: var(--accent); }
/* The warning that came off the opponent's hand panel when that panel went. It is
   the only thing on the board that says the round can end on the next play, so it
   is a badge rather than another line of gray text. */
.l3-canknock {
    font-size: 11px;
    font-weight: 700;
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-ink);
    white-space: nowrap;
}

/* ---------- the sweep tokens ---------- */
/* Public and match-long, so both seats' are always on screen. Backs while
   unspent (a sweep card's rank means nothing), faces once flipped. */
.l3-sweeprow {
    display: flex;
    gap: 3px;
    align-items: center;
    justify-content: center;
    margin-top: 2px;
}
.l3-sweeplabel { font-size: 11px; opacity: 0.6; margin-right: 3px; }
.l3-sweeprow .card { --cw: 20px; }
.card.l3-sweepspent { opacity: 0.5; }

/* ---------- the pile chip ----------
   One chip: whether a sweep is still available. `.pile-line` is already centered
   by the platform sheet, so nothing to do here but drop the swatch the platform
   draws before a pile chip - this chip counts no cards. */
.l3-piles .pile-chip::before { content: none; }
/* Once a sweep has gone nobody may sweep again this round, which changes how safe
   an unbeatable lead is - so the chip visibly switches off rather than just
   changing its words. */
.l3-sweepchip { border-color: var(--accent); }
.l3-sweepchip.spent { border-color: var(--panel-border); opacity: 0.6; }

/* ---------- the table ---------- */
.l3-table { margin-top: 4px; }
.l3-tablelabel {
    font-size: 14px;
    margin-bottom: 6px;
    min-height: 2.6em;
    text-align: center;
}
.l3-tablerow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    flex-wrap: wrap;
    justify-content: center;
    min-height: 100px;
}
.l3-play { display: flex; flex-direction: column; gap: 4px; align-items: center; opacity: 0.6; }
/* The last meld played is the one that has to be beaten, so it is the only one at
   full strength. What has already gone is still part of the position, though,
   which is why the climb is a row rather than one slot that clears. */
.l3-play.top { opacity: 1; }
.l3-playwho { font-size: 12px; }
.l3-play.top .l3-playwho { font-weight: 700; color: var(--accent); }
.l3-playcards { gap: 5px; justify-content: center; }
.l3-playcards .card { --cw: 52px; }
.l3-empty { font-size: 13px; opacity: 0.6; font-style: italic; }

/* ---------- the hand ----------
   The row centers as a unit (cards plus action column), and the cards center
   inside it - the same two-part treatment Grid Poker, Ring Poker, Exchange and
   Duel use, and for the same reason: a hand that grew from the left edge did not
   line up with the centered board above it.

   Both rules are scoped through .l3-handpanel. `#hand-cards` is on every page in
   the repo and an unscoped rule would center all of them; note also that this
   sets no min-height, because src/shared/ui/handspace.ts reserves the hand's
   height at runtime for every game and a CSS floor here would fight it. */
.panel.l3-handpanel .l3-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    justify-content: center;
}
.panel.l3-handpanel #hand-cards { flex: 1 1 auto; }

/* ---------- the action column ---------- */
.l3-actions {
    flex: 0 0 244px;
    display: flex;
    flex-direction: column;
    gap: 7px;
}
.l3-actions .btn { width: 100%; }
.l3-actionnote { font-size: 12px; opacity: 0.85; line-height: 1.35; }
.l3-actionnote.bad { color: var(--red); opacity: 1; }
.l3-actionnote.good { color: var(--accent); opacity: 1; }
.l3-actionnote.l3-hint { color: var(--accent); opacity: 1; }
.l3-actionnote.l3-sweepnote {
    padding-top: 8px;
    border-top: 1px dashed var(--panel-border);
}
/* A sweep spends one of two cards that have to last the whole match, so the
   button is deliberately not the primary one even when it is the best move. */
.btn.l3-sweepbtn { border-color: var(--accent); }

/* ---------- a card that cannot answer the table ----------
   Dimmed rather than hidden or disabled: the hand keeps its shape and its order,
   so the player can still see (and reorder, and pick up) a card that is only dead
   against THIS meld. Grayed as well as faded, because opacity alone reads as
   "loading" on a dark board. */
.card.l3-dead {
    opacity: 0.3;
    filter: grayscale(0.85);
}
/* Selecting one anyway is legitimate - it is how the column gets to explain why
   it does not work - so a dead card that is selected comes back to full strength. */
.card.l3-dead.sel,
.card.l3-dead.hinted { opacity: 1; filter: none; }
/* The knock question takes over the column, so it gets a heading of its own. */
.l3-knockhead { font-size: 15px; font-weight: 800; color: var(--accent); }

/* ---------- the round and match summary ---------- */
.panel.l3-summary { max-width: 560px; margin-left: auto; margin-right: auto; }
.panel.l3-summary .result-actions {
    justify-content: center;
    flex-wrap: wrap;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}
.panel.l3-summary .result-actions .l3-actionnote { text-align: center; }
.l3-scoretable { display: table; width: 100%; margin: 10px 0; font-size: 14px; }
.l3-scorerow { display: table-row; }
.l3-scorecell {
    display: table-cell;
    padding: 3px 8px;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.l3-scorecell.label { text-align: left; width: 55%; opacity: 0.85; }
.l3-scorerow.header .l3-scorecell { font-size: 12px; font-weight: 700; opacity: 0.7; }
.l3-scorerow.totals .l3-scorecell { border-top: 1px solid var(--panel-border); font-weight: 700; }
.l3-scorerow.running .l3-scorecell { font-weight: 800; color: var(--accent); }
.l3-scorehint { font-size: 11px; opacity: 0.65; font-weight: 400; }

/* ---------- the log ---------- */
.logline.l3-logsweep { color: var(--accent); font-weight: 700; }
.logline.l3-logend { font-weight: 700; }

/* The intro's accent treatment for the "<3" glyph. */
.setup-card .l3-eg { font-weight: 700; color: var(--accent); white-space: nowrap; }

@media (max-width: 720px) {
    /* The action column gives up its own column before the hand does. */
    .panel.l3-handpanel .l3-handrow { flex-wrap: wrap; }
    .l3-actions { flex: 1 1 100%; }
    .l3-playcards .card { --cw: 44px; }
    .l3-scorecell.label { width: 45%; }
    .l3-scorepill { min-width: 120px; }
}

/* ---------- Solo Poker ----------
 *
 * Classic's board is the shared build panel with two reference panels under it,
 * side by side where there is room: the still-to-come grid and the run banked so
 * far. Oregon's is a decision line (the offer, the current row, and the one other
 * thing the offer can do) over the same grid and the rows already settled. Both
 * finish on a four-column results table.
 *
 * Everything on the board axis is centered. The two things whose rows must line
 * up with each other - the banked list and the results table - keep their cells
 * aligned by being grids, and center as a block.
 */

/* ---------- the running score ----------
 *
 * First panel in the play area, and on screen for the whole run. Its chips are the
 * results panel's own .solo-score chips at a smaller size, so the bar the player
 * watches all game settles into the final answer rather than being replaced by a
 * different-looking one. Before the first bank it renders dashes in the same two
 * lines, so it never pops in above the build panel and pushes the cards down. */
.solo-prov-head {
    display: flex;
    align-items: baseline;
    justify-content: center;
    flex-wrap: wrap;
    column-gap: 22px;
    row-gap: 4px;
}
/* RELATIVE (how you did against the bots) and POINTS (what your hands are worth by
   type) are different measures that happen to share a ceiling of ten a hand, so
   they are tinted differently and each is a self-contained block. */
.solo-prov-metric { display: flex; align-items: baseline; gap: 8px; white-space: nowrap; }
.solo-prov-label { font-size: 14px; opacity: 0.8; }
.solo-prov-value {
    font-size: 34px;
    font-weight: 800;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}
.solo-prov-value.rel { color: var(--accent); }
.solo-prov-value.pts { color: var(--ink); }
.solo-prov-of { font-size: 13px; opacity: 0.6; }
/* The basis, always beside the number: one hand against three bots can only
   produce multiples of 10/3, so an early reading must not look authoritative. */
.solo-prov-basis { font-size: 12px; opacity: 0.6; white-space: nowrap; }
/* Before the first bank: the text a first score will carry, laid out but unseen,
   so the empty bar already takes the width its figures will need. */
.solo-ghost { visibility: hidden; }

/* ---------- score chips (the bar, and the results panel) ---------- */
.solo-scores { display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
.solo-score {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 10px 14px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    white-space: nowrap;
}
.solo-score.won { border-color: var(--accent); }
.solo-score-name { font-size: 14px; opacity: 0.8; }
.solo-score-value { font-size: 26px; font-weight: 800; font-variant-numeric: tabular-nums; }
.solo-score.won .solo-score-value { color: var(--accent); }
.solo-score-of { font-size: 13px; opacity: 0.6; }
.solo-score-pts {
    font-size: 12px;
    opacity: 0.7;
    padding-left: 7px;
    margin-left: 3px;
    border-left: 1px solid var(--panel-border);
    font-variant-numeric: tabular-nums;
}
/* 4px, not 10, and 10px of padding, not 14: with the deck grid's 23px cards,
   what lets the board fit the app layout's 1280x720 frame. */
.solo-provisional .solo-scores { margin: 4px 0 0; }
.panel.solo-provisional { padding-top: 10px; padding-bottom: 10px; }
.solo-scores.small { gap: 8px; }
.solo-scores.small .solo-score { padding: 5px 10px; gap: 6px; border-radius: 8px; }
.solo-scores.small .solo-score-value { font-size: 17px; }
/* Every figure a chip or the headline can show - a dash, 0.5, 10, 16.7 - takes the
   same width, so the chips never wrap differently from one bank to the next. On a
   phone the dash-to-0.5 change on the first bank wrapped a chip to a new line and
   moved the build panel 32px (tools/solopoker/solo-measure.mjs). */
.solo-scores.small .solo-score-value { min-width: 2.6ch; text-align: right; }
.solo-prov-value { min-width: 3.4ch; text-align: right; }

/* ---------- Classic's reference pair ----------
 *
 * The still-to-come grid and the banked strip, one centered row that wraps to two
 * when the play area is too narrow. Both panels size to their content, and both
 * have a FIXED size for the whole run: the grid blanks a dealt card in place, and
 * the strip's columns are fixed widths, so its width is set before the first hand
 * is banked. Neither panel is hidden mid-game. Together that means nothing in the
 * pair ever slides sideways, and the strip only ever grows downward, into space
 * nothing interactive occupies. */
.solo-pair {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    column-gap: 14px;
}
.solo-pair > .panel { width: fit-content; max-width: 100%; }

/* ---------- the banked strip ----------
 *
 * One grid for the whole list (each row is `display: contents`), so the index, the
 * five cards, the name and the two figures line up down the page. The name column
 * is a fixed width that truncates, with the full name in the title, which is what
 * keeps the strip from widening the day a long hand name first appears. */
.solo-banked-list {
    --bcw: 22px;
    display: grid;
    grid-template-columns: 1.5em calc(5 * var(--bcw) + 4 * 2px) 15.5em 2.4em 2.4em;
    column-gap: 8px;
    row-gap: 3px;
    align-items: center;
    font-size: 12px;
}
.solo-banked-row { display: contents; }
.solo-banked-row .cardrow.mini { gap: 2px; }
.solo-banked-row .card.mini { --cw: var(--bcw); }
.solo-banked-index { text-align: right; opacity: 0.6; font-variant-numeric: tabular-nums; }
.solo-banked-name {
    opacity: 0.85;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* What the hand scored, then the running total: what makes the score visibly climb
   hand by hand. */
.solo-banked-points { text-align: right; opacity: 0.7; font-variant-numeric: tabular-nums; }
.solo-banked-total { text-align: right; font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }
.solo-banked-empty { grid-column: 1 / -1; text-align: center; opacity: 0.6; padding: 4px 0; }

/* ---------- what is still to come ----------
 *
 * The whole deck as a live grid - one row per suit, ranks climbing left to right to
 * the Ace, the two Jokers in a last column - with every dealt card blanked out.
 * Faces use the simplified atlas (see .card.simple): at 26px the full art is a
 * smudge. The grid centers in its box; when the box is narrower than the grid (a
 * phone) the auto margins fall to zero rather than clipping its left edge, and the
 * box scrolls. */
.solo-deck { overflow-x: auto; }
.solo-deck-grid {
    /* 23px, from 26: the grid sits under the build area, and the app layout's
       1280x720 frame was 25px short of holding both. Still the simplified
       atlas, which is drawn for this size. */
    --dcw: 23px;
    display: grid;
    grid-template-columns: 2.4em repeat(14, var(--dcw));
    gap: 2px;
    align-items: center;
    width: max-content;
    margin-inline: auto;
}
.solo-deck-grid .card.mini { --cw: var(--dcw); }

/* Rank headers and the suit gutter: quiet, so the cards carry the panel. */
.solo-deck-rank {
    font-size: 11px;
    text-align: center;
    opacity: 0.55;
    font-variant-numeric: tabular-nums;
}
.solo-deck-suit { display: flex; align-items: baseline; gap: 3px; }
.solo-deck-pip { font-size: 14px; }
.solo-deck-suit.red .solo-deck-pip { color: var(--red); }
/* Per-suit refinement (see the .cardlabel block). The gutter sits beside the card
   sprites in the same grid, so this is the tightest text/sprite adjacency in the
   project - worth having the two agree. */
.solo-deck-suit.red.diamond .solo-deck-pip { color: var(--red-diamond); }
.solo-deck-suit.club .solo-deck-pip { color: var(--blue-club); }
/* How many of this suit are still live - what a four-flush decision turns on, so it
   is the one number given more weight than the labels around it. */
.solo-deck-count { font-size: 12px; opacity: 0.75; font-variant-numeric: tabular-nums; }

/* A dealt card: the cell keeps its shape so the grid stays a readable map, but the
   face goes. Fainter than the shared .card.empty placeholder on purpose - by the
   last round fifty of these are on screen at once, and fifty dashed outlines would
   read as the content rather than the background. */
.solo-deck-grid .card.solo-deck-gone {
    background-image: none;
    background-color: rgba(255, 255, 255, 0.05);
    box-shadow: none;
    border: 0;
}
/* The atlas has two empty cells in its Joker column; so does this. */
.solo-deck-blank { width: var(--dcw); }

/* ---------- the results table (both variants) ----------
 *
 * You beside three bots is four columns of five 27px cards, about 640px with the
 * slot labels, and the lifted summary dialog is 560px by default - which is where
 * the hands overlapped. So this game's dialog is widened (through its own class,
 * never the shared #result-panel id), and a column is never allowed below the width
 * of the five cards in it. On a phone even that does not fit, so the table shows
 * you and ONE bot, and the .solo-focus picker chooses which (Expert by default). */
.panel.result-overlay.solo-resultpanel { width: min(96vw, 760px); }
.result-table.solo-result .result-row {
    grid-template-columns: 34px repeat(4, minmax(146px, 1fr));
}
.result-table.solo-result .result-label { font-variant-numeric: tabular-nums; }
/* Each cell centers its hand and its scores. Every cell in a column is the same
   width and holds the same five cards, so centering moves a whole column together
   and the columns still read straight down. */
.result-table.solo-result .result-cell { text-align: center; }
.result-table.solo-result .cardrow { justify-content: center; }

/* What a hand scored, on its name line. Boxed rather than colored, so it cannot be
   mistaken for the rank badge at the other end of the same line. */
.solo-cell-points {
    display: inline-block;
    margin-left: 5px;
    padding: 0 0.35em;
    border: 1px solid var(--panel-border);
    border-radius: 4px;
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    opacity: 0.85;
}
.result-cell.total.solo-total-best { color: var(--accent); }

/* The two views of the table, and the narrow-screen bot picker. */
.solo-view,
.solo-focus {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin: 12px 0 10px;
    flex-wrap: wrap;
}
.solo-view-label,
.solo-focus-label { font-size: 13px; opacity: 0.7; }
.solo-view .btn.selected,
.solo-focus .btn.selected {
    background: var(--accent);
    color: var(--accent-ink);
    border-color: var(--accent);
    font-weight: 700;
}
/* All four columns fit on a wide screen, so there is nothing to pick there. */
.solo-focus { display: none; }

/* The rank within a row. A NUMERAL first and a color second - the digit alone says
   everything, so the table still reads with color vision that does not separate
   gold from gray. Only first place gets a fill; the rest step down in opacity, so
   forty badges do not compete with the cards they annotate. */
.solo-rank {
    display: inline-block;
    min-width: 1.35em;
    margin-right: 5px;
    padding: 0 0.2em;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    text-align: center;
    font-variant-numeric: tabular-nums;
}
.solo-rank-1 { background: var(--accent); color: var(--accent-ink); }
.solo-rank-2 { border: 1px solid var(--panel-border); opacity: 0.95; }
.solo-rank-3 { border: 1px solid var(--panel-border); opacity: 0.7; }
.solo-rank-4 { opacity: 0.5; }

/* The narrow table's verdict on each visible pair: you against the bot in focus.
   A rank out of four says little when only two of the four are on screen, so under
   720px this mark replaces the badge (see the media block). The word is the first
   channel: "Won" is filled, "Draw" is outlined. The note under the view toggle has
   a sentence for each, and shows the one that matches. */
.solo-pairmark,
.solo-narrow { display: none; }
.solo-pairmark {
    margin-right: 4px;
    padding: 0 0.4em;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 700;
    white-space: nowrap;
}
.solo-pairmark.win { background: var(--accent); color: var(--accent-ink); }
.solo-pairmark.draw { border: 1px solid var(--panel-border); }

/* ---------- Oregon: the decision line ----------
 *
 * The card on offer, the current row, and "or: open a new row", side by side and
 * centered, because together they are the whole of the choice. Every box is a
 * fixed size (five fixed columns, a fixed offer card, a fixed-width action box,
 * nowrap labels), so the line keeps its shape from one offer to the next and
 * nothing on it moves under the pointer. The boxes stretch to one height. */
.oregon-top {
    display: flex;
    justify-content: center;
    align-items: stretch;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 14px;
}
.oregon-top > * { display: flex; }

/* The card on offer. It cannot be declined, so it is framed as something demanding
   a home rather than a card sitting in a pile. */
.oregon-offer {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px 16px;
    background: var(--panel);
    border: 1px solid var(--accent);
    border-radius: 12px;
}
.oregon-offer-title { font-size: 14px; font-weight: 700; opacity: 0.85; white-space: nowrap; }
.card.oregon-offer-card { --cw: 76px; }

/* The other action. A fixed width, so a longer note late in the game cannot widen
   it and nudge the row beside it. */
.oregon-open {
    width: 11em;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    text-align: center;
}
.oregon-open-or {
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    opacity: 0.7;
}
.oregon-open-note { font-size: 12px; opacity: 0.7; }

/* ---------- Oregon: rows, live and settled ---------- */
.solo-oregon { margin-bottom: 14px; }
/* Settled rows, newest first, centered on the same axis as the live row above. */
.oregon-rows { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.oregon-row {
    padding: 10px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
}
.oregon-row.live { border-color: var(--accent); }
/* Settled rows are dimmed through opacity alone, NOT card size: every row is the
   same five columns so they read against each other. */
.oregon-row.frozen { opacity: 0.6; }
/* The heading and the prompt take the row's width without adding to it
   (width 0, min-width 100%), so the row is exactly as wide as its five columns
   and a long hand name truncates instead of widening the box. */
.oregon-row-head,
.oregon-row-prompt { width: 0; min-width: 100%; }
.oregon-row-head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin-bottom: 6px;
    white-space: nowrap;
}
.oregon-row-index { flex: none; font-size: 13px; font-weight: 700; }
.oregon-row-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 13px;
    opacity: 0.85;
}
.oregon-row-points {
    flex: none;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    padding: 0 0.4em;
    border: 1px solid var(--panel-border);
    border-radius: 4px;
}
/* What the row COST: the other side of the trade from the points. */
.oregon-row-spent { flex: none; font-size: 12px; opacity: 0.7; font-variant-numeric: tabular-nums; }
.oregon-row-prompt {
    margin-bottom: 8px;
    font-size: 12px;
    text-align: center;
    white-space: nowrap;
    color: var(--accent);
}

/* A FIXED five-column grid at ONE cell size for every row. A short final row pads
   its missing columns rather than letting its cards spread out of line. */
.oregon-row-cards {
    display: grid;
    grid-template-columns: repeat(5, var(--ocw, 62px));
    gap: 8px;
    justify-content: center;
}
.oregon-row-cards .card { --cw: var(--ocw, 62px); }

/* Every live position carries the swing a cover there would cause, so wrecking a
   made hand is visible BEFORE the click. The signed number is the first channel and
   color the second. The label's line box is reserved on every row, so heights
   match. */
.oregon-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    min-height: calc(var(--ocw, 62px) * 89 / 64 + 16px);
}
.oregon-slot-swing {
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    opacity: 0.7;
}
.oregon-slot.gain .oregon-slot-swing { color: var(--accent); opacity: 1; }
.oregon-slot.loss .oregon-slot-swing { color: var(--red); opacity: 1; }
/* Each card you can cover is lit, lightly, so the row reads as five controls. The
   ring is the accent (#ffd166) at partial strength, then full on hover or focus. */
.oregon-row-cards .card.coverable {
    box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.45), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.oregon-row-cards .card.coverable:hover,
.oregon-row-cards .card.coverable:focus-visible {
    box-shadow: 0 0 0 3px var(--accent), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}

/* Results: what each row cost, beside what it scored. */
.oregon-cell-spent {
    margin-left: 5px;
    font-size: 11px;
    opacity: 0.6;
    font-variant-numeric: tabular-nums;
}
/* A row some run never opened: it spent those cards covering instead. */
.oregon-norow { opacity: 0.35; text-align: center; font-size: 18px; }

/* ---------- narrow screens ---------- */
@media (max-width: 720px) {
    .solo-prov-value { font-size: 28px; }
    .solo-prov-basis { flex: 1 1 100%; text-align: center; }
    .solo-score-value { font-size: 22px; }

    .solo-banked-list {
        --bcw: 20px;
        grid-template-columns: 1.5em calc(5 * var(--bcw) + 4 * 2px) 10.5em 2.4em 2.4em;
        column-gap: 6px;
    }
    .solo-deck-grid { --dcw: 20px; }
    /* A 390px phone leaves 370px for a panel. The grid is 344px wide and the strip
       about 330, so the panels' side padding is trimmed to let both fit whole
       rather than scroll by a few pixels. Measured by tools/solopoker/solo-measure.mjs. */
    .solo-deck,
    .solo-banked { padding-inline: 10px; }

    /* You and the one bot the picker chose. */
    .solo-focus { display: flex; }
    .result-table.solo-result .solo-botcol:not(.focus) { display: none; }
    .result-table.solo-result .result-row {
        grid-template-columns: 30px repeat(2, minmax(0, 1fr));
    }
    .solo-cell-points { font-size: 10px; }
    /* The pair's verdict in the badge's place, and the note sentence that
       explains it in place of the one about ranks. */
    .result-table.solo-result .solo-rank,
    .solo-wide { display: none; }
    .solo-pairmark { display: inline-block; }
    .solo-narrow { display: inline; }

    .card.oregon-offer-card { --cw: 56px; }
    .oregon-row-cards { --ocw: 40px; gap: 5px; }
    .oregon-slot { --ocw: 40px; }
    .oregon-row { padding: 8px 10px; }
    /* The action box drops to its own line under the offer and the row, and lays
       out across it rather than down. */
    #solo-oregon-open { flex-basis: 100%; }
    .oregon-open { width: auto; flex: 1; flex-direction: row; flex-wrap: wrap; }
    .oregon-open-note { flex-basis: 100%; }
}
/* ---------- saint poker board ----------
 *
 * Vertical space is the constraint that shaped all of this. A five-player table
 * has a five-card river, next round's preview, five seats with two cards each,
 * a contextual control, and the player's own ten-card hand - and the HAND is the
 * thing that must not need scrolling to reach. So: the river and the preview sit
 * SIDE BY SIDE, and the seats run ACROSS rather than down, which puts every
 * player's two cards on screen at once. */
.saint-board {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin-bottom: 10px;
}
.saint-panel { margin: 0; padding: 10px 12px; }
.saint-panel h3, .saint-panel h4 { margin: 0 0 6px; }
.saint-river-panel { flex: 1 1 auto; min-width: 0; }
.saint-panel h4 {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.75;
    white-space: nowrap;
}
/* Its own panel to the RIGHT of the river, never inside the river's header:
   these are next round's cards and must not read as a label for this round's. */
.saint-future-box { flex: 0 0 auto; opacity: 0.75; }
.saint-future { gap: 3px; }
.saint-future .card { --cw: 34px; }
/*
 * An empty slot in the river or on a table. A committed donation is a real card
 * back (.card.simple.back - the simplified atlas is the only one with a back
 * cell, so the `simple` class is required, not decoration); a pending one is
 * .card.empty. That contrast is the whole indicator - an accent outline on the
 * committed slots read as a highlight on a real card.
 */
.saint-slot { opacity: 0.85; }
.saint-note { font-size: 13px; opacity: 0.75; align-self: center; }
.saint-error { color: var(--red); font-weight: 700; }
.saint-hint-text { color: var(--accent); }
.saint-chip {
    display: inline-block;
    padding: 2px 8px;
    margin-right: 6px;
    border-radius: 999px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    font-size: 12px;
}
/* The seats run ACROSS, so every player's two cards are visible at once. Each
   block is about two cards wide; five of them still fit a laptop, and wrap on a
   phone. */
.saint-seats {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 8px;
    margin-top: 4px;
}
.saint-seat {
    display: flex;
    flex-direction: column;
    gap: 4px;
    flex: 0 0 auto;
    min-width: 108px;
    max-width: 168px;
    padding: 6px 8px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.saint-seat-you { border-color: rgba(255, 209, 102, 0.45); }
.saint-seat-turn { outline: 2px solid var(--accent); outline-offset: 1px; }
.saint-seat-head { display: flex; align-items: baseline; gap: 8px; }
.saint-seat-name {
    font-weight: 700;
    font-size: 13px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* The two cards a player puts down, horizontally - the shape of the actual play. */
.saint-seat-cards { flex: 0 0 auto; gap: 4px; }
.saint-seat-cards .card { --cw: 46px; }
.saint-seat-note {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 6px;
    font-size: 12px;
    opacity: 0.85;
    min-width: 0;
}
.saint-rank { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.saint-place { font-weight: 800; color: var(--accent); }
.saint-points { font-weight: 700; }
.saint-seat-score {
    margin-left: auto;
    font-size: 17px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--accent);
}
/* Cards this seat took back in the open: public information, so shown rather than
   left as a memory test. Deliberately quieter and smaller than the played cards -
   these are in a hand, not on the table. */
.saint-known {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 3px;
    padding-top: 3px;
    border-top: 1px dotted rgba(255, 255, 255, 0.12);
}
.saint-known-label {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    margin-right: 2px;
}
.saint-known .card { --cw: 26px; }
.saint-actions { align-items: center; margin: 4px 0; }
/* The round scoring table. Grid rather than <table> because it is built from
   the standings at repaint time and every row has the same six cells. */
.saint-round-title { margin: 10px 0 6px; }
.saint-score-table { display: flex; flex-direction: column; gap: 2px; }
.saint-score-row {
    display: grid;
    grid-template-columns: 44px minmax(70px, 1fr) minmax(110px, 1fr) minmax(120px, 1.4fr) 60px 60px;
    gap: 8px;
    align-items: baseline;
    padding: 3px 0;
    border-bottom: 1px dotted rgba(255, 255, 255, 0.08);
}
.saint-score-head { font-size: 12px; opacity: 0.7; text-transform: uppercase; }
.saint-score-cell { font-size: 14px; }
.saint-score-cards { display: flex; gap: 6px; flex-wrap: wrap; }
.saint-totals { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
.saint-total-row { display: flex; gap: 10px; align-items: baseline; }
.saint-total-score {
    margin-left: auto;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--accent);
}
/* The rank-number table in the Cheat_Sheet's Variants section, on the setup
   screen, below the results, and on the Rules_Page. */
.saint-ranktable { border-collapse: collapse; width: 100%; font-size: 14px; }
.saint-ranktable th, .saint-ranktable td {
    text-align: left;
    padding: 3px 8px 3px 0;
    border-bottom: 1px dotted rgba(255, 255, 255, 0.08);
    vertical-align: top;
}
.saint-rank-value { width: 3.5em; font-weight: 800; color: var(--accent); }
.saint-rank-note { opacity: 0.75; }
@media (max-width: 720px) {
    /* The preview drops under the river rather than squeezing it. */
    .saint-board { flex-wrap: wrap; }
    .saint-future-box { flex: 1 1 100%; }
    .saint-future .card { --cw: 28px; }
    .saint-seat { min-width: 96px; }
    .saint-seat-cards .card { --cw: 40px; }
    .saint-known .card { --cw: 22px; }
    /* Cards and totals drop away first: place, player, hand and points are what
       a round's result actually says. */
    .saint-score-row { grid-template-columns: 38px 1fr 1fr 48px; }
    .saint-score-cards, .saint-score-row > .saint-score-cell:last-child { display: none; }
}
/* =========================================================================
 * A Christmas Tree Duel
 *
 * Prefix policy: every game-specific class is `ctree-` (matching `cascade-`,
 * `dracula-`, `duel-`). Shared classes are reused as-is: takeable/selected big
 * cards use `.card` plus `.ctree-take`/`.sel`, empty slots reuse `.card.empty`,
 * face-down cards are `.card.simple.back`, the buttons are `.btn`/`.controls`,
 * and the result panel is the shared `.result-*` family.
 *
 * ONE SCREEN. The tally, the big tree, the frill reference, and both small trees
 * all fit in one screen at 1280x800 and on a 390x844 phone, measured in a real
 * browser (Playwright). Two things buy that:
 *
 *  - The rows overlap the way the game deals them. Each row sits
 *    --ctree-rise below the one above it, which is 0.7 card widths: a covered
 *    card shows its top strip, and on the simplified art (def.ts paints every
 *    card from it) that strip is the rank, in the suit's ink. The rank ink ends
 *    at 0.6675 widths (the W8 box in src/shared/ui/setupdiagram/layout.ts),
 *    the same rise the Examples pictures use.
 *  - Card size follows the viewport HEIGHT on wide screens, because height is
 *    what runs out, and the viewport WIDTH on phones, where the 7-card bottom
 *    row and the two small trees side by side are what run out.
 *
 * Stacking: a later row covers the row above it, as on the table. An empty
 * space sits UNDER every card (z-index 0), so a card with nothing below it shows
 * whole, suit and all. A takeable card, a legal space, and the provisional card
 * of a pending score choice sit over everything (z-index 2).
 * ========================================================================= */

/* --- present tally chips ------------------------------------------------- */
.ctree-scores {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    margin: 2px 0 10px;
}
/* A name and a count, with a clear gap between them rather than a colon. */
.ctree-chip,
.ctree-tally-cell {
    display: inline-flex;
    align-items: baseline;
    gap: 0.6em;
    white-space: nowrap;
}
.ctree-chip {
    padding: 4px 12px;
    border-radius: 14px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    font-weight: 600;
    font-size: 14px;
}
.ctree-chip-count { display: inline-flex; align-items: baseline; gap: 0.3em; font-variant-numeric: tabular-nums; }
.ctree-chip.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
/* The deck: yellow, then red with a red edge once it is nearly out
   (PRESENTS_LOW in def.ts). The edge is the shape that says "running out"
   beside the color. */
.ctree-chip.deck { color: var(--accent); }
.ctree-chip.deck.low { color: var(--red); border-color: var(--red); }

/* --- card sizes and the overlap ----------------------------------------- */
.ctree-big { --cw: clamp(40px, 6.8vh, 60px); }
.ctree-treegrid { --cw: clamp(36px, 6vh, 54px); }
.ctree-big,
.ctree-treegrid { --ctree-rise: calc(var(--cw) * 0.7); }

/* --- the stage: score prompt, big tree, cheatsheet ----------------------
   Three columns with the big tree (the tallest element) centered. The side
   panels are shorter, so the score prompt appearing or disappearing never
   changes the stage height, and the small trees below never move. */
.ctree-stage {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: start;
    gap: 16px;
    margin-bottom: 12px;
}
/* Each panel is pinned to its column. Auto-placement would put the big tree in
   the LEFT column whenever the score prompt is hidden, because a hidden element
   takes no grid cell. */
.ctree-stage > .ctree-score,
.ctree-stage > .ctree-burst { grid-column: 1; grid-row: 1; justify-self: end; margin: 0; max-width: 300px; }
.ctree-stage > .ctree-big { grid-column: 2; grid-row: 1; margin: 0; }
.ctree-stage > .ctree-cheat { grid-column: 3; grid-row: 1; justify-self: start; }

/* --- both trees: overlapping rows ---------------------------------------- */
.ctree-big,
.ctree-treegrid {
    display: flex;
    flex-direction: column;
    align-items: center;
}
.ctree-row { display: flex; gap: calc(var(--cw) * 0.06); justify-content: center; }
.ctree-row + .ctree-row { margin-top: calc(var(--ctree-rise) - var(--cw) * 89 / 64); }
.ctree-cell { display: flex; position: relative; z-index: 2; }
.ctree-cell.gone,
.ctree-cell.open { z-index: 0; }
/* A legal space sits over the empty spaces below it but UNDER the cards above
   it, so their suits stay in view while the player picks a space. The cards
   covering it let a tap through, so the whole space is the target. */
.ctree-cell.legal { z-index: 1; }
.ctree-treegrid:has(.ctree-cell.legal) .ctree-cell:not(.legal) { pointer-events: none; }
.ctree-cell.take,
.ctree-cell.pending { z-index: 3; }

/* --- the big tree pyramid ------------------------------------------------ */
.ctree-big {
    margin: 0 auto;
    padding: 10px 8px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.16);
    width: fit-content;
    max-width: 100%;
}

/* Exposed, takeable cards read as active; buried ones recede; taken slots
   leave a faint gap so the shrinking pyramid stays legible. */
.ctree-take { cursor: pointer; outline: 2px solid var(--accent); outline-offset: 1px; }
.ctree-take:hover { transform: translateY(-4px); transition: transform 120ms ease; }
.ctree-take.sel { outline: 3px solid var(--accent); box-shadow: 0 0 10px 2px rgba(255, 209, 102, 0.6); }
/* No lift for the picked card: lifting it would cover the ranks of the two
   cards it rests on, which the player may be reading while choosing a space. */
.ctree-big .card.sel { transform: none; }
.ctree-exposed { outline: 2px solid rgba(255, 255, 255, 0.35); outline-offset: 1px; }
.ctree-buried { opacity: 0.85; }
.ctree-cell.gone { opacity: 0.18; }
.ctree-cell.gone .card.empty { border-style: dotted; }

/* --- the two small trees ------------------------------------------------- */
.ctree-smalls {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    align-items: flex-start;
    margin-bottom: 12px;
}
.ctree-tree {
    flex: 1 1 280px;
    padding: 8px 10px 10px;
    border-radius: 12px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.ctree-tree.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.ctree-tree-head {
    display: flex;
    justify-content: center;
    margin-bottom: 6px;
}
.ctree-tree-name { font-weight: 700; white-space: nowrap; }

/* An empty space that takes no card: a faint outline under its neighbors. */
.ctree-treegrid .ctree-cell.open .card.empty {
    background: none;
    border-color: rgba(255, 255, 255, 0.2);
}
/* A legal space for the picked card: a dashed accent slot that lifts on hover.
   Opaque, so the faint outlines of the empty spaces under it do not show
   through. */
.ctree-treegrid .card.empty.legal {
    border: 2px dashed var(--accent);
    background: rgba(255, 209, 102, 0.16);
    background: color-mix(in srgb, var(--accent) 20%, var(--felt-dark));
    cursor: pointer;
}
.ctree-treegrid .card.empty.legal:hover { transform: translateY(-3px); transition: transform 120ms ease; }
/* The provisional card while a scoring choice is open. */
.ctree-ghost { opacity: 0.85; outline: 2px dashed var(--accent); outline-offset: 1px; }
.ctree-cell.pending { filter: drop-shadow(0 0 6px rgba(255, 209, 102, 0.7)); }
/* The most recent placement, and the cards of a frill: one a pending choice
   would score, or one just scored (the face-down card that scored it too). */
.ctree-placed.last { outline: 2px solid var(--accent); outline-offset: 1px; }
.ctree-treegrid .card.frill { box-shadow: 0 0 8px 2px rgba(120, 220, 140, 0.85); }

/* --- the score-choice panel ---------------------------------------------- */
.ctree-score {
    padding: 10px 12px;
    border-radius: 12px;
    background: var(--panel);
    border: 1px solid var(--accent);
}
.ctree-score-head { font-weight: 700; margin-bottom: 6px; }
.ctree-frill-list { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 4px; }
.ctree-frill {
    padding: 2px 10px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.06);
    font-size: 14px;
    white-space: nowrap;
}
.ctree-score .controls { margin: 8px 0 0; }

/* --- the standing frill cheatsheet --------------------------------------- */
.ctree-cheat {
    max-width: 240px;
    padding: 10px 12px;
    border-radius: 12px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    font-size: 13px;
}
.ctree-cheat-title { margin: 0 0 6px; font-size: 14px; }
.ctree-cheat-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 5px;
}
.ctree-cheat-list li { line-height: 1.35; }
.ctree-cheat-note { margin: 8px 0 0; font-size: 12px; opacity: 0.7; }

/* --- the score notice ----------------------------------------------------
   Who scored, how many presents, and the frills' cards (frillPicture in
   def.ts, on the shared logpic grid). Opaque, edged in the frill glow's green,
   and never a tap target: it sits where the player may be about to tap. */
.ctree-burst {
    padding: 8px 10px;
    border-radius: 12px;
    background: linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
    border: 1px solid rgba(120, 220, 140, 0.7);
    box-shadow: 0 0 12px rgba(120, 220, 140, 0.35);
    pointer-events: none;
    animation: ctree-burst-in 0.3s ease-out;
}
.ctree-burst-head { font-weight: 700; margin-bottom: 4px; white-space: nowrap; }
.ctree-burst .logpic { margin: 0; font-size: 13px; }
.ctree-burst .logpic .card.logpic-card { --cw: 28px; }
@keyframes ctree-burst-in {
    from { opacity: 0; translate: 0 6px; }
    to { opacity: 1; translate: 0 0; }
}

/* --- the finished-game tally --------------------------------------------- */
.ctree-tally { display: flex; justify-content: center; gap: 16px; margin: 8px 0 14px; }
.ctree-tally-cell { font-weight: 700; font-size: 16px; }

@media (max-width: 720px) {
    /* Width runs out first: the big tree's 7-card bottom row, and the two small
       trees side by side at 5 cards each. The constants cover the page and
       panel padding. */
    .ctree-big { --cw: clamp(30px, calc((100vw - 48px) / 7.6), 46px); padding: 6px; }
    .ctree-treegrid { --cw: clamp(22px, calc((100vw - 72px) / 10.8), 34px); }
    .ctree-scores { margin-bottom: 8px; }
    /* Two lines, always: "Your turn: place ... (or tap another card)." wraps
       here and "Bot is thinking..." does not, and the board must not jump
       between the two. 16px keeps the longest hint inside the two lines. */
    .ctree-status { font-size: 16px; min-height: calc(2 * 1.4em); }

    /* Stack the stage: the big tree, then the reference full width under it. */
    .ctree-stage {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 8px;
        margin-bottom: 8px;
        position: relative;
    }
    .ctree-cheat { max-width: none; width: 100%; padding: 6px 10px; font-size: 12px; }
    .ctree-cheat-title { margin-bottom: 2px; font-size: 13px; }
    .ctree-cheat-list { gap: 1px; }
    .ctree-cheat-note { display: none; }

    /* The score notice covers the reference only (one or two frills fit in its
       height), and lets every tap through. */
    /* `justify-self: stretch` undoes the desktop column's `end`: browsers now
       align an absolutely positioned box by it too, which drew the notice as a
       right-aligned box of its own width. */
    .ctree-stage > .ctree-burst {
        position: absolute;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 20;
        max-width: none;
        justify-self: stretch;
        padding: 6px 10px;
    }
    .ctree-burst .logpic .card.logpic-card { --cw: 22px; }

    /* The score prompt covers the reference and the foot of the big tree, which
       nobody needs while deciding, and leaves both small trees in view. Out of
       the flow, so nothing moves when it opens. */
    .ctree-stage > .ctree-score {
        position: absolute;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 20;
        max-width: none;
        justify-self: stretch;
        background: linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
        box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.45);
    }

    /* The two small trees share one row. */
    .ctree-smalls { flex-wrap: nowrap; gap: 8px; }
    .ctree-tree { flex: 1 1 0; min-width: 0; padding: 4px 4px 6px; }
    .ctree-tree-head { font-size: 13px; margin-bottom: 2px; }
}
/* ==========================================================================
   Commitment
   ==========================================================================
   Two-player battle-committing game over three rounds. Every shared class
   (.card, .cardrow, .panel, .pile-chip, .logline, .result-actions,
   .winner-banner, .btn, ...) is reused as-is; only what is genuinely this
   game's own lives here, all of it prefixed `cmt-`.
   The layout has one job the siblings do not: EVERY BATTLE IS A LITTLE
   SCOREBOARD. Three fights per battle, both stacks visible with their
   engine-computed values (Kings applied), the leading side marked, and the
   would-be winner named - so the pass decision can be read off the board
   without doing the doubling arithmetic by hand. */
.cmt-boardwrap { margin-bottom: 12px; }
/* ---------- the scoreboard ---------- */
.cmt-score { display: flex; gap: 10px; align-items: stretch; flex-wrap: wrap; margin-bottom: 10px; }
.cmt-scorepill {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 6px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    min-width: 150px;
}
.cmt-scorepill.active { border-color: var(--accent); }
.cmt-scorename { font-weight: 700; font-size: 13px; }
.cmt-scorepoints {
    font-size: 22px;
    font-weight: 800;
    line-height: 1.1;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}
.cmt-scorecards { font-size: 13px; opacity: 0.8; font-variant-numeric: tabular-nums; }
/* A passed seat is out for the round - worth a badge, not just a log line. */
.cmt-passedmark {
    align-self: flex-start;
    font-size: 11px;
    font-weight: 700;
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--red);
    color: #fff;
}
/* The battle cards already won: the score, made physical. */
.cmt-claimedrow { display: flex; gap: 3px; margin-top: 2px; }
.cmt-claimedrow .card, .card.cmt-claimedcard { --cw: 26px; }
/* ---------- the piles ---------- */
.cmt-piles .pile-chip::before { content: none; }
.cmt-battlechip { border-color: var(--accent); }
/* ---------- the battles ----------
   ONE ROW, ALWAYS - as many equal columns as there are battles.
   Flex-wrap put three battles on two rows (two up, one orphaned below), which
   both wasted the width and broke the read: three battles are three parallel
   fronts being weighed against each other, so they belong side by side and the
   same size. Equal `minmax(0, 1fr)` columns also stop a battle with a tall
   stack from stealing width from its neighbors. */
.cmt-board { display: grid; gap: 12px; align-items: start; }
.cmt-board[data-battles="1"] {
    grid-template-columns: minmax(0, 480px);
    justify-content: center;
}
.cmt-board[data-battles="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.cmt-board[data-battles="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.cmt-battle {
    min-width: 0;
    padding: 8px 10px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
}
.cmt-battlehead { display: flex; gap: 10px; align-items: center; margin-bottom: 6px; }
/* The battle card lies LANDSCAPE between the players, as the rules deal it:
   a wrapper reserves the rotated footprint so the turn does not overlap. */
.cmt-bcardwrap {
    flex: 0 0 auto;
    /* The rotated card's own width. The wrapper reserves the EXACT rotated
       footprint - the card's height across, its width down - so the header
       never resizes and nothing below it can be nudged by the battle card. */
    --bcw: 46px;
    width: calc(var(--bcw) * 89 / 64);
    height: var(--bcw);
    position: relative;
}
.cmt-bcardwrap .card.cmt-bcard {
    --cw: var(--bcw);
    position: absolute;
    left: 50%;
    top: 50%;
    /* `translate` and `rotate`, never `transform`: the pop-in, the hover lift
       and the flight ghost all write `transform`, and replacing a turn kept
       there painted the card upright until they finished (ui-conventions, "A
       sideways card turns with rotate"). */
    translate: -50% -50%;
    rotate: 90deg;
}
.cmt-battlename { font-weight: 700; font-size: 13px; }
.cmt-battleleader { font-size: 12px; opacity: 0.85; }
/* The three fights as ONE GRID: opponent's stacks, the fight line, yours.
   Three rows shared by all three columns is what keeps THE CENTER OF THE
   BATTLEFIELD STRAIGHT - the suit line is a single grid row, so it is one
   straight horizontal rule across the battle no matter how the stacks around
   it grow. Each side's cell then hugs the line (align-self below) so stacks
   extend outward instead of pushing it. */
.cmt-fights {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: auto auto auto;
    /* NO column gap, so the three middle cells meet edge to edge and their
       borders form ONE continuous rule across the whole battle rather than
       three collinear dashes. The stacks get their separation back from their
       own side margins below. */
    gap: 4px 0;
}
.cmt-fightmid {
    display: flex;
    gap: 6px;
    align-items: baseline;
    justify-content: center;
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    /* The line itself, drawn across all three columns at the same height. */
    border-top: 1px solid var(--panel-border);
    border-bottom: 1px solid var(--panel-border);
    padding: 2px 0;
}
.cmt-fightsuit { font-size: 15px; }
.cmt-fightsuit.red { color: var(--red); }
/* Per-suit refinement (see the .cardlabel block). The three fight suits are
   hearts, clubs and diamonds, so all three lines now read distinctly. */
.cmt-fightsuit.red.diamond { color: var(--red-diamond); }
.cmt-fightsuit.club { color: var(--blue-club); }
/* Reads "your total - their total", the viewer's own first. */
.cmt-fightscore { font-variant-numeric: tabular-nums; }
.cmt-stack {
    border: 1px dashed var(--panel-border);
    border-radius: 8px;
    padding: 4px;
    min-height: 76px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    /* Column separation lives here rather than in the grid's column-gap, so the
       fight line above can run unbroken. */
    margin: 0 4px;
    /* The anchor for the absolutely-positioned target overlay below. */
    position: relative;
}
/* Both sides hug the fight line so the stacks grow AWAY from it. */
.cmt-stack.opp { align-self: end; }
.cmt-stack.own { align-self: start; }
/* The side currently taking this suit. Strictly higher only: a tied suit is
   won by neither, and neither cell lights. */
.cmt-stack.winning { border-color: var(--accent); border-style: solid; }
/* The stack fans downward, newest on top of the pile and lowest on screen,
   overlapped enough to stay compact but leaving every card's corner index
   readable - the order IS the King's doubling rule. */
.cmt-fan { display: flex; flex-direction: column; align-items: center; }
.cmt-fan .card { --cw: 46px; margin-top: calc(var(--cw) * -0.85); position: relative; }
.cmt-fan .card:first-child { margin-top: 0; }
.card.cmt-emptystack { --cw: 46px; opacity: 0.4; }
/* A card's effective value (a doubled pip, or a wild Spade's), pinned to its
   visible corner so the arithmetic never has to be done in the head. */
.cmt-cardvalue {
    position: absolute;
    right: 2px;
    top: 2px;
    font-size: 11px;
    font-weight: 800;
    line-height: 1;
    padding: 1px 4px;
    border-radius: 6px;
    background: var(--accent);
    color: var(--accent-ink);
}
/* ---------- the placement targets ----------
   An OVERLAY, not flow content: absolutely positioned inside the stack cell so
   arming a card reserves no space and moves nothing. Laying these out in flow
   grew the cell, which shoved the fight line, the neighboring battles and the
   hand downward the instant a card was tapped - the board jumped away from the
   spot being aimed at, and the animator read the whole scene sliding as cards
   in motion. */
.cmt-stack.eligible {
    border-style: solid;
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.1);
}
.cmt-targets {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 4px;
    padding: 3px;
    border-radius: 7px;
    /* Dim the stack under it so the call to action is unmistakable, without
       hiding what is being played onto. */
    background: rgba(0, 0, 0, 0.55);
}
/* Filled in the accent rather than outlined: this is the one control on the
   board, it appears on top of card art, and it has to be found instantly. */
.btn.small.primary.cmt-target {
    width: 100%;
    font-size: 11px;
    line-height: 1.2;
    padding: 4px 6px;
    white-space: normal;
    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.45);
}
/* ---------- the Queen's wipe ----------
   The Queen is injected into the cell she just emptied (def.ts `animate`) and
   HOLDS at full opacity for the first stretch, so she reads as landing, then
   fades out last - after the stacks she destroyed have dissolved where they
   stood. Without this the player saw two stacks vanish and never saw the card
   responsible, because the engine discards her in the same move that plays
   her. */
.cmt-wipeghostwrap {
    position: absolute;
    inset: 0;
    z-index: 4;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}
.card.cmt-wipeghost {
    --cw: 46px;
    animation: cmt-wipefade 1000ms ease-in forwards;
    box-shadow: 0 0 0 2px var(--accent), 0 4px 14px rgba(0, 0, 0, 0.6);
}
@keyframes cmt-wipefade {
    0% { opacity: 0; transform: scale(0.86); }
    12% { opacity: 1; transform: scale(1.04); }
    22% { opacity: 1; transform: scale(1); }
    /* The hold: she stays put while everything around her is dissolving. */
    55% { opacity: 1; transform: scale(1); }
    100% { opacity: 0; transform: scale(0.8) translateY(-8px); }
}
body.motion-off .card.cmt-wipeghost { animation: none; opacity: 1; }
/* ---------- the hand and the action column ---------- */
.panel.cmt-handpanel .cmt-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
}
.panel.cmt-handpanel #hand-cards { flex: 1 1 auto; }
.cmt-actions {
    flex: 0 0 244px;
    display: flex;
    flex-direction: column;
    gap: 7px;
}
.cmt-actions .btn { width: 100%; }
.cmt-actionnote { font-size: 12px; opacity: 0.85; line-height: 1.35; }
.cmt-actionnote.bad { color: var(--red); opacity: 1; }
.cmt-actionnote.good { color: var(--accent); opacity: 1; }
.cmt-actionnote.cmt-hint { color: var(--accent); opacity: 1; }
/* Passing ends the seat's whole round, so the button is never the primary
   one; the confirmation step is where it earns emphasis. */
.btn.cmt-passbtn { border-color: var(--red); }
/* ---------- the round and match summary ---------- */
.panel.cmt-summary { max-width: 620px; margin-left: auto; margin-right: auto; }
.panel.cmt-summary .result-actions {
    justify-content: center;
    flex-wrap: wrap;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}
.panel.cmt-summary .result-actions .cmt-actionnote { text-align: center; }
.cmt-battleresult { margin: 10px 0; }
.cmt-battleresulthead {
    display: flex;
    gap: 8px;
    align-items: center;
    font-size: 14px;
    margin-bottom: 4px;
}
.cmt-resultgrid {
    display: grid;
    grid-template-columns: minmax(70px, 1.4fr) repeat(4, 1fr);
    font-size: 14px;
    font-variant-numeric: tabular-nums;
}
.cmt-resultcell { padding: 2px 8px; text-align: right; }
.cmt-resultcell.label { text-align: left; opacity: 0.85; }
.cmt-resultcell.head { font-size: 12px; font-weight: 700; opacity: 0.7; }
.cmt-resultcell.won { color: var(--accent); font-weight: 700; }
.cmt-resultcell.total { font-weight: 800; border-left: 1px solid var(--panel-border); }
/* ---------- the log ---------- */
.logline.cmt-logeffect { color: var(--accent); }
.logline.cmt-logclaim { font-weight: 700; }
/* ---------- the Strategy tips block (tips(..., 'cmt-rules') in pages/commitment.mjs) ---------- */
.cmt-rules b { color: var(--accent); }
@media (max-width: 860px) {
    /* Three fronts still fit abreast here, on smaller cards. */
    .cmt-fan .card { --cw: 38px; }
    .card.cmt-emptystack { --cw: 38px; }
    .cmt-bcardwrap { --bcw: 36px; }
    .cmt-battle { padding: 6px 7px; }
}
@media (max-width: 720px) {
    /* The action column gives up its own column before the hand does. */
    .panel.cmt-handpanel .cmt-handrow { flex-wrap: wrap; }
    .cmt-actions { flex: 1 1 100%; }
    .cmt-scorepill { min-width: 120px; }
}
@media (max-width: 560px) {
    /* Below this the columns would be narrower than a fanned stack, so the
       battles stack vertically - one full-width front at a time, never the
       lopsided two-up-one-below the flex layout used to produce. */
    .cmt-board[data-battles="2"],
    .cmt-board[data-battles="3"] { grid-template-columns: minmax(0, 1fr); }
    .cmt-fan .card { --cw: 44px; }
    .card.cmt-emptystack { --cw: 44px; }
}

/* ==========================================================================
   Claim the Crown (.ctc-*)
   Two court panels laid out as the five Court Progress cells the win
   conditions are scored on, a proximity chip per seat in the progress line,
   and an action bar for the Decree / Dig decisions. Everything shared
   (.card, .cardrow, .panel, .pile-chip, .logline, .winner-banner, .btn, ...)
   is reused as-is; only what is genuinely this game's lives here.
   ========================================================================== */

/* The status line reserves two lines: the Decree explanation runs long, and
   the board must not step up and down the screen as the wording changes. */
.status.ctc-status { min-height: 2.6em; }

/* ---------- the progress line: proximity chips + the acting seat ---------- */
.ctc-proxname { font-weight: 700; margin-right: 6px; }
.ctc-proxval { opacity: 0.9; }
/* The white/green/yellow/orange/red proximity scale (Proximity::color).
   Border-tinted rather than filled so the chips stay readable on the dark
   panel background; red pulses nothing - it is a reading, not an alarm. */
.ctc-proxchip.prox-white { border-color: rgba(255, 255, 255, 0.5); }
.ctc-proxchip.prox-green { border-color: #3fb950; color: #7ee787; }
.ctc-proxchip.prox-yellow { border-color: #d29922; color: #e3b341; }
.ctc-proxchip.prox-orange { border-color: #db6d28; color: #f0883e; }
.ctc-proxchip.prox-red { border-color: #f85149; color: #ff7b72; box-shadow: 0 0 0 1px #f85149; }
.ctc-proxchip.prox-neutral { opacity: 0.7; }

/* ---------- the court panels ---------- */
.ctc-seat { padding: 10px 12px; }
/* One line, one card tall, for both seats: the name, (the opponent's hand,)
   and the discard pile. Reserved at the discard card's height, so neither the
   first discard nor the hand emptying moves the court below. nowrap: the
   hand row is the flexible item and shrinks by overlapping its backs. */
.ctc-seathead {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: nowrap;
    min-height: calc(40px * 89 / 64);
    margin-bottom: 8px;
}
.ctc-seatname { font-weight: 800; }
.ctc-seatname.acting { color: var(--accent); }
.ctc-discard {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-left: auto;
    flex: none;
    border-radius: 8px;
}
.ctc-seatname { flex: none; }
.ctc-discardlabel { font-size: 12px; opacity: 0.7; }
.ctc-discard .card.mini { --cw: 40px; }
.ctc-discardempty { opacity: 0.5; }
.ctc-discardcount { font-size: 12px; opacity: 0.75; }

/* The five Court Progress cells: four number suits and the Faces. A grid,
   so the cells keep their columns whether or not they hold cards - a court
   growing must never reflow the one beside it. */
.ctc-courtgrid {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 8px;
}
.ctc-cell {
    border: 1px dashed rgba(255, 255, 255, 0.15);
    border-radius: 8px;
    padding: 6px;
}
.ctc-cell.filled { border-style: solid; border-color: rgba(255, 255, 255, 0.25); }
.ctc-celllabel {
    font-size: 12px;
    opacity: 0.8;
    margin-bottom: 4px;
    font-weight: 700;
    text-align: center;
}
.ctc-celllabel.red { color: #ff7b72; }
/* Per-suit refinement, at THIS game's red intensity rather than the platform's
   - see the derivation note in :root. */
.ctc-celllabel.red.diamond { color: #ffa172; }
.ctc-celllabel.club { color: var(--blue-club); }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .ctc-celllabel.red.diamond { color: var(--fc-diamond); }
.ctc-cellcards {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px;
}
/* One card row is reserved in every cell, so a cell's FIRST card does not
   grow the panel and walk the hand down (measured: 15px at 1280px, 14px at
   390px, before this). Expressed in the card size, so it follows the phone
   breakpoint below. */
/* 40px, from 46, with the discard and the opponent's hand in the head: the
   two seats were 63px past the app layout's 1280x720 frame at its 0.8 floor.
   Read-only cards at this size take the simplified atlas (def.ts). */
.ctc-cell { --ctc-cellcw: 40px; }
.ctc-cellcards .card { --cw: var(--ctc-cellcw); }
.ctc-cellcards { min-height: calc(var(--ctc-cellcw) * 89 / 64); }
/* The pending card (the one whose ability is resolving) is ringed. */
.card.ctc-pending { outline: 2px solid var(--accent); outline-offset: 1px; }
/* ---------- what the played card is waiting for ---------- */
/* A pick the viewer owes (Club, Bribe, Dig, Inspire) is signposted three
   ways at once: the status line turns into a PROMPT naming the ability and
   the card it came from, the ZONE holding the choices is ringed, and every
   choice in it pulses. Only outline, box-shadow and opacity change - never a
   transform or a size - so none of it moves a card (the FLIP animator reads
   the transformed box; see the webplay-flip-jitter note), and the pulse's
   class is stripped from flight ghosts (ghostStripClasses in def.ts). */
.status.ctc-status.ctc-prompt { color: var(--accent); font-weight: 700; }
.ctc-zone {
    outline: 2px dashed var(--accent);
    outline-offset: 3px;
    border-radius: 8px;
}
.card.ctc-target {
    box-shadow: 0 0 0 2px rgba(63, 185, 80, 0.9);
    animation: ctc-pulse 1.3s ease-in-out infinite;
}
@keyframes ctc-pulse {
    0%, 100% { box-shadow: 0 0 0 2px rgba(63, 185, 80, 0.9); }
    50% { box-shadow: 0 0 0 4px rgba(63, 185, 80, 0.45), 0 0 10px rgba(63, 185, 80, 0.6); }
}
@media (prefers-reduced-motion: reduce) {
    .card.ctc-target { animation: none; }
}
/* In an Inspire pick, the court cards it cannot copy step back. */
.card.ctc-faded { opacity: 0.4; }

/* ---------- the opponent's hand, IN the seat head ---------- */
/* Faces for the cards the viewer has seen (and the whole hand during the
   viewer's own Bribe pick), backs for the rest. It sits between the name and
   the discard pile at the discard card's own size (.card.mini: 46px, 34px at
   560px), so it costs the panel no height: the head is one card tall either
   way. It takes the free width between the two and is centered in it. The
   BACKS overlap, which is what keeps a large hand on one line at 390px; the
   faces do not, since a simplified face reads in its middle. */
.ctc-opphand {
    --ctc-oppcw: 40px;
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 4px;
}
.ctc-opphand .card { --cw: var(--ctc-oppcw); }
.ctc-opphand .card.back + .card.back { margin-left: calc(var(--ctc-oppcw) * -0.55); }

/* ---------- the action bar (Decree / Dig decisions) ---------- */
/* Buttons only: the Decree's step-by-step instruction is on the status line,
   so the bar stays one row and the hand below it does not step down. */
.ctc-actions { min-height: 40px; align-items: center; }
/* A button's wide and narrow labels (cardButton in def.ts): both name the
   card; the narrow one is for phones, where two wide ones do not fit a row. */
.ctc-narrow { display: none; }
/* The two-tap Decree selection in the hand panel. */
.card.ctc-selected { outline: 3px solid var(--accent); outline-offset: 1px; }

/* ---------- the viewer's hand: one group per ability ---------- */
/* In the ability sort the tray holds one .ctc-handgroup per ability (the four
   number suits, then the Faces). The space BETWEEN groups is the tray's column
   gap and the space inside a group the group's own gap, so the separation is a
   container property: a margin class on a card would ride its flight ghost.
   A group never breaks across a wrap. The value sort mixes suits and paints
   one flat row at the ordinary gap. Scoped through the page's .ctc-handpanel,
   because #hand-cards is on every page. */
.ctc-handpanel #hand-cards.ctc-grouped { column-gap: 16px; }
.ctc-handgroup { display: flex; flex-wrap: nowrap; gap: 8px; }

/* ---------- the result panel ---------- */
.ctc-summary { max-width: 520px; margin-left: auto; margin-right: auto; }
.ctc-result-note { opacity: 0.9; margin: 6px 0 12px; }

/* On a wide board the win-condition trackers ride in the head's row, between
   the opponent's hand and the discard pile, rather than in a row of their own
   under the court: the head has room to spare there, and the two seats were
   30px past the app layout's 1280x720 frame at its 0.8 floor. The seat is a
   grid and the head `display: contents`, so the DOM (head, court, trackers)
   is unchanged and the narrow layout below keeps the stacked rows. */
@media (min-width: 721px) {
    .ctc-seat {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto auto;
        grid-template-rows: minmax(calc(40px * 89 / 64), auto) auto;
        column-gap: 12px;
        align-items: center;
    }
    .ctc-seathead { display: contents; }
    .ctc-seatname { grid-column: 1; grid-row: 1; }
    .ctc-opphand { grid-column: 2; grid-row: 1; }
    .ctc-tracks { grid-column: 3; grid-row: 1; margin-top: 0; justify-content: flex-end; }
    .ctc-discard { grid-column: 4; grid-row: 1; margin-left: 0; }
    .ctc-courtgrid { grid-column: 1 / -1; grid-row: 2; margin-top: 8px; }
}
@media (max-width: 720px) {
    .ctc-courtgrid { gap: 4px; }
    .ctc-cell { padding: 4px; min-height: 0; --ctc-cellcw: 34px; }
    .ctc-handpanel #hand-cards.ctc-grouped { column-gap: 12px; }
    .ctc-handgroup { gap: 6px; }
}

@media (max-width: 560px) {
    /* The hand row follows .card.mini (34px here), so the head stays one
       discard card tall; the "Discard" word goes, the pile's card says it. */
    .ctc-opphand { --ctc-oppcw: 34px; gap: 3px; }
    .ctc-seathead { gap: 6px; min-height: calc(34px * 89 / 64); }
    .ctc-discardlabel { display: none; }
}

@media (max-width: 480px) {
    /* The short button labels, so the two Decree buttons share a row. */
    .ctc-wide { display: none; }
    .ctc-narrow { display: inline; }
}

/* ---------- Claim the Crown: trackers and the legend ---------- */
/* One pip row per enabled win condition. The pips are the same statement as
   the proximity chip, but per TRACK - so a player can see WHICH condition
   their court is building toward. Centered on the panel's axis. */
.ctc-tracks {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px 16px;
    margin-top: 8px;
}
.ctc-track {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    opacity: 0.85;
}
.ctc-track.won { color: var(--accent); opacity: 1; font-weight: 700; }
/* The name is a button opening the condition's definition (#ctc-cond-<n>),
   dressed as text with a dotted underline: the familiar "there is more here"
   mark, without making every tracker look like a control. */
.ctc-trackname {
    white-space: nowrap;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-decoration: underline dotted;
    text-underline-offset: 3px;
    cursor: help;
}
.ctc-trackname:hover { text-decoration-style: solid; }
.ctc-pips { display: inline-flex; gap: 3px; }
.ctc-pip {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, 0.4);
}
.ctc-pip.filled { background: var(--accent); border-color: var(--accent); }

/* The always-on suit-ability reminder under the hand panel. */
.ctc-legend {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px 18px;
    font-size: 12.5px;
    opacity: 0.8;
    padding: 8px 2px;
}
.ctc-legkey b { margin-right: 2px; }
.ctc-legkey b.red { color: #ff7b72; }
/* Per-suit refinement, matching .ctc-celllabel above so the legend and the
   court cells agree. Modifiers are written by hand in
   pages/claim-the-crown.mjs, this legend being static HTML. */
.ctc-legkey b.red.diamond { color: #ffa172; }
.ctc-legkey b.club { color: var(--blue-club); }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .ctc-legkey b.red.diamond { color: var(--fc-diamond); }
/* ==========================================================================
 * Gin Rummy (src/ginrummy/def.ts). Scoreboard chrome (.gin-scoreboard and
 * friends) rides the shared .progress-score groups above; everything below
 * is the game's own furniture. Hand cards select with `.card.sel`, draw
 * targets light with `.card.lit`, opponent backs are `.card.simple.back` in
 * a `.cardrow`, and the summary reuses the shared .result-* family.
 * ========================================================================== */

/* The status line runs to two lines on a phone ("Draw from the stock, or take
   the 10&hearts; from the pile." wraps below about 480px of play area), so it
   reserves two whole lines to keep the board from stepping under the pointer
   (the truc-status recipe). The line height is set explicitly so the
   reservation is exact: it used to be 44px, meant as two lines, but two lines
   of 18px text at the default line height are 50.4px, so a wrapped status still
   grew the line and moved the hand 6px (tools/gin-measure.mjs, 390px). */
.status.gin-status { line-height: 25px; min-height: 50px; }

/* Boxes-won badge on a scoreboard chip: small, and out of the number's way. */
.gin-boxes { font-size: 11px; opacity: 0.55; letter-spacing: 0.05em; }

/* The opponent's hand: a caption, then the cards you KNOW they hold face up
   followed by backs for the rest, then what they have visibly refused. */
.gin-opp { margin: 6px 0 2px; }
.gin-opp-label { font-size: 13px; opacity: 0.7; margin-bottom: 4px; }
/*
 * ALWAYS ONE LINE. This row is a status readout, not a hand you play from, and
 * a two-line block of card backs was both bigger than the information in it and
 * the thing that pushed the piles and the hand down the screen on a phone. So
 * the cards are sized to fit eleven across whatever the width, from 46px down to
 * 20px, and the row never wraps.
 *
 * Sized against the VIEWPORT rather than a container query, unlike the hand: the
 * row sits directly in .playarea with no panel padding to account for, so the
 * page-layout arithmetic is exact enough, and it saves making this a containment
 * context for nothing.
 *
 * At 46px and below, a read-only strip takes the SIMPLIFIED atlas (see
 * ui-conventions): flat rank-over-suit art, which is the whole point of showing
 * their known cards face up. The `simple` class is on the element from
 * renderOpponent, since the atlas cannot be chosen from a media query.
 */
.gin-opp-cards {
    gap: 4px;
    align-items: flex-start;
    flex-wrap: nowrap;
    justify-content: center;
    --gin-oppcw: clamp(20px, calc((100vw - 76px) / 11), 46px);
}
@media (min-width: 951px) {
    /* 1680px is #screen-game's max-width in platform.css. */
    .gin-opp-cards {
        --gin-oppcw: clamp(20px, calc((min(1680px, 100vw) - 392px) / 11), 46px);
    }
}
.gin-opp-cards .card { --cw: var(--gin-oppcw); }
/* A card you watched them take. Cool blue, deliberately NOT the gold that
   means "actionable" everywhere else on the page: this is knowledge, not an
   affordance - there is nothing to click here. */
.gin-opp-cards .card.gin-known {
    outline: 2px solid rgba(120, 190, 255, 0.85);
    outline-offset: 1px;
}
/* The refusals line: ONE reserved line, always present, empty until there is a
   refusal to report. It is rendered unconditionally (see renderOpponent) because
   a line appearing mid-hand pushed the piles, the action bar and the hand 9px
   down the screen. nowrap plus ellipsis keeps a long list of refusals from
   taking a second line and doing the same thing again. */
.gin-opp-declined {
    font-size: 12px;
    opacity: 0.6;
    margin-top: 5px;
    min-height: 17px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.gin-opp-declined .cardlabel { opacity: 0.85; }

/* One of YOUR cards the opponent has seen you take. Same blue vocabulary as
   .gin-known, and deliberately a box-shadow rather than an outline: .card.sel
   (the armed discard) and .card.lit both own the OUTLINE, in gold, so a
   selected card that the opponent has also seen paints both rings instead of
   one silently winning. The second shadow restores .card's own drop shadow,
   which a bare box-shadow here would otherwise replace. */
.card.gin-seen {
    box-shadow: 0 0 0 2px rgba(120, 190, 255, 0.9),
        0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}

/* The stock and the discard pile, side by side with their captions, centered on
   the board axis with everything else (the Cuttle treatment). Two fixed-size
   piles, so centering moves nothing as the counts change. */
.gin-piles {
    display: flex;
    gap: 22px;
    align-items: flex-end;
    justify-content: center;
    margin: 10px 0;
}
.gin-pile { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.gin-pile-label {
    font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase;
    opacity: 0.6;
}
.gin-stock { position: relative; }
.gin-stock .card { display: block; }
/* The count sits in the MIDDLE of the card back, not tucked against a corner
   where it hung off the edge of the sprite and read as clipped. Centered it has
   the whole back as its backdrop, so it needs no trick to stay legible. */
.gin-stock-count {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    padding: 2px 9px;
    border-radius: 999px;
    font-size: 14px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
    background: rgba(12, 30, 20, 0.82);
    border: 1px solid rgba(255, 255, 255, 0.35);
    pointer-events: none;
    white-space: nowrap;
}

/*
 * The action bar is TWO rows, controls first, and each reserves its own height
 * whether or not it holds anything, so the hand below never moves.
 *
 * It used to be one wrapping flex row of buttons and a note. On a phone the
 * note wrapped onto a second line in some phases and not others, and the hand
 * walked 43px up and down the screen between one decision and the next
 * (tools/gin-measure.mjs, 390px). Now the buttons have a row that holds one
 * line of buttons, and the note has a row that holds its tallest line count at
 * each breakpoint: one line wherever the play area is wide enough for the
 * longest note, two on a phone. Heights are in px because the note's size is
 * set here in px too.
 */
.gin-actions {
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.gin-actions-row {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: 8px;
    /* Above a button's real height (38.4px: 7px padding, border, and the
       inherited line box), not equal to it. At 38px the empty row and the full
       row differed by 0.4px, which was enough to shift the hand a pixel. */
    min-height: 40px;
    align-items: center;
}
.gin-actions-row .btn { white-space: nowrap; }
.gin-actions-note {
    font-size: 13px;
    line-height: 18px;
    min-height: 18px;
    opacity: 0.7;
    text-align: center;
}
@media (max-width: 640px) {
    .gin-actions-note { min-height: 36px; }
}

/* Knock is the turn that ends the hand: visibly not a plain button. */
.btn.gin-knock {
    border-color: var(--accent);
    color: var(--accent);
    font-weight: 700;
}

/* The hand: two rows of bracketed groups - runs and the unmatched remainder on
   top, sets underneath - so it is a COLUMN of rows, not the shared .cardrow.
   .gin-row is the flex row inside it. */
.gin-hand {
    display: flex;
    flex-direction: column;
    gap: 10px;
    /* The hand is centered like every other game's, but this one is a column,
       so horizontal centering is align-items rather than the justify-content
       the shared #hand-cards rule sets (which lands on the vertical axis here
       and does nothing). */
    align-items: center;
}

/*
 * ONE ROW WHERE ELEVEN CARDS FIT, WRAPPING WHERE THEY DO NOT.
 *
 * Two complaints shaped this, and they pull in opposite directions. First:
 * "the cards keep moving whenever I draw", because the eleventh card of a draw
 * pushed the row past its width and wrapped, re-flowing the hand under the
 * pointer. Second: "I can't see my whole hand without scrolling in mobile",
 * because the first fix was nowrap plus overflow-x, and a phone cannot show
 * eleven readable cards on one line however they are sized.
 *
 * So the cards are sized to fit eleven on one line wherever that leaves them
 * readable, and BELOW that they hold a readable size and the row wraps. The
 * floor is what decides which regime a width is in: at 44px the single line
 * needs ~580px of panel, so anything from a small tablet up gets one line and
 * a phone gets two. There is no horizontal scrolling at any width - a hand you
 * have to scroll is worse than a hand on two lines, and a scrolling strip
 * cannot be centered either (see ui-conventions).
 *
 * The width budget for one line is 11 cards + 96px of gaps and bracket padding.
 * 96 is the worst case, not a guess: the main row tops out at FOUR brackets
 * (three runs of three plus the remainder), which is 4x10px of padding, 3x8px
 * between brackets, and 4px between each of the 7 remaining card pairs.
 *
 * What keeps the wrapped case from moving the board is not this file: the
 * shared handspace.ts holds #hand-cards at the tallest it has been this deal,
 * so a hand that gains or loses a line does not walk the panels below it up and
 * down the screen. Hence no min-height here, which that module forbids anyway.
 */
.gin-row {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: flex-start;
    /* Centered per row, which is what the site-wide #hand-cards rule cannot do
       for this game: #hand-cards is a COLUMN here, so justify-content there is
       the vertical axis. The column centers its rows with align-items below;
       this centers the cards within a row once one wraps. */
    justify-content: center;
    /* Room for the 5px lift .card.clickable:hover applies to a card under the
       pointer, so the panel edge cannot clip it. */
    padding: 6px 0 2px;
}
.gin-hand {
    --gin-cw: clamp(44px, calc((100vw - 160px) / 11), var(--cw));
}
@media (min-width: 951px) {
    /* Two columns: the play area is the page (max 1680, #screen-game in
       platform.css) less its padding, the 16px gap, the 300px log panel, and
       the hand panel's own padding. */
    .gin-hand {
        --gin-cw: clamp(44px, calc((min(1680px, 100vw) - 476px) / 11), var(--cw));
    }
}
/* Container queries measure the panel itself, which is exact rather than
   reverse-engineered from the page layout - so where they exist they win. */
@supports (container-type: inline-size) {
    /* Scoped through the class this game's page puts on #hand-panel. Unscoped it
       made every game's hand panel a containment context, and layout containment
       turns an element into the containing block for its position:fixed
       descendants - the same trap that put the board's container-type on #board
       rather than on .playarea. */
    .gin-handpanel { container-type: inline-size; }
    .gin-hand { --gin-cw: clamp(44px, calc((100cqw - 96px) / 11), var(--cw)); }
}
.gin-hand .card { --cw: var(--gin-cw); }

/* The sets row, beneath the rest: a finished set is the part of the hand you
   have stopped thinking about, so it reads quieter than the live cards above. */
.gin-setsrow { opacity: 0.92; }

/* Knock mode: the cards you can knock with are lit (the shared .card.lit gold
   outline) and the rest are inert, so they dim to say so. Scoped through
   gin-hand, the class this game's page puts on #hand-cards, so no other game's
   hand is reached. */
.gin-hand.gin-knocking .card:not(.lit) { opacity: 0.45; }

.gin-meldgroup {
    display: inline-flex; gap: 4px; padding: 4px 5px 6px;
    border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.22);
    /*
     * Shrinkable AND wrapping, which together are what keep a wide bracket
     * inside the panel. A bracket can hold up to eleven cards - a hand with no
     * melds is one unmatched bracket, and one long run is a legal gin hand - and
     * on a 320px screen eleven cards do not fit on a line at any size worth
     * showing. With `flex: none` such a bracket sat at its max-content width and
     * the ROW overflowed (measured: 135px past the panel on each side, with the
     * page scrolling sideways to match), because a flex item that cannot shrink
     * never reaches the width at which its own wrapping would kick in.
     *
     * Flexbox breaks lines before it resolves flexible lengths, so allowing the
     * shrink costs nothing at the widths where things fit: a bracket that does
     * not fit the current line still moves to the next line whole, and only one
     * that cannot fit a line ALONE wraps internally.
     */
    flex: 0 1 auto;
    min-width: 0;
    flex-wrap: wrap;
}
.gin-meldgroup.gin-deadgroup {
    border-style: dashed;
    border-color: rgba(255, 255, 255, 0.14);
}
.gin-meldgroup.gin-layoffgroup { border-color: var(--accent); }

/* The hand summary's two exposed spreads. */
.gin-summary { max-width: 720px; }
.gin-spreads { display: flex; flex-direction: column; gap: 10px; margin: 8px 0; }
.gin-spread-name { font-size: 13px; opacity: 0.75; margin-bottom: 4px; }
.gin-spread-cards { align-items: flex-start; }
.gin-spread-cards .card { --cw: 34px; }
.gin-spread-cards .gin-meldgroup { padding: 3px 4px 4px; }
/*
 * The score table opts out of the shared .result-row grid, which is
 * `70px repeat(auto-fit, minmax(180px, 1fr))` (54px/140px on a phone). Two
 * things went wrong with it here: auto-fit drops a track when three columns no
 * longer fit, which threw the second player's numbers onto a line of their own,
 * and this table's labels are sentences ("Boxes (20 per hand won)") that a 54px
 * track shreds into one word per line.
 *
 * Gin Rummy always seats exactly two, so the track list is explicit rather than
 * auto-fit: nothing can reflow at any width. The label takes the space that is
 * left and wraps inside its own column; the numbers are short, so they get a
 * narrow right-aligned track each, which is how a score column should read
 * anyway.
 */
.gin-result .result-row {
    grid-template-columns: minmax(0, 1fr) repeat(2, minmax(52px, auto));
    gap: 6px 10px;
}
.gin-result .result-cell { text-align: right; font-variant-numeric: tabular-nums; }
.gin-result .result-label { align-self: center; }
.gin-result .result-cell.gin-won { color: var(--accent); font-weight: 700; }

/* The card that arrived this turn, marked until the discard ends the turn.
   This is what makes a SORTED hand workable: the new card lands in rank order
   rather than at the end, and the ring is how you find it. Gold, because
   unlike .gin-seen this is about your own action - and it is a box-shadow so it
   stacks with .sel / .lit outlines instead of one silently winning. The second
   shadow restores .card's own drop shadow. */
.card.gin-drawn {
    box-shadow: 0 0 0 3px var(--accent),
        0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
/* Both at once: the opponent watched you take it AND it is this turn's draw. */
.card.gin-drawn.gin-seen {
    box-shadow: 0 0 0 3px var(--accent),
        0 0 0 5px rgba(120, 190, 255, 0.9),
        0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}

@media (max-width: 640px) {
    .gin-piles { gap: 14px; }
    /* Tighter gaps buy back ~20px of card width on the narrowest screens, where
       the opponent row is at its floor. */
    .gin-opp-cards { gap: 2px; }
    .gin-spread-cards .card { --cw: 28px; }
    .gin-stock-count { font-size: 12px; padding: 1px 7px; }
}

/* =========================================================================
 * Bonanza
 *
 * Prefix policy: every Bonanza-specific class is `bonanza-`. The shared
 * layout classes (.card, .cardrow, .center-cell, .panel, .opponent*,
 * .pile-chip, .winner-banner, .result-*, ...) are reused as-is, so only
 * what is genuinely new lives here.
 * ========================================================================= */

/* Take-controls helper text, beside the Take button. */
.bonanza-help { align-self: center; font-size: 13px; opacity: 0.8; }

/* The one card nobody else has seen: an inset gold ring (an inset shadow
   composes with the .sel outline, which sits OUTSIDE the card). */
.card.bonanza-secret { box-shadow: inset 0 0 0 2px var(--accent); }

/* Opponent panels: the whole hand as a fixed SEVEN-SLOT row - the face-down
   secret card, the public takes face up on the simplified atlas (legible at
   this size, unlike the full art), and an empty slot per card still to come.
   The row's shape never changes, so the panel never resizes mid-animation
   (the original "opponent's cards are janky" report), and the "showing"
   caption line always renders (see min-height). */
.bonanza-opp-row { gap: 4px; margin-top: 6px; }
.bonanza-opp-row .card { --cw: 40px; }
.bonanza-opp-hand { font-size: 12px; opacity: 0.75; margin-top: 4px; min-height: 15px; }
@media (max-width: 640px) {
    .bonanza-opp-row { gap: 3px; }
    .bonanza-opp-row .card { --cw: 32px; }
}

/* The viewer's own Duel Poker-style caption: the made hand so far. */
.bonanza-hand-best { font-size: 13px; opacity: 0.85; margin-top: 8px; }

/* The legend under the hand explaining the secret-card marker. */
.bonanza-hand-note { font-size: 13px; opacity: 0.8; margin-top: 6px; }

/* Showdown rows: one block per player - name, best hand name, and all
   seven cards with the two unused ones dimmed. */
.bonanza-result-row {
    padding: 10px 12px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    margin-bottom: 8px;
}
.bonanza-result-row.winner { border-color: var(--accent); }
.bonanza-result-name { font-weight: 700; margin-bottom: 2px; }
.bonanza-result-row .result-handname { margin: 0 0 6px; }
.bonanza-result-row .card.dim { opacity: 0.35; filter: saturate(0.6); }

/* =========================================================================
 * Whiskey Poker
 *
 * Prefix policy: every Whiskey-Poker-specific class is `whiskey-`. The
 * shared layout classes (.card, .cardrow, .panel, .opponent*, .controls,
 * .winner-banner, .result-handname, .result-actions, .opt-desc, ...) are
 * reused as-is, so only what is genuinely new lives here.
 * ========================================================================= */

/* The one-line notes: under the spare, under the hand, and under the action
   bar. Each RESERVES its line even while empty, and none of them wraps (every
   note def.ts writes fits one line at 360px), so a note appearing or changing
   on a turn change moves nothing below it. tools/whiskeypoker/whiskey-measure.mjs pins it. */
.whiskey-note {
    font-size: 13px;
    opacity: 0.8;
    margin: 8px 0 0;
    min-height: 1.4em;
    line-height: 1.4;
    text-align: center;
}
/* The note under the action bar sits close under it: the bar's own bottom
   margin already separates them. */
.whiskey-action-note { margin: -4px 0 8px; }

/* LINE RESERVATIONS FOR THE TWO LINES ABOVE THE BOARD, which move everything
   under them if their line count changes between turns. The ids are the
   shell's, so these are scoped through the classes renderStatus puts on them.
   Desktop: one line each, which the shared .status / .progress already
   reserve. Up to 720px the longest status ("The spare hand was turned face
   up. AI 1 is thinking...", a hint) and the longest progress line ("Phase 2
   of 2 · 2 Jokers in play · position seen twice, and a third time is a
   draw.") need two lines, so two are reserved for each, whatever the turn
   says. In em of each line's own type, so they follow its font size. */
@media (max-width: 720px) {
    .status.whiskey-status { min-height: calc(2 * 1.4em); }
    .progress.whiskey-progress { min-height: calc(2 * 1.4em); }
}

/* THE ACTION BAR IS ONE HEIGHT IN BOTH PHASES. Phase 1 paints three buttons
   (Pass, Take the unseen spare, Knock: 346px at the default padding) and
   phase 2 two, so on a phone the bar wrapped to two rows in phase 1 only, and
   the note under it moved at the phase change. Narrower padding fits all
   three on one row down to a 360px viewport (322px of 340); below that, two
   rows are reserved whatever the phase. In terms of the button's own type
   (1.4em line, 7px padding each side, 1px border), so it follows the font. */
@media (max-width: 420px) {
    .whiskey-actions .btn { padding: 7px 10px; }
}
@media (max-width: 359px) {
    .whiskey-actions { min-height: calc(2 * (1.4em + 16px) + 8px); align-content: flex-start; }
}

/* One axis (Cuttle treatment): the spare hand centers; the player's hand is
   centered by the site-wide #hand-cards rule. Both rows hold a fixed five
   cards, so centering moves no container. #whiskey-spare is this page's own
   id. */
#whiskey-spare { justify-content: center; }

/* Seat badges in the opponent strip: the dealer, and who has knocked. */
.whiskey-badge {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 8px;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    background: rgba(255, 255, 255, 0.12);
    opacity: 0.85;
}
.whiskey-badge.whiskey-knocked { background: var(--accent); color: #1a1a1a; opacity: 1; }

/* An opponent's five cards: known faces first, backs for the rest. */
.whiskey-opp-cards .card { --cw: 30px; }

/* The hand's current name rides in the hand title as the shared .handname
   chip, beside the label on a wide screen. Neither half ever wraps, so the
   head is one shape per breakpoint whatever the hand is called. Scoped
   through the page's own whiskey-handpanel class, so no other game's title
   is touched. */
.whiskey-handpanel #hand-title { white-space: nowrap; }
@media (max-width: 720px) {
    /* The sort button sits out of the flow at the head's right edge (see
       .panel-head), and the centered title cannot see it. Side by side, a
       long label and a long name ("Player 2's hand", "Full House (Queens
       over Jacks)") run under the button below about 720px, so the chip takes
       a line of its own there. ALWAYS, not only when it is long: every hand
       has a name, so the head holds the same two lines on every repaint and
       the cards below it never move. The chip's top margin clears the
       button, which only reaches down the first line. */
    .whiskey-handpanel #hand-title {
        display: flex;
        flex-direction: column;
        align-items: center;
    }
    .whiskey-handpanel #hand-title .whiskey-handname { margin: 6px 0 0; }
}

/* A card in YOUR hand that everybody watched you take from the spare: an
   inset ring (an inset shadow composes with the .sel outline, which sits
   OUTSIDE the card). */
.card.whiskey-known { box-shadow: inset 0 0 0 2px rgba(255, 209, 102, 0.75); }

/* Showdown rows: one block per player in ranked order - place, name, hand
   name, then the five shown cards. The column is capped and centered, and
   both the name line and the card row center inside it, so every seat's
   five cards sit on one centered axis. #whiskey-showdown is this page's own
   panel (see pages/whiskeypoker.mjs), so none of this reaches a shared id. */
.whiskey-result-rows { max-width: 520px; margin: 0 auto; }
.whiskey-result-row .cardrow { justify-content: center; }
.whiskey-result-row {
    padding: 10px 12px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    margin-bottom: 8px;
}
.whiskey-result-row.winner { border-color: var(--accent); }
.whiskey-result-head {
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 6px;
}
.whiskey-place { font-weight: 700; opacity: 0.75; min-width: 2.2em; }
.whiskey-result-name { font-weight: 700; }
.whiskey-result-row .result-handname { margin: 0; }

/* ==========================================================================
 * Counterbluff (src/counterbluff/def.ts).
 *
 * A two-player bluffing duel, so the board is two rows of committed cards -
 * opponent above, viewer below - with a name gutter down the left. The
 * scoreboard is the shared .progress-score widget (Counterbluff opts in with
 * .cbf-scoreboard), carrying both totals, the stock, and what the hand is
 * currently worth: the triangular pot is the number a player actually
 * watches. Nothing above the hand may change height mid-hand - the status
 * line reserves two lines, the table always shows five slots a side, and
 * the action bar keeps its column inside the hand panel whether or not it
 * holds a decision (the Le Truc recipe throughout).
 * ========================================================================== */

/* The status line runs to two lines when it is explaining a lowball answer
   or the fold price; reserve the height so the table never steps under the
   cursor (the truc-status recipe). */
.status.cbf-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.cbf-status { min-height: 4.4em; }
}

/* --- the scoreboard line -------------------------------------------------- */

/* The shared .progress sets opacity 0.7 and a child cannot climb back out of
   an inherited opacity; chip chrome (.progress-score, .mine, .leading) is
   shared. */
.progress.cbf-scoreboard {
    opacity: 1;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
}
.cbf-scorename { font-size: 13px; opacity: 0.75; margin-right: 6px; }
.cbf-scorenum {
    font-size: 19px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.progress-score.leading .cbf-scorenum { color: var(--accent); }
.cbf-stock {
    font-size: 13px;
    opacity: 0.85;
    padding: 4px 9px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
}
/* What the hand is worth right now, pinned to the right-hand end: it is the
   number the fold decision is about, and public in every mode. */
.cbf-pot {
    margin-left: auto;
    font-size: 13px;
    opacity: 0.9;
    padding: 4px 10px;
    border-radius: 999px;
    border: 1px solid rgba(255, 209, 102, 0.4);
    font-variant-numeric: tabular-nums;
}

/* --- the table ------------------------------------------------------------ */

.cbf-table {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 10px 0 14px;
}
.cbf-table .card { --cw: var(--cbf-cw, 64px); }
.cbf-seatrow {
    display: flex;
    align-items: center;
    gap: 14px;
}
.cbf-seatname {
    flex: 0 0 110px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: 14px;
}
.cbf-name { font-weight: 700; }
.cbf-held { font-size: 12px; opacity: 0.7; }
.cbf-lead {
    font-size: 11px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--accent);
}
.cbf-cards {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    /* Five slots, always: a played card must never reflow the row. */
    min-height: calc(var(--cbf-cw, 64px) * 89 / 64);
}
/* Cards + the what-have-they-got caption underneath. The caption line is
   always present (min-height) so a name appearing never shifts the table. */
.cbf-commit {
    display: flex;
    flex-direction: column;
    gap: 3px;
    flex: 1 1 auto;
    /* Lets a caption line ellipsize instead of widening the row. */
    min-width: 0;
}
/* Two lines, both always reserved: what the row makes (and who is ahead),
   then where it can still go. Each line is one line at every width
   (nowrap + ellipsis), so a long target never grows the table under the
   hand. */
.cbf-caption {
    display: flex;
    flex-direction: column;
    min-height: 2.6em;
    font-size: 13px;
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
    min-width: 0;
}
.cbf-capline {
    min-height: 1.3em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    opacity: 0.9;
}
/* The standing against the other row: a small chip after the hand name. */
.cbf-standing {
    margin-left: 8px;
    padding: 0 6px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.03em;
}
.cbf-ahead { background: var(--accent); color: var(--accent-ink); }
.cbf-level { border: 1px solid rgba(255, 255, 255, 0.45); }
/* A target or a draw, by what it would do against the other row as it stands
   now: green beats it, red loses even to it. The tooltip says against what. */
.cbf-target { font-style: italic; }
.cbf-beats { color: #7fd08f; }
.cbf-loses { color: var(--red); }
/* What the five picked cards make, ahead of committing them. Against a
   face-up call it is colored by the comparison, so the accent yields. */
.cbf-selected-note { color: var(--accent); opacity: 1; font-weight: 600; }
.cbf-selected-note.cbf-beats { color: #7fd08f; }
.cbf-selected-note.cbf-loses { color: var(--red); }

@media (max-width: 620px) {
    .cbf-table { --cbf-cw: 46px; }
    .cbf-seatname { flex-basis: 78px; }
}

/* --- the hand panel + action bar (the Le Truc recipe) ---------------------- */

.cbf-handrow {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    min-height: calc(var(--cw) * 89 / 64);
}
.cbf-handrow #hand-cards { flex: 1 1 auto; }

.cbf-actions {
    /* Fixed column, present even when empty: a decision appearing must not
       slide the cards sideways. It is also the tallest thing in the row, so it
       reserves its tallest ordinary shape: three buttons, the selection note
       at two lines, and the closing note at two (measured 244px, which moved
       the hand panel 30px between an ordinary turn and a Lowball answer). */
    flex: 0 0 240px;
    min-height: 246px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 0;
    border-radius: 12px;
    border: 1px solid transparent;
}
.cbf-actions.filled {
    padding: 9px 11px;
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 209, 102, 0.35);
}
.cbf-actions .btn { width: 100%; }
.cbf-actions-note { font-size: 12px; opacity: 0.8; line-height: 1.35; }
@media (max-width: 760px) {
    .cbf-handrow { flex-wrap: wrap; }
    /* Under the hand the column becomes a ROW, and its contents change with
       the turn: nothing, a Fold button, Call Lowball and its note, or three
       Lowball buttons. Sized to its contents it moved the hand panel by up to
       187px between turns (measured at 390px), so it reserves the tallest
       ordinary shape: two rows of buttons plus a two-line note, laid out as a
       wrapping grid so the three answers fit in two rows. */
    .cbf-actions {
        flex: 1 1 100%;
        display: grid;
        grid-template-columns: 1fr 1fr;
        align-content: start;
        /* Two button rows, the one-line selection note, the two-line note. */
        min-height: calc(2 * 36px + 16px + 32px + 3 * 7px + 20px);
    }
    .cbf-actions .cbf-actions-note { grid-column: 1 / -1; }
    .cbf-actions.filled { min-height: calc(2 * 36px + 16px + 32px + 3 * 7px + 20px); }
    /* The selection note stays one line, so it cannot take a second. */
    .cbf-actions .cbf-selected-note {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    /* One line per button, so every button in the grid is the same 38px. */
    .cbf-actions .btn {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        padding-left: 8px;
        padding-right: 8px;
        font-size: 14px;
    }
}

/* A card picked into the five-card lowball selection. */
.card.cbf-picked {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    transform: translateY(-6px);
}

/* --- the hand summary ------------------------------------------------------ */

.panel.cbf-summary {
    max-width: 560px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
.panel.cbf-summary .result-actions { justify-content: center; flex-wrap: wrap; }
.panel.cbf-summary .result-note { text-align: center; }
/* Two rows of a name, a hand description, and the points - nothing like the
   poker siblings' grid of five-card hands, so size to content. */
.cbf-result { display: table; margin: 0 auto; }
.cbf-result .result-row {
    display: table-row;
    grid-template-columns: none;
}
.cbf-result .result-label,
.cbf-result .result-cell {
    display: table-cell;
    padding: 3px 10px;
    text-align: left;
    font-size: 14px;
}
.cbf-totals { font-weight: 600; }

/* --- one finished hand as cards: the log headings and the final summary ---- */

/* Both seats' cards from one hand, each on its own line: name, the cards as
   small simplified pictures, what they made, and the points on the scorer's
   line. The column widths are fixed so the cards line up hand after hand. */
.cbf-hand { display: block; margin-top: 4px; }
.cbf-handrow-sum {
    display: grid;
    grid-template-columns: 44px auto minmax(0, 1fr) 34px;
    align-items: center;
    column-gap: 6px;
    min-height: 34px;
    font-weight: 400;
}
.cbf-sumseat {
    font-size: 12px;
    opacity: 0.8;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.cbf-sumcards { display: flex; gap: 2px; min-width: calc(5 * 24px + 4 * 2px); }
.cbf-sumcards .card.mini { --cw: 24px; }
.cbf-sumname {
    font-size: 12px;
    opacity: 0.85;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.cbf-sumpoints {
    font-size: 12px;
    font-weight: 700;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.cbf-won .cbf-sumpoints, .cbf-won .cbf-sumseat { color: var(--accent); opacity: 1; }
/* The heading line of a hand in the log is the <summary>'s own text, which the
   platform makes bold; the card rows under it are content, not heading. */
details.loggroup > summary .cbf-hand { font-weight: 400; }
.cbf-loghead { white-space: normal; }

/* The final summary: every hand of the game in order. */
.cbf-history {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 12px auto 0;
    max-width: 440px;
    text-align: left;
}
.cbf-history-hand {
    padding: 6px 10px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.18);
}
.cbf-history-head { font-size: 13px; font-weight: 600; }
.cbf-history .cbf-sumcards { min-width: calc(5 * 30px + 4 * 3px); gap: 3px; }
.cbf-history .cbf-sumcards .card.mini { --cw: 30px; }
.cbf-history .cbf-handrow-sum {
    grid-template-columns: 52px auto minmax(0, 1fr) 36px;
    min-height: 44px;
}
/* Lifted into the shell's summary dialog, a long game scrolls inside it:
   keep the Play again row in view (the Expeditions sticky-row recipe). */
.panel.result-overlay.cbf-summary .result-actions {
    position: sticky;
    bottom: -14px;
    margin: 8px -16px 0;
    padding: 10px 16px 14px;
    z-index: 1;
    background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)),
        linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
}

/* --- the cards you have not seen ------------------------------------------- */

.panel.cbf-unseen { padding: 8px 12px; }
.cbf-unseen-toggle {
    background: none;
    border: 0;
    color: inherit;
    font: inherit;
    font-size: 13px;
    padding: 2px 0;
    cursor: pointer;
    text-align: left;
    width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.cbf-unseen-toggle:hover { color: var(--accent); }
/* Fixed grid: a row per suit, a cell per rank, high to low. Cells the viewer
   has seen stay in place, dimmed and struck through, so nothing moves as
   cards go. */
.cbf-unseen-grid {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-top: 6px;
    font-variant-numeric: tabular-nums;
}
.cbf-unseen-row {
    display: grid;
    grid-template-columns: 18px repeat(13, minmax(0, 1fr));
    gap: 2px;
    align-items: center;
}
.cbf-unseen-suit { font-size: 15px; text-align: center; }
.cbf-unseen-cell {
    text-align: center;
    font-size: 13px;
    font-weight: 700;
    padding: 2px 0;
    border-radius: 4px;
    background: rgba(255, 255, 255, 0.1);
}
.cbf-unseen-cell.cbf-seen {
    opacity: 0.3;
    font-weight: 400;
    text-decoration: line-through;
    background: none;
}
.cbf-unseen-row .cbf-unseen-joker { grid-column: span 3; font-size: 12px; }
.cbf-unseen-cell.red, .cbf-unseen-suit.red { color: var(--red); }
.cbf-unseen-cell.red.diamond, .cbf-unseen-suit.red.diamond { color: var(--red-diamond); }
.cbf-unseen-cell.club, .cbf-unseen-suit.club { color: var(--blue-club); }
/* ==========================================================================
   Dueling Nobles
   (src/duelingnobles/, pages/duelingnobles.mjs)
   ==========================================================================
   Two-player court-and-combat game. Every shared class (.card, .cardrow,
   .panel, .pile-chip, .logline, .result-actions, .winner-banner, .btn, .sel)
   is reused as-is; only what is genuinely this game's own lives here, all of
   it prefixed `dnob-`. The board is two COURTYARDS (opponent above, viewer
   below) around a battle panel that itemizes the force arithmetic. */
.dnob-court {
    padding: 8px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    margin-bottom: 10px;
}
.dnob-courthead {
    display: flex;
    gap: 12px;
    align-items: baseline;
    flex-wrap: wrap;
    margin-bottom: 6px;
    font-size: 13px;
}
.dnob-courtname { font-weight: 700; font-size: 14px; }
.dnob-gold { color: var(--accent); font-weight: 700; font-variant-numeric: tabular-nums; }
.dnob-handcount { opacity: 0.8; font-variant-numeric: tabular-nums; }
.dnob-agentsspent { opacity: 0.7; font-size: 12px; }
.dnob-courtrow {
    display: flex;
    gap: 14px;
    flex-wrap: wrap;
    align-items: flex-start;
}
/* ---------- one Noble in arms ---------- */
.dnob-noble {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 4px;
    border-radius: 8px;
}
.dnob-noble.lit { outline: 2px solid var(--accent); outline-offset: 2px; }
.dnob-noble.sel { outline: 2px solid var(--accent); background: rgba(255, 255, 255, 0.06); }
.card.dnob-noblecard { --cw: 64px; }
.card.dnob-noblecard.lit { box-shadow: 0 0 0 2px var(--accent); }
.card.dnob-knightcard { --cw: 32px; }
.card.dnob-supportcard { --cw: 46px; }
.dnob-noblecap { font-size: 11px; opacity: 0.75; }
/* The combat ability, under the rank. Kept to one line and never wrapped: the
   slot sits in a reserved-height row, and a wrapped tag would grow it. */
.dnob-ability {
    font-size: 10px;
    opacity: 0.6;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
/* Defeat pips: one slot per tolerated defeat plus the fatal one. */
.dnob-pips { display: flex; gap: 3px; }
.dnob-pip {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    border: 1px solid var(--panel-border);
    background: rgba(255, 255, 255, 0.08);
}
.dnob-pip.hit { background: var(--red); border-color: var(--red); }
/* ---------- the battle panel ---------- */
.dnob-battle, .dnob-spy {
    padding: 8px 12px;
    border: 1px solid var(--accent);
    border-radius: 10px;
    background: var(--panel);
    margin-bottom: 10px;
}
.dnob-battletitle { margin: 0 0 6px; font-size: 14px; }
.dnob-battlerow {
    display: flex;
    gap: 16px;
    flex-wrap: wrap;
    align-items: flex-start;
}
.dnob-battleside {
    flex: 1 1 240px;
    max-width: 380px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.dnob-battlerole { font-weight: 700; font-size: 13px; }
.dnob-battlecards { display: flex; gap: 6px; align-items: flex-end; flex-wrap: wrap; }
.dnob-dice { font-size: 34px; line-height: 1; }
.dnob-forcelines { font-size: 13px; max-width: 240px; }
.dnob-forceline { display: flex; justify-content: space-between; gap: 12px; }
.dnob-forceline.total {
    border-top: 1px solid var(--panel-border);
    margin-top: 3px;
    padding-top: 3px;
    font-weight: 700;
}
.dnob-forceline.rout { color: var(--red); font-weight: 800; }
.dnob-forcevalue { font-variant-numeric: tabular-nums; }
.dnob-battleoutcome { margin-top: 6px; font-size: 13px; }
.dnob-battleoutcome.bad { color: var(--red); }
/* ---------- the hand and the action column ---------- */
.panel.dnob-handpanel .dnob-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
}
.panel.dnob-handpanel #hand-cards { flex: 1 1 auto; }
.dnob-actions {
    flex: 0 0 244px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: stretch;
}
.dnob-actionnote { font-size: 13px; opacity: 0.9; }
.dnob-actionnote.bad { color: var(--red); }
.dnob-note { font-size: 13px; opacity: 0.8; align-self: center; }
.dnob-note.bad { color: var(--red); opacity: 1; }
.dnob-discardrow .card { --cw: 40px; }
/* ---------- the log ---------- */
.logline.dnob-logbattle { color: var(--accent); }
@media (max-width: 720px) {
    /* The action column gives up its own column before the hand does. */
    .panel.dnob-handpanel .dnob-handrow { flex-wrap: wrap; }
    .dnob-actions { flex: 1 1 100%; }
    .card.dnob-noblecard { --cw: 52px; }
    .card.dnob-supportcard { --cw: 40px; }
    .dnob-courtrow { gap: 8px; }
}

/* ==== One Too Many (onetoomany.html) - only what is genuinely new lives
        here; the panels, buttons and cards are the shared classes ==== */

.otm-board { display: flex; flex-direction: column; gap: 12px; }

/* The shared center: Diamond runs, grouped with a visible gap per run. */
.otm-deckline { font-size: 13px; opacity: 0.85; margin-bottom: 8px; }
/*
 * THE CENTER NEVER WRAPS ABOVE PHONE WIDTH. A round can put all 13 Diamonds
 * here, and a second row of them pushed the action row and every seat panel
 * down a card's height mid-round. So each card takes the width a row of all
 * 13 can afford in this container: 13 cards plus at most 120px of gaps (16px
 * between runs, 4px inside one), capped at the .mini 46px and floored at 28px,
 * which the narrowest non-phone board (about 530px wide) still fits in one
 * row. The empty line is a card row tall, so the first Diamond moves nothing.
 * Behind @supports because a custom property has no parse-time fallback: an
 * unsupported cqw would blank every center card. A phone reserves two rows of
 * 30px cards instead (below), which always hold 13.
 */
.otm-center {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    align-items: flex-start;
}
.otm-center > * { --otm-ccw: 46px; }
@supports (width: 1cqw) {
    .otm-center { container-type: inline-size; }
    .otm-center > * { --otm-ccw: clamp(28px, calc((100cqw - 120px) / 13), 46px); }
}
.otm-run { gap: 4px; border-radius: 6px; }
.otm-run .card { --cw: var(--otm-ccw); }
/* The run that busted the round, ringed once its last card has landed. */
.otm-run.otm-bustrun { outline: 2px solid var(--red); outline-offset: 3px; }
.otm-center-empty {
    font-size: 13px;
    opacity: 0.7;
    display: flex;
    align-items: center;
    min-height: calc(var(--otm-ccw) * 89 / 64);
}
.otm-runline { margin-top: 8px; font-size: 13px; opacity: 0.9; }
.otm-runline.otm-danger { color: var(--red); font-weight: 700; opacity: 1; }
/* The bust threshold is a rules reminder, not part of the reading. */
.otm-runline-note { font-style: italic; opacity: 0.8; font-weight: 400; }

/* The commit / catch-up controls: a row of number buttons. The row is EMPTY
   once you have banked and the others play on, and the round report replaces
   it at the end, so it reserves the number row's height (measured in Chromium:
   one row of buttons is 38px here, two rows on a phone 85px). Without it the
   seat panels below walked up and down by up to 109px on a phone. */
.otm-actions { flex-wrap: wrap; align-items: center; min-height: 38px; }
.otm-actions .btn.otm-num { min-width: 46px; }
.otm-actions .btn.otm-hinted { outline: 2px dashed var(--accent); }
.otm-note { flex-basis: 100%; font-size: 13px; opacity: 0.8; }
.otm-note.otm-hint { color: var(--accent); opacity: 1; }

/* The round-end report: a sentence and the Next round button, side by side
   so the pair is about as tall as the row of number buttons it replaces.
   While the deal that ended the round is still landing it is laid out but
   invisible (`otm-held`), so showing it moves nothing. */
.otm-report {
    flex-basis: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 8px 16px;
}
.otm-report.otm-held { visibility: hidden; }
.otm-report-text { max-width: 620px; font-weight: 600; font-size: 14px; }
.otm-report .btn { flex: none; }

/* Per-seat panels. */
.otm-players {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 10px;
}
.panel.otm-seat { padding: 10px 12px; }
.panel.otm-seat.otm-me { outline: 2px solid var(--accent); }
.otm-seat-head {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    font-weight: 700;
    margin-bottom: 6px;
}
.otm-seat-score { font-variant-numeric: tabular-nums; opacity: 0.9; }
/* The pile: overlapped public faces, and the deal animation's landing zone. */
.otm-pile {
    gap: 0;
    flex-wrap: nowrap;
    overflow: hidden;
    min-height: calc(34px * 89 / 64);
    margin-bottom: 6px;
}
.otm-pile .card { --cw: 34px; flex: none; }
.otm-pile .card + .card { margin-left: -24px; }
.otm-seat-target { opacity: 0.6; font-weight: 600; }
.otm-seat-body { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.otm-chip {
    font-size: 12px;
    padding: 2px 8px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.08);
}
.otm-chip.otm-redo { background: rgba(255, 209, 102, 0.22); }
.otm-chip.otm-out { opacity: 0.85; }
/* The ask that busted the round: the one chip on the table that cost points. */
.otm-chip.otm-busted { background: rgba(214, 40, 40, 0.45); font-weight: 700; }
.otm-chip.otm-thinking { font-style: italic; opacity: 0.75; }
/* Last turn's revealed ask: public, but history - quieter than a live chip. */
.otm-chip.otm-lastfingers { opacity: 0.7; }
.otm-fingers {
    font-size: 16px;
    font-weight: 800;
    padding: 2px 10px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.14);
    font-variant-numeric: tabular-nums;
}
.otm-fingers.otm-greediest { background: rgba(214, 40, 40, 0.45); }

/* Final standings. */
.otm-final { display: flex; flex-direction: column; gap: 4px; margin: 10px 0; }
.otm-final-row {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 4px 10px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.05);
}
.otm-final-row.otm-final-winner { background: rgba(255, 209, 102, 0.18); font-weight: 700; }
.otm-final-score { font-variant-numeric: tabular-nums; }

@media (max-width: 560px) {
    /* Two rows of 30px cards always hold all 13 Diamonds at 320px and up, so
       the center reserves both and never grows mid-round. */
    .otm-center {
        gap: 6px 8px;
        align-content: flex-start;
        min-height: calc(2 * 30px * 89 / 64 + 6px);
    }
    .otm-center > * { --otm-ccw: 30px; }
    .otm-run { gap: 2px; }
    .otm-players { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
    /* A phone panel is a name, a score, and two or three short chips: the
       pile's faces (what banking pays is on a chip), the target (it is on
       the progress line), and the "Choosing" chip (an empty slot says it)
       all go. The deal animation flies to the panel instead of a face. */
    .otm-pile,
    .otm-seat-target,
    .otm-chip.otm-thinking { display: none; }
    /* Eleven number buttons wrap to two rows on a phone. */
    .otm-actions { min-height: 85px; }
    .panel.otm-seat { padding: 8px 10px; }
    .otm-seat-head { margin-bottom: 4px; }
}
/* ==========================================================================
 * Liar (src/liar/def.ts).
 *
 * A two-player deduction duel, so the board is a symmetric stack around the
 * hidden card: the opponent's played cards and claims on top, the viewer's
 * mirrored below. Every played card carries a caption of what it asked and
 * every answer stays on screen as a chip - the design brief is "all the
 * information, all the time". The tracker grid sits beside the face-down
 * card in every phase: the whole nine-card pack, with everything the viewer
 * can already see grayed out. Nothing above the hand may change height
 * mid-turn: the status line reserves its tallest line count, played rows
 * always show four slots plus a caption line, and the action bar reserves
 * its tallest state (the Le Truc recipe, via Counterbluff).
 *
 * THE PHONE FOLD. Measured in Chromium at 390x664, the action bar started at
 * 781px, 117px below the fold, and the hand ended at 765px. After this
 * block the bar starts at 564px and its lowest button (a picked card's
 * second row) ends at 657px, and a whole game played through the UI moved
 * nothing. Three things pay for it at 620px and below, and no card in the
 * hand got smaller:
 *  - the progress line, which only carries the turn number, is hidden, and
 *    the status line drops to 16px with three lines reserved (the table
 *    starts at 128px, not 219px);
 *  - the middle band goes to two columns, the claims beside the hidden card
 *    and grid rather than in rows of their own (the table is 317px, not
 *    423px);
 *  - the action bar lays its buttons out in a row instead of a stack, and
 *    reserves two rows (104px) instead of the desktop column (196px).
 * At 360x640 the same layout ends 15px below the fold, and only in the
 * picked-card state.
 * ========================================================================== */
.status.liar-status { min-height: 2.9em; }
/* --- the progress line ----------------------------------------------------- */
/* The shared .progress sets opacity 0.7 and a child cannot climb back out of
   an inherited opacity. The line holds the turn number only. */
.progress.liar-progress {
    opacity: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
}
/* --- the table ------------------------------------------------------------- */
.liar-table {
    display: flex;
    flex-direction: column;
    gap: 8px;
    /* Tight above and below: the progress line's own 8px already sets the
       table off, the hand panel's border sets off the rest, and the app
       layout's 1280x720 frame had 18px of these margins too few to spare. */
    margin: 0 0 6px;
}
/* The opponent's claims, the hidden card with the grid, and your claims. On a
   wide screen it stacks with the table's own spacing, which is the layout the
   game shipped with; the phone block at the end makes it two columns. */
.liar-band {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.liar-table .card { --cw: var(--liar-cw, 64px); }
.liar-seatrow {
    display: flex;
    align-items: flex-start;
    gap: 14px;
}
.liar-seatname {
    flex: 0 0 110px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: 14px;
}
.liar-name { font-weight: 700; }
.liar-held { font-size: 12px; opacity: 0.7; }
.liar-toact {
    font-size: 11px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--accent);
}
/* The marker keeps its slot when idle - a gutter that grows and shrinks
   would bounce its whole row. */
.liar-toact.off { visibility: hidden; }
.liar-cards {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    /* Four slots plus their caption line, always: a played card must never
       reflow the row. */
    min-height: calc(var(--liar-cw, 64px) * 89 / 64 + 18px);
}
.liar-playedcell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
}
/* What the played card asked: "♥?" / "Q?" under it. */
.liar-caption {
    font-size: 12px;
    opacity: 0.8;
    min-height: 16px;
    line-height: 16px;
    font-variant-numeric: tabular-nums;
}
.liar-caption.red { color: #ff7b72; }
/* Per-suit refinement (see the .cardlabel block). The pack is spades, hearts
   and clubs (LIAR_SUITS), so there is no diamond rule to write here - a
   `diamond` modifier can never reach this game. */
.liar-caption.club { color: var(--blue-club); }
/* --- the claims rows -------------------------------------------------------- */
.liar-claims {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
    margin-left: 124px; /* under the cards, aligned past the name gutter */
    /* One chip line, reserved before the first chip lands. */
    min-height: 30px;
}
.liar-claimslabel { font-size: 12px; opacity: 0.7; margin-right: 4px; }
.liar-noclaims { font-size: 12px; opacity: 0.45; font-style: italic; }
.liar-claim {
    /* A chip is a <span> normally and a <button> in BS mode; the resets
       keep the two PIXEL-IDENTICAL so arming BS never reflows the row. */
    appearance: none;
    -webkit-appearance: none;
    font-family: inherit;
    font-size: 13px;
    font-weight: 700;
    padding: 3px 9px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.25);
    font-variant-numeric: tabular-nums;
    line-height: 1.3;
    color: inherit;
}
.liar-claim.red { color: #ff7b72; }
/* Clubs, at the same 2-class weight as the red above and DELIBERATELY before
   `.liar-claim.liar-lie`: a revealed lie is painted in the lie color, and
   ordering these two the same way keeps that true for a club claim as well. */
.liar-claim.club { color: var(--blue-club); }
/* A claim armed as a BS target (a real button in that mode). */
.liar-claim.liar-targetable { cursor: pointer; }
.liar-claim.liar-targetable:hover { border-color: var(--accent); }
.liar-claim.liar-picked {
    border-color: var(--accent);
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}
/* The viewer's own lie (private: the table hides behind the handoff gate). */
.liar-claim.liar-ownlie { border-style: dashed; border-color: rgba(255, 209, 102, 0.55); }
/* A lie laid bare at game end. */
.liar-claim.liar-lie {
    border-color: #ff7b72;
    color: #ff7b72;
    text-decoration: line-through;
}
.liar-claim.liar-lie.red { text-decoration-color: #ff7b72; }
/* The claim a BS call ended the game on. */
.liar-claim.liar-challenged {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}
/* --- the center: the hidden card beside the always-on tracker grid ---------- */
/* The grid is rendered in EVERY phase (grayed cells = accounted for), so the
   center strip never changes shape: arming a guess only lights it up. */
.liar-center {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 22px;
}
.liar-grid {
    display: grid;
    grid-template-columns: repeat(3, auto);
    gap: 6px;
    padding: 10px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.3);
    border: 1px solid rgba(255, 209, 102, 0.35);
}
/* Tracker-sized cells: small enough that the permanent grid stays quiet. */
.liar-grid .card.mini { --cw: 40px; }
/* Guess mode lights the frame instead of moving anything. */
.liar-grid.liar-arming {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}
/* The reveal at game end: the hidden card (accent) and a wrong guess (red),
   in the grid AND in the result panel's reveal row. */
.card.liar-revealed {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
}
.card.liar-missed {
    outline: 3px solid #ff7b72;
    outline-offset: 1px;
}
/* A grid cell the viewer can already see somewhere: it cannot be the hidden
   card, so it stays visible for reference but reads as spent. */
.liar-grid .card.liar-known {
    opacity: 0.3;
    filter: grayscale(0.8);
    box-shadow: none;
}
.card.liar-picked {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}
.liar-grid .card.clickable:hover { transform: translateY(-3px); }
/* --- the hand panel + action bar (the Le Truc recipe) ------------------------ */
.liar-handrow {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    min-height: calc(var(--cw) * 89 / 64);
}
.liar-handrow #hand-cards { flex: 1 1 auto; }
.liar-actions {
    /* Fixed column, present even when empty: a decision appearing must not
       slide the cards sideways. And a reserved HEIGHT covering the tallest
       decision stack, so the panel does not collapse while the opponent
       thinks and re-grow on your turn - the pulse this game shipped with. */
    flex: 0 0 250px;
    /* Sized to the tallest stack: a picked card's three buttons at ~38px
       each, the two 7px gaps, and the filled padding and border. */
    min-height: 150px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 0;
    border-radius: 12px;
    border: 1px solid transparent;
}
/* The answer phase's claim numbers ride one compact row. */
.liar-claimbtns {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}
.liar-actions .liar-claimbtns .btn {
    width: auto;
    flex: 0 0 auto;
    min-width: 46px;
}
.liar-actions.filled {
    padding: 9px 11px;
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 209, 102, 0.35);
}
.liar-actions .btn { width: 100%; }
.liar-actions-note { font-size: 12px; opacity: 0.8; line-height: 1.35; }
/* Under the hand once the column no longer fits beside it. The buttons go
   in a row there: stacked full width, a picked card's three buttons were
   the 196px that put the bar below a phone's fold. */
@media (max-width: 760px) {
    .liar-handrow { flex-wrap: wrap; gap: 10px; }
    .liar-actions {
        flex: 1 1 100%;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        align-content: flex-start;
        justify-content: center;
        gap: 6px;
        /* Two rows of buttons (a picked card's three wrap 2 + 1 on a phone),
           the 6px row gap, and the filled padding and border. Measured at
           103px in Chromium, so 104 leaves a pixel of rounding. */
        min-height: 104px;
    }
    .liar-actions .btn { width: auto; flex: 1 1 auto; white-space: nowrap; }
    .liar-actions .liar-claimbtns { flex: 0 0 auto; }
    .liar-actions-note { flex: 1 1 100%; text-align: center; }
}
/* --- the result panel -------------------------------------------------------- */
.panel.liar-summary {
    max-width: 560px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
.panel.liar-summary .result-actions { justify-content: center; flex-wrap: wrap; }
.panel.liar-summary .result-note { text-align: center; }
/* The finish, face up: the guessed card (beside the real hidden card when
   it missed) at the top of the result panel. */
.liar-revealrow {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: 22px;
    margin: 10px 0 6px;
}
.liar-heldrow {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin: 6px 0;
}
.liar-heldrow .result-label { font-size: 13px; opacity: 0.8; }
/* --- phones ------------------------------------------------------------------
   See THE PHONE FOLD in the header. Every reservation is re-derived at this
   size rather than carried over from the desktop figures. */
@media (max-width: 620px) {
    /* 16px over three reserved lines: the longest ordinary prompt is two
       lines at 358px, and a hint runs to three. */
    .status.liar-status { font-size: 16px; line-height: 1.25; min-height: 3.75em; }
    .progress.liar-progress { display: none; }
    .liar-table { --liar-cw: 46px; gap: 6px; margin: 4px 0 8px; }
    .liar-seatname { flex-basis: 78px; }
    /* Two columns: the claims on the left, opponent's at the top against the
       opponent's row and yours at the bottom against yours, and the hidden
       card with the grid on the right. The grid sets the band's height. */
    .liar-band {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas: "opp center" "mine center";
        column-gap: 10px;
        row-gap: 6px;
    }
    .liar-band > .liar-claims.liar-opp { grid-area: opp; align-self: start; }
    .liar-band > .liar-claims.liar-mine {
        grid-area: mine;
        align-self: end;
        align-content: flex-end;
    }
    .liar-band > .liar-center { grid-area: center; }
    .liar-claims {
        margin-left: 0;
        align-content: flex-start;
        /* Two chip lines and the 6px gap between them: four claims with a
           revealed lie wrap to a second line in a 180px column. */
        min-height: 56px;
    }
    .liar-claim { padding: 3px 7px; }
    .liar-center { gap: 10px; }
    .liar-grid { gap: 5px; padding: 6px; }
    .liar-grid .card.mini { --cw: 28px; }
    .panel.liar-handpanel { padding: 10px 12px; margin-bottom: 10px; }
    .panel.liar-handpanel h3 { margin-bottom: 6px; }
}

/* ==== Marry Up (marryup.html) - only what is genuinely new lives here;
        the panels, buttons and cards are the shared classes ==== */

.mup-board { display: flex; flex-direction: column; gap: 12px; }

/* The opponents' strip across the top: drafted cards, face down except the
   aces of closed rounds. Two seats, so they share the width evenly. */
.mup-players {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    gap: 10px;
}
.panel.mup-seat { padding: 10px 12px; }
.panel.mup-seat.mup-me { outline: 2px solid var(--accent); }
.mup-seat-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
    font-weight: 700;
    margin-bottom: 6px;
}
.mup-chip {
    font-size: 12px;
    font-weight: 400;
    padding: 2px 8px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.08);
}
.mup-chip.mup-thinking { font-style: italic; opacity: 0.75; }
.mup-chip.mup-winner { background: rgba(255, 209, 102, 0.22); font-weight: 700; }

/* The drafted row: backs (and face-up aces) during the draft.
   ONE ROW, ALWAYS. It used to wrap, and on a phone the fifth card took it to
   a second row, which walked the knowledge panel, your cards, and your hand
   38px down the screen mid-draft (measured at 390px). Instead the cards close
   up as the row fills: the step between cards is whatever lets SIX fit the
   panel (a seat never holds more), capped at the 4px gap. The overlap covers
   a card's right edge, so the rank in its top-left corner stays readable.
   `100cqw` is the panel's content width, which is why the opponents' panels
   are query containers; they hold nothing position: fixed (the flight ghosts
   live on <body>), so the containment reaches nothing else. */
#mup-opponents > .mup-seat { container: mupopp / inline-size; }
.mup-drafted {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    min-height: calc(40px * 89 / 64);
}
.mup-drafted .card { --cw: 40px; flex: none; }
.mup-drafted .card + .card { margin-left: min(4px, (100cqw - 6 * var(--cw)) / 5); }
.mup-empty { font-size: 12px; opacity: 0.6; align-self: center; }

/* Hands grouped marriage by marriage: your own all through the draft (a pair
   shows the moment you make it), everyone's at the reveal. Each group is
   labeled, and the cards nobody married are dimmed at the end. */
.mup-marriages {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: flex-start;
    justify-content: center;
}
.mup-marriages .card { --cw: 40px; }
.mup-marriage { display: flex; flex-direction: column; gap: 3px; }
.mup-marriage .cardrow { gap: 3px; }
.mup-marriage-name { font-size: 11px; opacity: 0.85; text-align: center; }
/* Dimmed enough to read as "not in a marriage", not so far that the rank and
   suit stop being legible: these are the cards a player is deciding what to do
   about next, so they have to be readable at a glance. */
.mup-singles .card { opacity: 0.88; }
/* Your own hand is the one you read constantly: bigger than the opponents'. */
#mup-you .mup-marriages { gap: 14px; }
#mup-you .mup-marriages .card { --cw: 52px; }
#mup-you .mup-marriage-name { font-size: 12px; }

/* The packet: the hand you are picking from this round. A hand of cards is
   always centered (ui-conventions.md). The shared rule reaches `#hand-cards`,
   which this page does not have, so the hand row says it here. */
.mup-packet {
    justify-content: center;
    min-height: calc(var(--cw) * 89 / 64);
}
.mup-packet .card.mup-hinted { outline: 2px dashed var(--accent); }
.mup-note { margin-top: 8px; font-size: 13px; opacity: 0.8; }

/* ---- What you know: the two hands you cannot see ----------------------- *
 * Memory assistance, not a peek: every card here is one you have held in your
 * own hand (see renderKnow and readPacketFor in def.ts). Each row keeps its
 * height whether or not it has cards in it, so the panel does not grow under
 * the hand as the rounds go by.
 */
/* A container of its own, so the label reservation below keys on the panel's
   real width rather than the viewport's (the log column comes and goes at
   950px). Game-owned class, and nothing in it is position: fixed, so the
   containment reaches nothing else. */
.mup-know { display: flex; flex-direction: column; gap: 8px; container: mupknow / inline-size; }
.mup-know-row { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.mup-know-label { font-size: 12px; line-height: 16px; opacity: 0.85; text-align: center; }

/* The two readings share one row, each under the seat it is about: your
   left-hand neighbor on the left, the far seat on the right, in the same order
   as the opponent panels above. Always two columns, at every width, because
   which side a reading is on is what says whose hand it is.
   Each reading is a two-track SUBGRID (label, then cards), so the two labels
   share one row height and the two card rows stay level even when only one
   label wraps. */
.mup-know-hands {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: auto auto;
    column-gap: 14px;
    row-gap: 4px;
}
.mup-know-hands .mup-know-row {
    display: grid;
    grid-row: span 2;
    grid-template-rows: subgrid;
    justify-items: center;
}
.mup-know-hands .mup-know-label { align-self: end; }
/* Narrow panel: a column is too thin for "Bot West is choosing from 4 of
   these 5:" on one line, and whether it wraps changes round to round (the
   wording and the counts both change). Reserve the two lines up front so the
   panel, and everything under it, holds still (ui-conventions, "Containers
   must not move"). */
@container mupknow (max-width: 680px) {
    .mup-know-hands .mup-know-label { min-height: 32px; }
}
.mup-know-cards {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 3px;
    min-height: calc(34px * 89 / 64);
}
.mup-know-cards .card { --cw: 34px; }
/* Gone for good, so smaller than a hand still in play and set apart from it. */
.mup-know-gone { border-top: 1px dashed rgba(255, 255, 255, 0.14); padding-top: 6px; }
.mup-know-gone .mup-know-cards { min-height: calc(28px * 89 / 64); }
.mup-know-gone .mup-know-cards .card { --cw: 28px; }

/* Final standings (result panel). A real <table>, sized to its content and
   centered as a block (`margin: auto`), so each name sits right beside its
   results and every row's results start in the same column. It used to be
   flex rows with `space-between`, which pushed the two to opposite edges of
   the dialog. The lifted dialog centers its text, so every cell says
   `text-align: left` for itself. `separate` with a vertical spacing gives each
   row its own rounded band, which `collapse` cannot. */
.mup-final {
    margin: 10px auto;
    border-collapse: separate;
    border-spacing: 0 4px;
}
.mup-final td {
    padding: 4px 10px;
    text-align: left;
    vertical-align: baseline;
    background: rgba(255, 255, 255, 0.05);
}
.mup-final td:first-child { border-radius: 8px 0 0 8px; }
.mup-final td:last-child { border-radius: 0 8px 8px 0; }
.mup-final .mup-final-winner td { background: rgba(255, 209, 102, 0.18); font-weight: 700; }
.mup-final-name { white-space: nowrap; }
.mup-final-detail { font-size: 13px; opacity: 0.9; }
/* The summary names every married card, and the suit is what decides whether a
   royal marriage survived, so the glyphs are colored here as they are in the
   log. The platform's own suit modifiers are scoped to #log-entries, so this
   surface opts in for itself; `.diamond` is one class deeper so it wins over
   `.red`. */
.mup-final-detail .red { color: var(--red); }
.mup-final-detail .red.diamond { color: var(--red-diamond); }
.mup-final-detail .club { color: var(--blue-club); }

@media (max-width: 560px) {
    .mup-drafted .card, .mup-marriages .card { --cw: 32px; }
    #mup-you .mup-marriages .card { --cw: 42px; }
    .mup-players { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
    .mup-know-hands { column-gap: 8px; }
    /* The reservations re-derived at the smaller card sizes, rather than
       leaving the desktop figures here (ui-conventions). */
    .mup-know-cards { min-height: calc(28px * 89 / 64); }
    .mup-know-cards .card { --cw: 28px; }
    .mup-know-gone .mup-know-cards { min-height: calc(24px * 89 / 64); }
    .mup-know-gone .mup-know-cards .card { --cw: 24px; }
}
/* ==========================================================================
   Crown Rivals
   (src/crownrivals/, pages/crownrivals.mjs)
   ==========================================================================
   Two-player deck-building duel. Every shared class (.card, .cardrow,
   .panel, .logline, .result-actions, .winner-banner, .btn, .sel) is reused
   as-is; only what is genuinely this game's own lives here, all of it
   prefixed `crv-`. The board is two RIVAL PANELS (opponent above, viewer
   below) around the Market row. */
.crv-seat {
    padding: 8px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    margin-bottom: 10px;
}
.crv-seathead {
    display: flex;
    gap: 10px;
    align-items: center;
    flex-wrap: wrap;
    font-size: 13px;
}
.crv-seatname { font-weight: 700; font-size: 14px; }
.crv-influence {
    font-size: 20px;
    font-weight: 800;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}
.crv-chip {
    font-size: 12px;
    padding: 2px 8px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.08);
    font-variant-numeric: tabular-nums;
}
.crv-chip.crv-poolchip { background: rgba(214, 40, 40, 0.35); font-weight: 700; }
.crv-chip.crv-bonuschip { background: rgba(255, 209, 102, 0.22); }
.btn.crv-discardtoggle { font-variant-numeric: tabular-nums; }
.crv-resources {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-top: 6px;
    font-size: 12px;
}
/* ---------- zones inside a seat panel ---------- */
.crv-zone { margin-top: 8px; }
.crv-zonetitle { font-size: 12px; opacity: 0.8; margin-bottom: 4px; }
.crv-played {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 3px;
    border-radius: 8px;
}
.crv-played.sel { outline: 2px solid var(--accent); background: rgba(255, 255, 255, 0.06); }
.crv-played.lit { outline: 2px dashed var(--accent); outline-offset: 1px; }
.crv-playedcap { font-size: 11px; opacity: 0.75; }
/* Every card outside the hand and the Market is read, not handled, and all of
   them sit in the 30px to 46px band where the full art collapses into a
   smudge - so each carries `simple` at its call site in def.ts. The Rabble
   came down from 52px to 46px to stay inside that band rather than straddle
   it. See .kiro/steering/ui-conventions.md. */
.card.crv-rabblecard { --cw: 46px; }
.card.crv-boardcard { --cw: 46px; }
.card.crv-diamondcard { --cw: 30px; margin-top: -14px; }
.card.crv-discardcard { --cw: 38px; }
.crv-rabblerow, .crv-boardrow { gap: 10px; align-items: flex-start; }
.crv-note { font-size: 13px; opacity: 0.8; align-self: center; }
/* ---------- the Market ---------- */
.crv-market {
    padding: 8px 12px;
    border: 1px solid var(--accent);
    border-radius: 10px;
    background: var(--panel);
    margin-bottom: 10px;
}
/* The Market is the one shared thing on the board, so it sits on the page's
   own axis: both its chip line and its five cards are centered. Neither row
   scrolls, so centering cannot clip anything off an unreachable edge. */
.crv-markethead {
    display: flex;
    gap: 10px;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    margin-bottom: 6px;
    font-size: 13px;
}
.crv-marketname { font-weight: 700; font-size: 14px; }
.crv-marketrow { gap: 12px; align-items: flex-start; justify-content: center; }
.crv-marketslot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
}
.card.crv-marketcard { --cw: 56px; }
.crv-marketcost {
    font-size: 12px;
    font-weight: 700;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}
/* ---------- the hand and the action column ---------- */
.panel.crv-handpanel .crv-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
}
.panel.crv-handpanel #hand-cards { flex: 1 1 auto; }
.panel.crv-handpanel #hand-cards .card.lit { outline: 2px dashed var(--accent); }
.crv-actions {
    flex: 0 0 244px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: stretch;
}
.crv-actionnote { font-size: 13px; opacity: 0.9; }
/* ---------- the log ---------- */
.logline.crv-logscore { color: var(--accent); }
@media (max-width: 720px) {
    /* The action column gives up its own column before the hand does. */
    .panel.crv-handpanel .crv-handrow { flex-wrap: wrap; }
    .crv-actions { flex: 1 1 100%; }
    .card.crv-marketcard { --cw: 46px; }
    .card.crv-rabblecard { --cw: 40px; }
    .card.crv-boardcard { --cw: 40px; }
    .crv-marketrow { gap: 8px; }
}

/* ==== Disloyal Royals (disloyalroyals.html) - everything shared (.panel,
   .pile-chip, .logline, .result-actions, .winner-banner, .btn, .sel, .lit)
   is reused as-is; only what is genuinely this game's own lives here, all of
   it prefixed `droyals-`. The board is two FAMILIES (opponent above, viewer
   below) around a roll panel that replays the last Club/Dig dice.

   HEIGHT IS THE CONSTRAINT HERE, not width: two families stacked above the
   hand put the cards a long way down the page (playtest: "the play areas
   might be too tall"). Every size below is the smallest that still reads, and
   the two card strips OVERLAP rather than wrap, which caps their height at one
   row however many cards go into them. That is also what keeps the board
   still: a strip that wraps grows the panel above the hand, which is the one
   movement .kiro/steering/ui-conventions.md says never to allow. ==== */
.droyals-family {
    padding: 6px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.03);
    margin-bottom: 8px;
}
.droyals-familyhead {
    display: flex;
    gap: 12px;
    align-items: baseline;
    flex-wrap: wrap;
    margin-bottom: 4px;
    font-size: 13px;
}
.droyals-familyname { font-weight: 700; font-size: 14px; }
.droyals-handcount { opacity: 0.8; font-variant-numeric: tabular-nums; }
.droyals-points { color: var(--accent); font-weight: 700; }
.droyals-familyrow {
    display: flex;
    gap: 18px;
    align-items: flex-end;
    flex-wrap: wrap;
    justify-content: center;
}
/* ---------- one royal: buried riches above, defenses below ---------- */
.droyals-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 3px;
    border-radius: 8px;
}
.droyals-slot.lit { outline: 2px solid var(--accent); outline-offset: 2px; }
.droyals-slot.sel { background: rgba(255, 255, 255, 0.07); }
.card.droyals-royalcard { --cw: 56px; }
.card.droyals-royalcard.lit { box-shadow: 0 0 0 2px var(--accent); }

/* The empty seat a fallen royal leaves: a labeled vacancy, not a blank
   outline. It becomes the crown target while that mode is armed. */
.card.droyals-seat {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    text-align: center;
    color: var(--muted, rgba(255, 255, 255, 0.55));
    border-style: dashed;
}
button.card.droyals-seat { cursor: pointer; }

/* Both strips overlap their cards, so N of them are one row tall at any N. */
.droyals-buried,
.droyals-defense {
    display: flex;
    justify-content: center;
    flex-wrap: nowrap;
}
.droyals-buried .card + .card,
.droyals-defense .card + .card { margin-left: -60%; }
.droyals-buried { min-height: calc(26px * 89 / 64); }
.droyals-defense { min-height: calc(28px * 89 / 64); }
.card.droyals-buriedcard { --cw: 26px; }
.card.droyals-defensecard { --cw: 28px; }
button.card.droyals-buriedcard { cursor: pointer; }
.card.droyals-buriedcard.lit { box-shadow: 0 0 0 2px var(--accent); }
.droyals-slotcap {
    font-size: 10.5px;
    opacity: 0.75;
    text-align: center;
    white-space: nowrap;
}
.droyals-slotcap.bad { color: var(--red); opacity: 0.9; }
/* ---------- the roll panel, and the cards a dig unearthed ---------- */
.droyals-mid {
    padding: 6px 12px;
    border: 1px solid var(--accent);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.03);
    margin-bottom: 8px;
}
/* One row, reserved at the height of a reveal face and never allowed to wrap:
   the dice and the unearthed cards share it, and a second row here would walk
   the hand down the page mid-turn. */
.droyals-rollline {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-wrap: nowrap;
    overflow-x: auto;
    min-height: calc(30px * 89 / 64);
    font-size: 13px;
}
.droyals-rolltext { white-space: nowrap; }
.droyals-die { font-size: 30px; line-height: 1; opacity: 0.55; }
.droyals-die.hit { opacity: 1; color: var(--accent); }
.card.droyals-revealcard { --cw: 30px; opacity: 0.75; }
.card.droyals-revealcard.droyals-stolen {
    opacity: 1;
    box-shadow: 0 0 0 2px var(--accent);
}
/* ---------- the hand and the action column ---------- */
.panel.droyals-handpanel .droyals-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
}
.panel.droyals-handpanel #hand-cards { flex: 1 1 auto; }
.droyals-actions {
    flex: 0 0 244px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: stretch;
}
.droyals-actionnote { font-size: 13px; opacity: 0.9; }
/* ---------- the log ---------- */
.logline.droyals-logkill { color: var(--red); }
@media (max-width: 720px) {
    /* The action column gives up its own column before the hand does. */
    .panel.droyals-handpanel .droyals-handrow { flex-wrap: wrap; }
    .droyals-actions { flex: 1 1 100%; }
    .card.droyals-royalcard { --cw: 46px; }
    .card.droyals-defensecard { --cw: 24px; }
    .card.droyals-buriedcard { --cw: 22px; }
    .card.droyals-revealcard { --cw: 26px; }
    /* The reservations follow the cards down, or they reserve dead space. */
    .droyals-buried { min-height: calc(22px * 89 / 64); }
    .droyals-defense { min-height: calc(24px * 89 / 64); }
    .droyals-rollline { min-height: calc(26px * 89 / 64); }
    .droyals-familyrow { gap: 8px; }
    .droyals-slotcap { font-size: 10px; }
}


/* ------------------------------------------------------------------------- */
/* Polarité                                                                    */
/* ------------------------------------------------------------------------- */
/*
 * Prefix policy: every Polarité-specific class is `polarite-` (matching
 * `cascade-`, `dracula-`, `duel-`). Shared classes are reused as-is and get
 * no rules here: hand cards select with `.card.sel`, arm with `.clickable`
 * and flash with `.card.hinted`; neutral face-down particles are
 * `.card.simple.back`; the action row is a `.controls`; empty cells reuse
 * the `.card.empty` dashed recipe on the cell box (the cascade-cell
 * precedent).
 */

/* Board row: the field column, with the quark cheat sheet down its right side.
   align-items: flex-start keeps the aid pinned to the top of the field rather
   than centered against it, so expanding it grows downward only. */
.polarite-boardrow {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: 14px;
    width: fit-content;
    margin: 0 auto;
}
.polarite-fieldcol { flex: none; }

/* The side cheat sheet. Fixed width so opening and closing it cannot resize
   the field beside it. */
.polarite-quark-aid {
    flex: none;
    width: 240px;
    margin: 0;
    font-size: 13px;
}
.polarite-quark-aid > summary { font-size: 13px; }

.polarite-grid {
    --polarite-gap: 6px;
    /* WIDTH: reserve what actually sits beside the four card columns, measured
       rather than guessed: above 950px the playarea is 100vw - 348px (page
       padding plus the log column), the side aid takes 240px, the row gap 14px,
       and the grid's own three gaps 18px. 348 + 240 + 14 + 18 = 620.
       HEIGHT: the hand must be on screen without scrolling (a reported bug: at
       1280 x 800 it started at the fold). Everything above and below the field
       is a fixed height, measured in a browser: topbar, status and progress
       lines, the opponent's row, the two territory lines, the legend, the hand
       panel's one row and the action row come to about 520px at desktop sizes.
       The four card rows (x 89/64) and their three gaps share what is left. */
    --polarite-cw: max(46px, min(var(--cw), calc((100vw - 620px) / 4),
        calc((100vh - 520px) / 4 * 64 / 89)));
    display: grid;
    grid-template-columns: repeat(4, var(--polarite-cw));
    gap: var(--polarite-gap);
    width: fit-content;
    margin: 4px auto;
}
/* Less the cell's 1px border on each side, so a card fills its cell exactly
   instead of spilling into the gap (the cascade-grid recipe). */
.polarite-grid .card { --cw: calc(var(--polarite-cw) - 2px); }

.polarite-cell {
    position: relative;
    width: var(--polarite-cw);
    height: calc(var(--polarite-cw) * 89 / 64);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 8px;
    /* Transparent by default so the highlight borders never shift layout. */
    border: 1px solid transparent;
}
/* A vacant cell stays visibly empty: the shared .card.empty dashed recipe. */
.polarite-cell.empty {
    background: rgba(255, 255, 255, 0.05);
    border: 1px dashed rgba(255, 255, 255, 0.25);
}

/* The territory seam (standard game): a gold line between rows 2 and 3,
   drawn as a ::before on each row-3 cell so the grid geometry never moves. */
.polarite-grid.split .polarite-cell:nth-child(n+9):nth-child(-n+12)::before {
    content: '';
    position: absolute;
    top: calc(-0.5 * var(--polarite-gap) - 1px);
    left: calc(-0.5 * var(--polarite-gap));
    right: calc(-0.5 * var(--polarite-gap));
    height: 2px;
    background: rgba(255, 209, 102, 0.55);
    pointer-events: none;
}

/* Territory captions above and below the grid, with the live open count. */
.polarite-sideline {
    text-align: center;
    font-size: 13px;
    opacity: 0.85;
    margin: 4px 0;
}

/* Legal target for the selected card: the neutral light ring (cascade's
   vocabulary); gold is reserved for the quark window below. */
.polarite-cell.legal {
    border: 1px solid rgba(255, 255, 255, 0.55);
    background: rgba(255, 255, 255, 0.1);
    cursor: pointer;
}
.polarite-cell.legal:hover { background: rgba(255, 255, 255, 0.18); }

/* An INVERT target - dropping onto an occupied cell - is easy to overlook,
   so it reads distinctly from a channel: a blue ring (defined AFTER .legal so
   it wins the equal-specificity tie) plus a flip badge over the covered card.
   Blue, not gold: gold is the quark window's color. */
.polarite-cell.legal-invert {
    border: 1px solid #6aa9e0;
    background: rgba(106, 169, 224, 0.22);
}
.polarite-cell.legal-invert:hover { background: rgba(106, 169, 224, 0.34); }
/* Top-left, opposite the stack-count badge, so the two never collide. */
.polarite-invert-badge {
    position: absolute;
    top: -6px;
    left: -6px;
    z-index: 1;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #6aa9e0;
    color: #08243c;
    font-size: 13px;
    font-weight: 700;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}

/* --- "what can go here": the per-cell level gauge ------------------------ */
/* Shown whenever no card is selected, on every empty cell the seat that fills
   it could place on. Six segments, A on the left to 6 on the right (def.ts
   paintGauge, read off engine.handFitsAt). INFORMATION, not a button: no
   cursor, no hover, pointer-events off, so it never competes with the armed
   .legal cells that appear once a card is picked. */
.polarite-cell.open-channel {
    border: 1px dashed rgba(255, 255, 255, 0.4);
    background: rgba(255, 255, 255, 0.06);
}
/* An occupied cell an inversion could go on: only the edge, no bar over the
   card's art. */
.polarite-cell.open-invert { border: 1px dashed rgba(106, 169, 224, 0.65); }

/* The gauge runs the width of the cell, so its six segments stay readable
   down to the smallest field (each about a sixth of a 46px cell). Two states:
   lit (that hand holds a card of this level that fits) and a faint slot, so
   the six positions still read as six. Gold for the hand at the foot of the
   screen, a softer white for the other, so your own bars stand out. */
.polarite-gauge {
    position: absolute;
    left: 3px;
    right: 3px;
    bottom: 3px;
    z-index: 1;
    display: flex;
    gap: 2px;
    height: 10px;
    padding: 2px;
    border-radius: 4px;
    background: rgba(9, 26, 18, 0.8);
    pointer-events: none;
}
.polarite-seg {
    flex: 1;
    border-radius: 2px;
    background: rgba(244, 247, 245, 0.1);
}
.polarite-gauge.near .polarite-seg.fit { background: var(--accent); }
.polarite-gauge.far .polarite-seg.fit { background: rgba(244, 247, 245, 0.78); }

/* --- the law on the card: a charge arrow on each face-up particle --------- */
/* Red pushes its neighbors UP (they must be higher), black DOWN (lower). In
   the card's own color, in the top-left corner the invert badge only takes
   while a card is selected. */
/* Top-RIGHT: the card's rank and suit index sit top-left, and the arrow must
   not cover them. The stack badge moves to the bottom-right below. */
.polarite-charge {
    position: absolute;
    top: 2px;
    right: 2px;
    z-index: 1;
    /* Sized off the cell, so a small field gets a small arrow. */
    font-size: clamp(8px, calc(var(--polarite-cw, 60px) * 0.15), 12px);
    line-height: 1;
    padding: 1px 2px;
    border-radius: 3px;
    color: #fff;
    pointer-events: none;
}
/* A filled chip in the charge's own color, white arrow: it reads on a white
   card face, which a dark arrow on white did not. */
.polarite-charge.up { background: #c62828; }
.polarite-charge.down { background: #1b1f24; }
.polarite-charge.inline {
    position: static;
    display: inline-block;
    margin-right: 4px;
    font-size: 10px;
}

/* --- quark icons on the hand's 2 to 5 (never on the board) --------------- */
/* Scoped through the game's own hand panel class: #hand-cards is on every
   page (ui-conventions.md, "Shared ids need a game class"). */
.polarite-handpanel .card { position: relative; }
/* Bottom-center, between the card's two corner indexes, so it never covers a
   rank or a suit pip. Sized off the card, so a small hand gets a small badge. */
.card .polarite-quark-badge {
    position: absolute;
    bottom: 4%;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: max(15px, calc(var(--cw) * 0.3));
    height: max(15px, calc(var(--cw) * 0.3));
    border-radius: 50%;
    background: #1d3b2b;
    color: #ffd166;
    box-shadow: 0 0 0 1px rgba(255, 209, 102, 0.6);
    pointer-events: none;
}
.polarite-quark-slot {
    display: inline-flex;
    vertical-align: -2px;
    margin-right: 5px;
    color: var(--accent);
}

/* --- the legend under the field ------------------------------------------ */
.polarite-legend {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px 14px;
    margin: 4px 0 0;
    font-size: 12px;
    opacity: 0.85;
}
.polarite-legend-item { display: inline-flex; align-items: center; white-space: nowrap; }
.polarite-gauge.inline {
    position: static;
    display: inline-flex;
    width: 54px;
    margin-right: 6px;
}
.polarite-gauge.inline.one { width: 14px; }
.polarite-legend-note { opacity: 0.75; }

/* Quark window target (repel / polarize): gold, the "special action" hue,
   and it PULSES, because a move is expected and these are the only cells
   that take it. Everything else on the field goes quiet (.polarite-quiet). */
.polarite-cell.legal-extra {
    border: 2px solid var(--accent);
    background: rgba(255, 209, 102, 0.24);
    cursor: pointer;
    animation: polarite-target 1.1s ease-in-out infinite alternate;
}
.polarite-cell.legal-extra:hover { background: rgba(255, 209, 102, 0.36); }
@keyframes polarite-target {
    from { box-shadow: 0 0 0 0 rgba(255, 209, 102, 0.7); }
    to { box-shadow: 0 0 0 6px rgba(255, 209, 102, 0); }
}
.polarite-cell.polarite-quiet { opacity: 0.45; }
/* Where a repelled particle will go: an arrow on the target, away from the 3. */
.polarite-push {
    position: absolute;
    z-index: 2;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: var(--accent);
    color: #3a2b00;
    font-size: 16px;
    font-weight: 700;
    line-height: 24px;
    text-align: center;
    pointer-events: none;
}
.polarite-push.up { top: 4px; left: calc(50% - 12px); }
.polarite-push.down { bottom: 4px; left: calc(50% - 12px); }
.polarite-push.left { left: 4px; top: calc(50% - 12px); }
.polarite-push.right { right: 4px; top: calc(50% - 12px); }

/* The quark window's prompt in the action row: says what to do, with the
   ability's icon, beside the Skip button. */
.polarite-quark-prompt {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 7px 12px;
    border-radius: 8px;
    border: 1px solid var(--accent);
    background: rgba(255, 209, 102, 0.16);
    color: var(--accent);
    font-weight: 700;
}

/* Hint flash on the suggested cell (the .card.hinted treatment on a cell). */
.polarite-cell.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}

/* Classic: the inversion parked while its discard is being chosen. */
.polarite-cell.pending {
    border: 1px solid var(--accent);
    background: rgba(255, 209, 102, 0.18);
}

/* Mad Scientist: a dashed ring around the whole grid says "this edge is not
   really an edge" - the field wraps onto the opposite side. */
.polarite-grid.torus {
    outline: 2px dashed rgba(255, 209, 102, 0.35);
    outline-offset: 6px;
}

/* A stack of two particles: a second card edge peeking out behind the top
   one, offset down and right, so the stack looks like a stack and no badge
   sits on the card's art. Neutral grey: gold is the quark window's color. */
.polarite-cell.stacked::after {
    content: '';
    position: absolute;
    inset: 3px -3px -3px 3px;
    z-index: -1;
    border-radius: 6%;
    background: #c9d1cc;
    box-shadow: 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
    pointer-events: none;
}
.polarite-cell.stacked { isolation: isolate; }

/* A hand card with no legal position right now: dimmed, still inspectable.
   Applied to both seats' hands (both are public) during the play phase. */
.polarite-dead { opacity: 0.4; filter: grayscale(0.5); }

/* The action row sits BELOW the hand, so its own height is what would move
   the rules block under it as Pass comes and goes. Reserve one button row
   (the shared .btn is 15px/1.2 text plus 8px padding and a 1px border) so
   nothing shifts between a turn that offers Pass and one that does not. */
.polarite-actions { min-height: calc(15px * 1.2 + 18px); }

/* The quark player aid: a compact reference beside the board (standard game
   only). Reuses the shared .rules details styling; just tightens the list. */
.polarite-quark-list { margin: 4px 0 0; padding-left: 18px; }
.polarite-quark-list li { margin: 3px 0; }
.polarite-quark-note { margin: 6px 0 0; font-size: 12px; opacity: 0.7; }

/* The opponent's hand is public information: shown small, above the field.
   A compact panel, with its title beside the cards rather than over them, so
   it costs one short row of height. */
.panel.polarite-opp {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    padding: 6px 12px;
    margin-bottom: 6px;
}
.panel.polarite-opp .panel-head h3 { margin: 0; font-size: 13px; white-space: nowrap; }
.polarite-opp .card { --cw: 34px; }

/* YOUR hand: one row of twelve, never two, and the SAME size as the cards on
   the field, so a card in hand reads as the card it becomes when placed. It
   follows the field's height cap too, which is what keeps the hand on screen.
   Never smaller than a card you can still tap and read. Scoped through the
   game's own panel class (ui-conventions.md). */
.panel.polarite-handpanel {
    --polarite-handcw: max(46px, min(var(--cw), calc((100vw - 348px - 32px - 88px) / 12),
        calc((100vh - 520px) / 4 * 64 / 89) + 8px));
    padding: 8px 12px;
    margin-bottom: 6px;
}
.panel.polarite-handpanel h3 { margin: 0 0 6px; font-size: 14px; }
.polarite-handpanel .card { --cw: var(--polarite-handcw); }

/* Single column: the board may use the room the log column had. */
@media (max-width: 950px) {
    .polarite-grid {
        --polarite-cw: max(46px, min(var(--cw), calc((100vw - 354px) / 4),
            calc((100vh - 520px) / 4 * 64 / 89)));
    }
    .panel.polarite-handpanel {
        --polarite-handcw: max(40px, min(var(--cw), calc((100vw - 32px - 32px - 88px) / 12),
            calc((100vh - 520px) / 4 * 64 / 89) + 8px));
    }
}
/* Too narrow for a column beside the field: the cheat sheet drops underneath
   it, full width, and the field gets all four columns back. 620px is where a
   306px grid plus the 240px aid and the 14px gap stops fitting the playarea,
   measured; above it the aid stays at the field's side, which is where it is
   wanted. */
@media (max-width: 620px) {
    .polarite-boardrow { flex-direction: column; align-items: center; }
    .polarite-quark-aid { width: 100%; max-width: 420px; }
    /* A phone scrolls, and a tap target matters more than the fold here, so
       the field takes the width and no height cap applies. */
    .polarite-grid { --polarite-cw: min(var(--cw), calc((100vw - 100px) / 4)); }
    /* Twelve cards cannot fit one row on a phone at a size worth tapping, so
       the hand wraps to two (handspace.ts reserves the height). */
    .panel.polarite-handpanel { --polarite-handcw: var(--cw); }
}
/* Phones: tighter gaps, and the four columns share the full width. */
@media (max-width: 560px) {
    .polarite-grid {
        --polarite-gap: 3px;
        --polarite-cw: min(var(--cw), calc((100vw - 80px) / 4));
    }
}
body.motion-off .polarite-cell.hinted { animation: none; }
/* With motion off the 2px gold ring and the dimmed field still mark the
   targets; only the pulse goes. */
body.motion-off .polarite-cell.legal-extra { animation: none; }

/* ------------------------------------------------------------------------- */
/* Sono                                                                      */
/* ------------------------------------------------------------------------- */
/*
 * Prefix policy: every Sono-specific class is `sono-` (matching `polarite-`,
 * `cascade-`, `duel-`). Shared classes are reused as-is and get no rules
 * here: hand cards select with `.card.sel`, arm with `.clickable` and flash
 * with `.card.hinted`; the face-down diagonal seeds are `.card.simple.back`;
 * the action row is a `.controls`; the outcome uses `.winner-banner` and
 * `.result-note`.
 *
 * The board is ONE six-by-six grid rather than a grid plus separate score
 * strips, so the row chips down the right edge and the column chips along the
 * bottom stay aligned with their lines by construction - no shared width
 * variable to keep in sync, and nothing to re-align at a media query.
 */
/* Positioned so the line breakdown (#sono-detail) can hang off its bottom
   edge. No isolation here on purpose: the breakdown has to paint over the hand
   panel, which comes later in the page, and an isolated board would keep it
   inside the board's own stacking context. */
.sono-board { position: relative; display: flex; flex-direction: column; gap: 6px; }
/* The board and the other hand: one line where both fit, the hand under the
   board where not. Under the hand panel, the other hand cost a desktop the
   123px that put the board past the app layout's 1280x720 frame. */
.sono-tablerow {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    gap: 6px 16px;
    margin-bottom: 8px;
}
.sono-tablerow > [hidden] { display: none; }
.sono-tablerow > .panel.sono-opp { flex: 0 1 440px; min-width: 0; margin: 0; align-self: center; }
.sono-tablerow > .panel.sono-opp .cardrow { gap: 4px; justify-content: center; }

.sono-grid {
    --sono-gap: 5px;
    --sono-cw: min(var(--cw), calc((100vw - 520px) / 5));
    display: grid;
    grid-template-columns: repeat(5, var(--sono-cw)) auto;
    gap: var(--sono-gap);
    width: fit-content;
    margin: 2px auto;
    align-items: center;
    justify-items: center;
}
/* A shorter window, 760px and under (the app layout's 1280x720 frame), takes
   the grid's cells at 85% of the site size, which leaves the hand room under
   it. The 720px block below sets the phone size and is unchanged. */
@media (max-height: 760px) {
    .sono-grid { --sono-cw: min(calc(var(--cw) * 0.85), calc((100vw - 520px) / 5)); }
}
/* Less the cell's 1px border on each side, so a card fills its cell exactly. */
.sono-grid .card { --cw: calc(var(--sono-cw) - 2px); }
.sono-cell {
    position: relative;
    width: var(--sono-cw);
    height: calc(var(--sono-cw) * 89 / 64); /* CARD_ASPECT */
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 8px;
    border: 1px solid transparent; /* so highlights never shift layout */
}
.sono-cell.empty {
    background: rgba(255, 255, 255, 0.05);
    border: 1px dashed rgba(255, 255, 255, 0.25);
}
/* The diagonal seats the five face-down seeds: mark it even once turned up,
   because "which cell was dealt, not played" stays useful all game. */
.sono-cell.diag.empty { background: rgba(255, 209, 102, 0.09); }
.sono-cell.diag::after {
    content: '';
    position: absolute;
    inset: auto 3px 3px auto;
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: rgba(255, 209, 102, 0.55);
}
.sono-cell.legal {
    cursor: pointer;
    background: rgba(255, 255, 255, 0.14);
    border: 1px solid rgba(255, 255, 255, 0.55);
}
.sono-cell.legal:hover, .sono-cell.legal:focus-visible {
    background: rgba(255, 255, 255, 0.22);
    outline: none;
}
/* An empty cell nothing touches yet: inert, so the lit set reads clearly. */
.sono-cell.unreachable { opacity: 0.45; }
/* Hint flash on the suggested cell (the .card.hinted treatment on a cell). */
.sono-cell.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
/* The card played last, until the next one lands. Cool blue, the duel-poker
   .last-played / Dracula .last-placed hue, because gold already means
   selected, legal and hinted here. On the CELL, not the card: the card is what
   flies in from the hand, and a class on it would ride the flight ghost. The
   ring sits mostly over the card's own edge (negative offset), so it never
   reaches across the narrow phone gap into the next cell, and an outline
   changes no geometry, so nothing re-flows when it moves. */
.sono-cell.sono-last {
    outline: 3px solid #6aa9e0;
    outline-offset: -2px;
    box-shadow: 0 0 9px rgba(106, 169, 224, 0.65);
}
/* An open breakdown: the rest of the tableau steps back so the five cells
   being counted stand out. Opacity changes no layout box, so the card-flight
   animator cannot read it as movement. */
.sono-grid.sono-focus .sono-cell:not(.sono-inline) { opacity: 0.35; }
/* Score chips: the right-hand column is the rows, the bottom row the columns.
   Tinted for whose line it is, so the board says who is building what. Each
   one is a <button> that opens its line's breakdown, so the browser's own
   button font, color and background are reset first. */
.sono-score {
    font: inherit;
    color: inherit;
    background: transparent;
    cursor: pointer;
    min-width: 1.6em;
    padding: 1px 4px;
    border-radius: 5px;
    border: 1px solid var(--panel-border);
    font-size: 12px;
    font-weight: 700;
    text-align: center;
    font-variant-numeric: tabular-nums;
    opacity: 0.55;
}
.sono-score.scoring, .sono-score:hover { opacity: 1; }
.sono-score:focus-visible { opacity: 1; outline: 2px solid #fff; outline-offset: 1px; }
.sono-score.mine { border-color: var(--accent); color: var(--accent); }
/* The chip whose breakdown is open. A color change only, so the chip keeps its
   size and the column of chips never shifts. */
.sono-score.open {
    opacity: 1;
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-ink);
}
.sono-score-column { min-width: 0; width: 100%; }
.sono-corner { width: 100%; }
/* --- one line's breakdown ------------------------------------------------ */
/* It OVERLAYS the area under the board instead of inserting a row, so opening
   it never pushes the hand down: opaque, capped in height with its own scroll.
   z-index 20 puts it over the hand panel (whose cards are unpositioned) and
   under the phone-docked tutorial coach (30) and every modal (500 and up). */
.sono-detail {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 20;
    width: min(380px, 100%);
    margin: 4px auto 0;
    max-height: 60vh;
    overflow-y: auto;
    background: var(--felt-dark);
    border-color: var(--accent);
    box-shadow: 0 8px 22px rgba(0, 0, 0, 0.5);
}
.sono-detailhead {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
}
.sono-detailhead h3 { margin: 0; font-size: 14px; white-space: nowrap; }
/* The line's five cells in order. An empty cell keeps its room, so the list
   below does not shuffle as the line fills. */
.sono-detailcards {
    display: flex;
    justify-content: center;
    align-items: flex-end;
    gap: 4px;
    margin: 8px 0;
}
.sono-detail .card { --cw: 38px; }
.sono-detailgap {
    width: 38px;
    height: calc(38px * 89 / 64); /* CARD_ASPECT */
    border-radius: 5px;
    border: 1px dashed rgba(255, 255, 255, 0.22);
}
.sono-detaillist { list-style: none; margin: 0; padding: 0; }
.sono-detailitem {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 3px 0;
    font-size: 13px;
    border-top: 1px solid rgba(255, 255, 255, 0.07);
}
.sono-detailpts {
    min-width: 1.6em;
    text-align: right;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
.sono-detaillabel { flex: 0 0 auto; }
/* The cards that make one part (the pair, the four), when they are only some
   of the line. Declared after `.sono-detail .card` so the smaller size wins. */
.sono-detailmade { flex: 1 1 auto; display: flex; justify-content: flex-end; gap: 3px; }
.sono-detailmade .card { --cw: 26px; }
.sono-detailtotal {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-top: 6px;
    padding-top: 5px;
    border-top: 1px solid var(--panel-border);
    font-weight: 700;
}
.sono-detailnote { margin: 6px 0 0; font-size: 12px; opacity: 0.8; }
/* The other hand: backs while it is private, faces once the diagonal is up. */
.sono-opp .card { --cw: 34px; }
.sono-error { margin-top: 4px; font-size: 13px; color: #ff9c8a; }
/* The two scoring columns of the result panel, side by side on a wide screen. */
.sono-results { display: flex; flex-wrap: wrap; gap: 14px; }
.sono-resultcol {
    flex: 1 1 240px;
    padding: 8px 10px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
}
.sono-resultcol.won { border-color: var(--accent); }
.sono-resultcol h3 { margin: 0 0 6px; font-size: 14px; }
.sono-resultline {
    display: flex;
    align-items: baseline;
    gap: 8px;
    font-size: 13px;
    opacity: 0.6;
}
.sono-resultline.scoring { opacity: 1; }
.sono-resultname { min-width: 5.5em; opacity: 0.8; }
.sono-resultlabel { flex: 1 1 auto; }
.sono-resultpts { font-weight: 700; font-variant-numeric: tabular-nums; }
.sono-resulttotal {
    margin-top: 6px;
    padding-top: 5px;
    border-top: 1px solid var(--panel-border);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
@media (max-width: 720px) {
    .sono-grid { --sono-cw: min(52px, calc((100vw - 58px) / 5)); --sono-gap: 3px; }
    .sono-opp .card { --cw: 26px; }
}
body.motion-off .sono-cell.hinted { animation: none; }
/* ==== River Rats (riverrats.html) - everything shared (.card, .cardrow,
   .panel, .panel-head, .pile-chip, .logline, .result-actions, .winner-banner,
   .btn, .segmented, .sel, .lit, .hinted, .card.simple.back, .card.empty) is
   reused as-is; only what is genuinely this game's own lives here, all of it
   prefixed `rr-`.

   The table reads top to bottom: the stakes rail, the two Rats, the Rat's
   Hand, your Collective Hand, the showdown, the Market, your hand. ==== */

/* ---------- the stakes rail: Prediction, Jokers, Debt ---------- */
.rr-track {
    display: flex;
    gap: 14px;
    flex-wrap: wrap;
    align-items: flex-start;
    padding: 6px 10px;
    margin-bottom: 8px;
    border: 1px solid var(--accent);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.04);
}
.rr-track-group { display: flex; flex-direction: column; gap: 4px; }
.rr-track-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    opacity: 0.7;
}
.rr-track-row { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; }
.rr-track-note { font-size: 12px; opacity: 0.85; }
.rr-track .card { --cw: 30px; }
/* A Joker already out of the game: the slot stays, so the count still reads. */
.card.rr-joker-gone {
    --cw: 30px;
    background: none;
    border: 1px dashed rgba(255, 255, 255, 0.22);
    opacity: 0.5;
}
.rr-chip {
    display: flex;
    flex-direction: column;
    align-items: center;
    min-width: 74px;
    padding: 3px 8px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.2);
}
.rr-chip-label { font-size: 11px; opacity: 0.75; white-space: nowrap; }
.rr-chip-value { font-weight: 800; font-variant-numeric: tabular-nums; }
/* Green when Debt is landing on a Rat, red when it is landing on you: the same
   number means opposite things depending on whose track it is. */
.rr-chip.good .rr-chip-value { color: #9be59b; }
.rr-chip.danger { border-color: var(--red); }
.rr-chip.danger .rr-chip-value { color: var(--red); }
.rr-chip.done { opacity: 0.6; }

/* ---------- the two River Rats ---------- */
.rr-ratrow { display: flex; gap: 14px; flex-wrap: wrap; }
.rr-ratseat { display: flex; gap: 8px; align-items: flex-start; flex: 1 1 240px; }
.card.rr-ratcard { --cw: 48px; }
.rr-ratseat.rr-rat-active .card.rr-ratcard { box-shadow: 0 0 0 2px var(--accent); }
/* A defeated Rat is ROTATED on the table, and its card stays in play - the
   rotation is the rulebook's own signal that the Ability is dead. */
.rr-ratseat.rr-rat-defeated .card.rr-ratcard {
    /* `rotate`, never `transform` (ui-conventions, "A sideways card turns
       with rotate"). */
    rotate: 90deg;
    margin: 12px 8px;
}
.rr-ratseat.rr-rat-inactive { opacity: 0.65; }
.rr-ratinfo { flex: 1 1 auto; min-width: 0; }
.rr-ratname { font-weight: 700; font-size: 14px; }
.rr-ratdebt { font-variant-numeric: tabular-nums; font-size: 13px; color: #9be59b; }
.rr-ratnote { font-size: 12px; opacity: 0.8; }
.rr-ratbonus { font-size: 12px; color: var(--accent); }

/* ---------- the Rat's Hand and the Collective Hand ---------- */
.rr-headnote { font-size: 12px; opacity: 0.75; }
.card.rr-ratking { box-shadow: 0 0 0 2px var(--accent); }
/* The Rat's Hand is a READ-ONLY strip of 9-13 cards, so it is drawn small and
   from the simplified atlas (see the thresholds in ui-conventions.md: at or
   below 46px a read-only zone takes `simple`, and the full art is a smear well
   before the row would fit). The tighter gap is what buys the extra cards. */
.rr-rathand .cardrow { gap: 5px; }
.rr-rathand .card { --cw: 44px; }
/* Except when the Hearts Power is armed: then each face-down card is a real
   <button> the player has to hit, so it has to clear the 40px touch floor the
   rest of this strip sits below. Four classes, so this beats the phone rule
   further down on specificity rather than relying on source order, and the row
   scrolls, so one wider card costs nothing. */
.rr-rathand .card.back.clickable { --cw: 46px; }
/* The floor the round has to clear, read off the Rat's face-up cards. */
.rr-floor { margin-top: 6px; font-size: 13px; color: var(--accent); }
.rr-collective .card { --cw: 56px; }
.rr-slot { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.rr-slotcap { font-size: 11px; opacity: 0.75; }
/* The Spades Ability's locked first card, and a named Joker: both are still
   real faces, so the marker is a border rather than a cover. */
.card.rr-facedown { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.35); opacity: 0.85; }
.card.rr-declared { box-shadow: 0 0 0 2px var(--accent); }
.rr-empty { font-size: 13px; opacity: 0.7; padding: 6px 0; }

/* ---------- the showdown ---------- */
.rr-showdown .winner-banner { font-size: 20px; }
.rr-showdown .winner-banner.bad { color: var(--red); }
.rr-sd-compare { display: flex; gap: 22px; flex-wrap: wrap; margin-bottom: 10px; }
.rr-sd-side {
    flex: 1 1 240px;
    padding: 8px 10px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
}
.rr-sd-side.best { border-color: var(--accent); }
.rr-sd-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    opacity: 0.7;
    margin-bottom: 4px;
}
.rr-sd-name { margin-top: 4px; font-size: 13px; font-weight: 600; }
.rr-sd-note { font-size: 13px; opacity: 0.85; }
.rr-sd-defeat { margin-top: 6px; font-size: 13px; color: var(--accent); }
.rr-sd-all { margin-top: 10px; }
/* Which of the Rat's cards actually played, and which were spare. */
.card.rr-sd-used { box-shadow: 0 0 0 2px var(--accent); }
.card.rr-sd-spare { opacity: 0.45; }

/* ---------- the Market ---------- */
/* Full art, not `simple`: the Market is a PICK target (tapping a card takes it),
   which is the side of the 35-46px band that keeps the illustrations. */
.rr-market .card { --cw: 50px; }

/* ---------- your hand and the action column ---------- */
.panel.rr-handpanel .rr-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
}
.panel.rr-handpanel #hand-cards { flex: 1 1 auto; }
.rr-actions {
    flex: 0 0 268px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: stretch;
}
.rr-actionnote { font-size: 13px; opacity: 0.9; }
.rr-actionnote.rr-dim { opacity: 0.6; }
.rr-actionsep {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    opacity: 0.6;
    text-align: center;
    border-top: 1px solid var(--panel-border);
    padding-top: 6px;
}
/* The Joker naming picker: a suit row and a rank row, not 52 buttons. */
.rr-picker { display: flex; gap: 4px; flex-wrap: wrap; }
.rr-picker .btn { padding: 4px 8px; min-width: 32px; }
.rr-picker .btn.selected { background: var(--accent); color: var(--accent-ink); }

/* ---------- the card counter ---------- */
.rr-unseen { margin-bottom: 10px; }
.rr-unseen-row {
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
    margin-top: 6px;
}
.rr-unseen-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    min-width: 30px;
    padding: 2px 4px;
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.2);
}
.rr-unseen-rank { font-size: 11px; opacity: 0.75; }
.rr-unseen-count { font-weight: 700; font-variant-numeric: tabular-nums; }
/* A rank with none left is the useful negative: the draw is dead, so it is dimmed
   rather than hidden - the column has to stay in place to be scannable. */
.rr-unseen-none { opacity: 0.4; }
/* Suit inks, derived the platform way (see the --red-diamond note in
   platform.css): hearts keeps --red and spades keeps the default ink, so only the
   second suit of each family carries a new value. */
.rr-unseen-suit { font-size: 14px; opacity: 1; }
.rr-unseen-suit.red { color: var(--red); }
.rr-unseen-suit.red.diamond { color: var(--red-diamond); }
.rr-unseen-suit.club { color: var(--blue-club); }

/* ---------- the log ---------- */
.logline.rr-loggood { color: #9be59b; }
.logline.rr-logbad { color: var(--red); }

/* ---------- the rules panels ---------- */
.rr-ruletable { border-collapse: collapse; margin: 8px 0; font-size: 14px; }
.rr-ruletable th,
.rr-ruletable td { text-align: left; padding: 3px 12px 3px 0; vertical-align: top; }
.rr-ruletable thead th {
    font-size: 12px;
    text-transform: uppercase;
    opacity: 0.7;
    letter-spacing: 0.4px;
}
.rr-ruletable tbody th { font-size: 16px; white-space: nowrap; }
.rr-note { font-size: 13px; opacity: 0.8; }

@media (max-width: 720px) {
    .rr-track { gap: 10px; padding: 5px 8px; }
    .rr-track .card { --cw: 26px; }
    .card.rr-joker-gone { --cw: 26px; }
    .rr-chip { min-width: 58px; }
    .card.rr-ratcard { --cw: 40px; }
    /* 30px x 9 cards + 8 gaps = 310px, inside a 360px phone's usable width, so
       the common Rat's Hand needs no scrolling at all; the 13-card expert one
       scrolls rather than wrapping. */
    .rr-rathand .cardrow { gap: 4px; }
    .rr-rathand .card { --cw: 30px; }
    .rr-collective .card { --cw: 44px; }
    .rr-market .card { --cw: 40px; }
    .rr-ratrow { gap: 8px; }
    /* The action column gives up its own column before the hand does. */
    .panel.rr-handpanel .rr-handrow { flex-wrap: wrap; }
    .rr-actions { flex: 1 1 100%; }
}
body.motion-off .rr-ratseat.rr-rat-defeated .card.rr-ratcard {
    rotate: none;
    margin: 0;
}
/* ==== Gallery Gambit (gallerygambit.html) - only what is genuinely new
        lives here; the panels, buttons and cards are the shared classes ====
   Three ideas in this layout. An Exhibit is a FIXED-WIDTH FRAME: its empty
   slots are always drawn and the text under it never sets its width, because
   "how many slots have I got left" is the central question of the game and a
   frame that grew as it filled would push its neighbors about. Everything on
   the board axis is CENTERED (the seats, the Market, your gallery). And every
   line whose wording changes (the status line, the action bar, a seat's
   recap, the hand note) reserves its tallest shape, so nothing below it
   steps up and down the screen. */
.gg-board { display: flex; flex-direction: column; gap: 10px; }

/* ---------- the status line: a recap of the last turn, then the ask ---------- */
.status.gg-status { min-height: 3.1em; }
.gg-recap { display: block; font-size: 14px; font-weight: 500; opacity: 0.75; }
.gg-ask { display: block; }
.gg-ask.gg-hint { color: var(--accent); }
/* Lit whenever this device may act. The padding is unconditional, so the bar
   can appear without shifting the text sideways. */
.status.gg-status { border-left: 3px solid transparent; padding-left: 10px; }
.status.gg-status.gg-turn { border-left-color: var(--accent); }

/* ---------- the opponents ---------- */
.gg-opponents {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
}
.panel.gg-seat { padding: 8px 10px; }
.panel.gg-seat.gg-active { outline: 2px solid rgba(255, 209, 102, 0.6); }
.gg-seat-head {
    display: flex;
    justify-content: space-between;
    gap: 10px;
    font-weight: 700;
    font-size: 14px;
}
.gg-seat-score { font-variant-numeric: tabular-nums; opacity: 0.9; }
/* One line, always, whatever the last turn was: the text never sets the
   panel's width (width 0 plus a 100% floor), and a long one is cut off with
   the whole sentence in its tooltip. */
.gg-seat-recap {
    width: 0;
    min-width: 100%;
    font-size: 12px;
    opacity: 0.8;
    margin: 2px 0 6px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.gg-ex-strip { display: flex; flex-wrap: nowrap; gap: 6px; align-items: flex-start; }
.gg-seat { --gg-oppcw: 30px; }
.gg-seat .card.mini { --cw: var(--gg-oppcw); }
.gg-seat .card.empty.mini { --cw: var(--gg-oppcw); }
.gg-opp-hand { display: flex; flex-direction: column; gap: 3px; align-items: center; }
.gg-opp-hand-label { font-size: 12px; font-weight: 700; opacity: 0.8; }
/* Room for two cards always, so the panel does not change width mid-turn. */
.gg-opp-hand-cards {
    gap: 3px;
    flex-wrap: nowrap;
    min-width: calc(2 * var(--gg-oppcw) + 3px);
    justify-content: center;
}
/* Their latest move, kept until their next turn. Quieter than the bot
   telegraph (.ai-telegraph), which is the same accent at full strength. */
.gg-seat .card.gg-recent {
    outline: 2px solid rgba(255, 209, 102, 0.8);
    outline-offset: 1px;
}
.gg-exhibit.gg-recent-ex { border-color: rgba(255, 209, 102, 0.8); }
/* A hand card the whole table knows (it was drafted face up). */
.card.gg-known { box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.9); }

/* ---------- the deck and the Market ---------- */
.gg-marketpanel { padding-top: 8px; padding-bottom: 10px; }
.gg-marketrow {
    --gg-mcw: 64px;
    display: flex;
    flex-wrap: nowrap;
    gap: 14px;
    align-items: center;
    overflow-x: auto;
    /* One card row, whatever the cards shrink to. */
    min-height: calc(var(--gg-mcw) * 89 / 64 + 6px);
}
/* Centered by auto margins rather than justify-content, so an overflow at a
   narrow width scrolls instead of clipping its left edge. */
.gg-marketrow > :first-child { margin-left: auto; }
.gg-marketrow > :last-child { margin-right: auto; }
/* The deck is a control, not decoration: tapping it is the whole Draw action,
   so it gets the card back from the simplified atlas and a count badge. */
.gg-deck { display: flex; flex: none; }
.card.gg-deckcard {
    --cw: var(--gg-mcw);
    position: relative;
    display: grid;
    place-items: center;
    cursor: default;
}
button.card.gg-deckcard { cursor: pointer; transition: transform 120ms ease; }
button.card.gg-deckcard:hover { transform: translateY(-5px); }
.card.gg-deckcard.gg-empty { opacity: 0.4; }
.gg-deckcount {
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: #fff;
    background: rgba(0, 0, 0, 0.62);
    border-radius: 8px;
    padding: 1px 7px;
    font-size: 13px;
}
.gg-market {
    flex: none;
    gap: 10px;
    align-items: center;
    flex-wrap: nowrap;
}
.gg-market .card { --cw: var(--gg-mcw); }
/* The eight-card Market of the Joker exchange: smaller cards, same row. */
.gg-market.gg-many { gap: 6px; }
.gg-market.gg-many .card { --cw: calc(var(--gg-mcw) * 0.75); }
.card.gg-inert { opacity: 0.4; }

/* ---------- the action bar: a key to the three gestures ---------- */
.controls.gg-actions {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 8px;
    min-height: 48px;
    margin: 0;
}
.gg-action {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 5px 10px;
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.14);
    opacity: 0.55;
}
.gg-actions-live .gg-action { opacity: 1; border-color: rgba(255, 209, 102, 0.45); }
.gg-action.gg-action-on { opacity: 1; border-color: var(--accent); }
.gg-action-or { font-size: 12px; opacity: 0.6; }
.gg-action-words { display: flex; flex-direction: column; line-height: 1.2; }
.gg-action-title { font-weight: 700; font-size: 13px; white-space: nowrap; }
.gg-action-how { font-size: 12px; opacity: 0.8; white-space: nowrap; }
.gg-stage-title { font-weight: 700; white-space: nowrap; }

/* ---------- your gallery: A, B, C and the hand in one centered row ---------- */
.panel.gg-youpanel.gg-active { outline: 2px solid rgba(255, 209, 102, 0.6); }
.gg-yourow {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    gap: 12px;
}
.gg-exhibits {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px;
    align-items: flex-start;
}
.gg-stash {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 8px 10px;
}
.gg-stash-label { font-size: 13px; font-weight: 700; }
.gg-cardrow {
    --gg-handcw: 58px;
    gap: 8px;
    flex-wrap: nowrap;
    /* Two cards' room always: the row is centered, so a hand that grew and
       shrank would slide every Exhibit beside it. */
    min-width: calc(2 * var(--gg-handcw) + 8px);
    justify-content: center;
}
.gg-cardrow .card { --cw: var(--gg-handcw); }
.gg-hand-note {
    text-align: center;
    font-size: 12px;
    opacity: 0.8;
    min-height: 1.4em;
    margin-top: 4px;
}

/* ---------- one Exhibit ---------- */
.gg-exhibit {
    display: flex;
    flex-direction: column;
    gap: 5px;
    padding: 7px 9px;
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.14);
}
.gg-exhibit:not(.gg-compact) { min-width: 150px; }
/* Paying something right now. */
.gg-exhibit.gg-set { border-color: rgba(255, 209, 102, 0.45); }
/* Chosen first: half of a Gambit, or the wall a placement goes to. */
.gg-exhibit.gg-picked { border-color: #78c8ff; box-shadow: 0 0 0 1px #78c8ff; }
.gg-exhibit.gg-staged {
    border-color: #78c8ff;
    box-shadow: 0 0 0 1px #78c8ff;
    background: rgba(120, 200, 255, 0.1);
}
/* A legal home for the hand card picked right now. */
.gg-exhibit.gg-target {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}
.gg-ex-head {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    align-items: baseline;
    font-size: 13px;
    font-weight: 700;
    white-space: nowrap;
}
/* On your own galleries the Exhibit name is a real button whenever a tap on
   it does something, so the keyboard reaches it. */
/* A head is a button in some phases and a span in others, so both carry the
   same line box and the same 1px underline, or the frame steps 2px. */
.gg-ex-name { line-height: 18px; border-bottom: 1px solid transparent; }
.gg-ex-pts { line-height: 18px; }
.gg-ex-button {
    font: inherit;
    line-height: 18px;
    color: inherit;
    background: none;
    border: none;
    border-bottom: 1px dotted rgba(255, 255, 255, 0.45);
    padding: 0;
    cursor: pointer;
    text-align: left;
}
.gg-ex-button:hover { color: var(--accent); border-bottom-color: var(--accent); }
.gg-ex-pts { font-variant-numeric: tabular-nums; opacity: 0.85; }
.gg-ex-pts.gg-ex-lead { color: var(--accent); opacity: 1; }
.gg-ex-cards { gap: 5px; flex-wrap: nowrap; }
.gg-ex-cards .card { --cw: 54px; }
/* The two lines under an Exhibit: its Set kind, then its Battle. Neither may
   set the frame's width, and each stays one line. */
.gg-ex-note,
.gg-ex-battle {
    width: 0;
    min-width: 100%;
    font-size: 12px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.gg-ex-note { opacity: 0.8; }
.gg-ex-battle { opacity: 0.7; }
.gg-ex-battle.gg-ex-lead { color: var(--accent); opacity: 1; }
/* The cards that are actually scoring; the rest of the wall scores nothing. */
.card.gg-scoring { box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.85); }
/* The Joker being exchanged right now. */
.card.gg-swapping { outline: 3px solid #78c8ff; outline-offset: 1px; }
.card.gg-dimmed { opacity: 0.4; }

/* The compact frame in an opponent's panel. */
.gg-exhibit.gg-compact { padding: 4px 6px; gap: 3px; }
.gg-exhibit.gg-compact .gg-ex-head { font-size: 12px; gap: 6px; }
.gg-exhibit.gg-compact .gg-ex-cards { gap: 3px; }

/* An empty slot you may click: the shared dashed placeholder, armed. */
.card.empty.gg-slot { cursor: pointer; border-color: var(--accent); }
.card.empty.gg-slot:hover { background: rgba(255, 209, 102, 0.14); }
.card.gg-selected {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    transform: translateY(-4px);
}

/* ---------- the Round summary, lifted into a dialog by the shell ---------- */
.gg-card { display: flex; flex-direction: column; gap: 4px; }
.gg-card-title { font-weight: 700; margin-bottom: 4px; text-align: center; }
.gg-card-grid { display: flex; flex-direction: column; gap: 4px; }
.gg-card-row {
    display: grid;
    grid-template-columns: minmax(70px, 1fr) repeat(3, minmax(96px, 1.6fr)) 52px 52px;
    gap: 8px;
    align-items: start;
    padding: 5px 8px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.05);
}
.gg-card-row.gg-card-head { background: none; font-size: 12px; opacity: 0.75; }
.gg-card-name { font-weight: 700; align-self: center; }
.gg-card-cell { display: flex; flex-direction: column; gap: 2px; }
.gg-card-cell.gg-card-nil { opacity: 0.55; }
.gg-cell-pts { font-weight: 700; font-variant-numeric: tabular-nums; }
.gg-card-cell.gg-card-won .gg-cell-pts { color: var(--accent); }
.gg-cell-set { font-size: 11px; opacity: 0.8; }
.gg-cell-battle { opacity: 1; color: #78c8ff; }
/* A card that fought the Battle but is not in the Set that scored. */
.gg-cell-cards .card.gg-battle-only { outline: 2px dashed #78c8ff; outline-offset: 1px; }
.gg-cell-cards { display: flex; flex-wrap: nowrap; gap: 2px; }
.gg-cell-cards .card.mini { --cw: 22px; }
.gg-card-total {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    text-align: right;
    align-self: center;
}
.gg-card-legend { font-size: 12px; opacity: 0.7; margin-top: 6px; text-align: center; }

/* ---------- final standings ---------- */
.gg-final { display: flex; flex-direction: column; gap: 4px; margin: 10px 0; }
.gg-final-row {
    display: grid;
    grid-template-columns: 1fr auto 56px;
    gap: 12px;
    padding: 4px 10px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.05);
}
.gg-final-row.gg-final-winner { background: rgba(255, 209, 102, 0.18); font-weight: 700; }
.gg-final-rounds { font-variant-numeric: tabular-nums; opacity: 0.8; }
.gg-final-score { font-variant-numeric: tabular-nums; text-align: right; }

@media (max-width: 620px) {
    .status.gg-status { min-height: 5.3em; }
}
@media (max-width: 560px) {
    /* The Market, the hand, the exhibits, and the opponents' hands are fixed
       frames that stay one line on a phone too. Each says so in its own rule
       (.gg-market, .gg-cardrow, .gg-ex-cards, .gg-opp-hand-cards), and those
       beat the shared phone wrap rule on order, so nothing is restated here. */
    /* A, B and C become items of your gallery row itself, so the hand can sit
       beside Exhibit C instead of taking a row of its own. */
    .gg-exhibits { display: contents; }
    /* The Market holds 1 to 8 cards nearly always (more about 3% of turns),
       and a second row would step everything below it down the screen. So
       its cards shrink with the count to keep ONE row (the row reserves its
       height at the largest card), measured against the 278px left beside
       the deck at 390px. From 7 cards they are 34px and below, which is why
       def.ts paints them from the simplified atlas. Past 8 the row scrolls. */
    .gg-market.gg-n5 { gap: 6px; }
    .gg-market.gg-n6 .card { --cw: 40px; }
    .gg-market.gg-n7 .card { --cw: 34px; }
    .gg-market.gg-n8 .card { --cw: 29px; }
    .gg-seat { --gg-oppcw: 22px; }
    .gg-ex-strip { gap: 4px; }
    .gg-marketrow { --gg-mcw: 48px; gap: 10px; }
    .gg-ex-cards .card { --cw: 40px; }
    .gg-exhibit:not(.gg-compact) { min-width: 132px; }
    .gg-ex-note, .gg-ex-battle { font-size: 11px; }
    .gg-cardrow { --gg-handcw: 48px; }
    /* The three action keys stay on one row: the how-to line wraps under its
       title rather than the chips wrapping under each other. */
    .controls.gg-actions { gap: 5px; }
    .gg-action { padding: 4px 6px; gap: 5px; }
    .gg-action-or { font-size: 11px; }
    .gg-action-title { font-size: 12px; }
    .gg-action-how { font-size: 11px; white-space: normal; }
    .gg-card-row {
        grid-template-columns: minmax(56px, 1fr) repeat(3, 1fr) 40px 40px;
        gap: 5px;
        font-size: 13px;
    }
    .gg-cell-cards { flex-wrap: wrap; }
    .gg-cell-cards .card.mini { --cw: 20px; }
}


/* The Set table in the Cheat_Sheet (pages/gallerygambit.mjs), on the board,
   the setup screen, and the gallery-gambit/rules/ page alike. */
.gg-settable { border-collapse: collapse; margin: 6px 0 10px; }
.gg-settable th,
.gg-settable td {
    text-align: left;
    padding: 3px 12px 3px 0;
    vertical-align: baseline;
    white-space: nowrap;
}
.gg-settable th {
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.55;
}
/* ==== Royal Divorce. Everything shared (.panel, .cardrow, .card, .btn,
   .logline, .result-actions, .winner-banner, .sel, .lit, .clickable,
   .card.simple.back) is reused as-is; only what is genuinely this game's
   own lives here, all of it prefixed `rdiv-`. The board is two SIDES
   (opponent above, viewer below) around a middle strip holding the discard
   pool and the Nest Egg. ==== */

/* ---------- the board grid ----------
   The two sides and the middle strip share ONE two-column grid, centered on
   the table:

       [ Commission ] [ six-slot tableau half ]   <- opponent
       [  Nest Egg  ] [ discard pool          ]   <- middle strip
       [ Commission ] [ six-slot tableau half ]   <- viewer

   Cards fill the second column outward from the Commission and never
   reflow. The column is sized so a FINISHED round sits centered, and that
   is 4.5 cards, not six: a side can take six, but failed Defenses and dead
   Claims send cards to the pool instead, so a round ends on 4 (45%) or 5
   (33%) far more often than 6 (5%), measured over 1,200 self-play rounds.
   Centered on 4.5, both usual finishes land within a quarter card of
   center; a sixth card simply runs on past the column to the right. The
   three boxes have identical horizontal padding and borders, so the
   centered grids line up. */
.rdiv-side, .rdiv-mid {
    --rdiv-cw: 48px;          /* tableau and pool card width */
    --rdiv-gap: 4px;
    --rdiv-lead: 64px;        /* the Commission / Nest Egg column */
    --rdiv-area: calc(4.5 * var(--rdiv-cw) + 3.5 * var(--rdiv-gap));
    /* Room for a full six-card half, for what may overhang the column. */
    --rdiv-full: calc(6 * var(--rdiv-cw) + 5 * var(--rdiv-gap));
}
.rdiv-siderow, .rdiv-midrow {
    display: grid;
    grid-template-columns: var(--rdiv-lead) var(--rdiv-area);
    column-gap: 12px;
    justify-content: center;
    align-items: center;
}

/* ---------- the two sides ---------- */
/* 6px between the three bands, not 10: the 1280x720 app-layout frame had
   12px too few for the tracker under the hand. */
.rdiv-side {
    padding: 8px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.03);
    margin-bottom: 6px;
}
.rdiv-sidehead {
    display: flex;
    gap: 12px;
    align-items: baseline;
    justify-content: center;
    margin-bottom: 6px;
    font-size: 13px;
}
.rdiv-sidename { font-weight: 700; font-size: 14px; }
.rdiv-score { color: var(--accent); font-weight: 700; }
.rdiv-commission {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.card.rdiv-commissioncard { --cw: 56px; }
.rdiv-tableau {
    display: flex;
    gap: var(--rdiv-gap);
    align-items: center;
    justify-content: flex-start;
    /* A fifth or sixth card - or a pending Claim lying sideways - runs on
       past the column (overflow is visible) rather than wrapping. */
    flex-wrap: nowrap;
    min-height: calc(var(--rdiv-cw) * 89 / 64);
}
.card.rdiv-tableaucard { --cw: var(--rdiv-cw); }
/* The unanswered Claim lies sideways at the growing end of the tableau. */
/* `rotate`, never `transform`, and scoped under the tableau so a flight ghost
   (a clone in <body>) matches nothing here (ui-conventions, "A sideways card
   turns with rotate"). */
.rdiv-tableau .card.rdiv-tableaucard.rdiv-sideways {
    rotate: 90deg;
    margin: 0 calc((var(--rdiv-cw) * 89 / 64 - var(--rdiv-cw)) / 2);
}
/* At the reveal, the cards that actually paid their side. */
.card.rdiv-tableaucard.rdiv-scored { box-shadow: 0 0 0 2px var(--accent); }
.rdiv-cap { font-size: 11px; opacity: 0.75; text-align: center; }
.rdiv-tableau > .rdiv-cap { text-align: left; }

/* ---------- the middle strip: Nest Egg + discard pool ---------- */
.rdiv-mid {
    padding: 8px 12px;
    border: 1px solid var(--accent);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.04);
    margin-bottom: 6px;
}
.rdiv-nestegg {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.card.rdiv-neCard { --cw: 56px; }
.rdiv-pool {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
}
/* The pool grows all round (failed Defenses, dead Claims), so unlike the
   tableau it may wrap - but only after six, the same run a full tableau
   half may take past the column, so the strip stays one line in practice. */
.rdiv-poolcards {
    display: flex;
    gap: var(--rdiv-gap);
    align-items: center;
    flex-wrap: wrap;
    width: var(--rdiv-full);
}
.card.rdiv-poolcard { --cw: var(--rdiv-cw); }
/* The peek variant's private glimpse: face up for the viewer only, dimmed
   to read as "face down on the table, but YOU know it". A DASHED neutral
   outline, never the accent: gold on a pool card means "picked for the
   trade", and the two sit side by side in the same strip. */
.card.rdiv-peeked {
    opacity: 0.72;
    outline: 2px dashed rgba(255, 255, 255, 0.6);
    outline-offset: 1px;
}

/* ---------- the hand and the action column ---------- */
.panel.rdiv-handpanel .rdiv-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
}
.panel.rdiv-handpanel #hand-cards { flex: 1 1 auto; }
/* A card that cannot be played right now (an illegal Defense, a Wild
   offered as a Commission): still readable, plainly not an option. */
.card.rdiv-dim { opacity: 0.38; filter: grayscale(0.5); }
.rdiv-actions {
    flex: 0 0 244px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: stretch;
}
.rdiv-actionnote { font-size: 13px; opacity: 0.9; }

/* ---------- the deduction tracker ---------- */
.rdiv-tracker { display: flex; flex-direction: column; gap: 8px; }
/* The two rows share a caption column, so their card strips start on the
   same line and the same card sits in the same place in both. */
.rdiv-trackrow {
    display: grid;
    grid-template-columns: 12em minmax(0, 1fr);
    column-gap: 10px;
    align-items: center;
}
.rdiv-trackcap { font-size: 12px; opacity: 0.85; }
.rdiv-trackcards {
    display: flex;
    gap: 3px;
    align-items: center;
    flex-wrap: wrap;
}
.card.rdiv-trackcard { --cw: 30px; }
/* Provably in the opponent's hand (an open pool trade). */
.card.rdiv-trackcard.rdiv-known { box-shadow: 0 0 0 2px var(--panel-border); }
/* Provably NOT in their hand: the better Nest Egg guesses. */
.card.rdiv-trackcard.rdiv-hot { box-shadow: 0 0 0 2px var(--accent); }

/* ---------- the round breakdown (reveal, and the final result) ----------
   One centered row of tiles - Commission | Nest Egg | Commission - each a
   caption, its card, and the verdict under it, every line centered on the
   tile so titles and cards share one axis. */
.rdiv-reveal {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: 36px;
    flex-wrap: wrap;
    margin: 4px 0 10px;
}
.rdiv-revealtile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    text-align: center;
    min-width: 110px;
    max-width: 190px;
}
.rdiv-revealwho { font-weight: 700; font-size: 14px; }
.rdiv-revealcards { display: flex; align-items: center; gap: 6px; }
.rdiv-revealarrow { font-size: 18px; opacity: 0.7; }
.card.rdiv-revealcard { --cw: 56px; }
.rdiv-revealverdict { font-size: 12px; opacity: 0.85; }
.rdiv-revealsum {
    display: flex;
    align-items: baseline;
    justify-content: center;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.rdiv-revealeq { margin: 0 0.5em; opacity: 0.7; }
.rdiv-revealpts { font-weight: 700; font-size: 16px; opacity: 0.55; }
.rdiv-revealhit .rdiv-revealpts { color: var(--accent); opacity: 1; }
.rdiv-revealtotals { text-align: center; font-size: 14px; margin-top: 2px; }

@media (max-width: 720px) {
    /* The action column gives up its own column before the hand does. */
    .panel.rdiv-handpanel .rdiv-handrow { flex-wrap: wrap; }
    .rdiv-actions { flex: 1 1 100%; }
    .rdiv-side, .rdiv-mid { --rdiv-cw: 38px; }
    .card.rdiv-commissioncard, .card.rdiv-neCard { --cw: 48px; }
    .rdiv-siderow, .rdiv-midrow { column-gap: 8px; }
    .rdiv-reveal { gap: 18px; }
    .card.rdiv-revealcard { --cw: 48px; }
}
@media (max-width: 560px) {
    /* No room for a caption column: each caption sits above its strip. */
    .rdiv-trackrow { grid-template-columns: minmax(0, 1fr); row-gap: 4px; }
    /* The breakdown stays ONE row: three equal tiles sharing the width,
       the arithmetic a size down (and free to wrap its points under). */
    .rdiv-reveal { gap: 8px; flex-wrap: nowrap; }
    .rdiv-revealtile { flex: 1 1 0; min-width: 0; }
    .rdiv-revealcards { flex-wrap: wrap; justify-content: center; }
    .card.rdiv-revealcard { --cw: 44px; }
    .rdiv-revealsum { font-size: 12px; flex-wrap: wrap; white-space: normal; }
    .rdiv-revealeq { margin: 0 0.35em; }
    .rdiv-revealwho { font-size: 13px; }
}
@media (max-width: 379px) {
    /* The centered grid needs lead + gap + a full six-card run (at 38px
       cards: 64 + 8 + 248 = 320px of panel content, which a 380px screen
       still has). Below that, let each half take what is left and wrap,
       with the Commission column still pinned beside it. */
    .rdiv-siderow, .rdiv-midrow {
        grid-template-columns: var(--rdiv-lead) minmax(0, 1fr);
    }
    .rdiv-tableau { flex-wrap: wrap; }
    .rdiv-poolcards { width: auto; }
}

/* ==========================================================================
   Divided Ranks (dr-)

   Three surfaces: the reserve card (pinned above the table so it never moves,
   because it is in play from the deal to the showdown), the table stage (the
   four cards being divided, the two pairs on offer, or the card armed for a
   reserve swap), and the two knowledge trackers. The trackers are the game's
   signature feature and are styled as REFERENCE - quiet, small cards, out of
   the way of the decision.
   ========================================================================== */

/* ---------- tracker 1 and the reserve, in one row ----------
   Side by side where both fit (each wants about 320px), stacked in the same
   order where they do not. Stacked, they cost a desktop the 107px the app
   layout's 1280x720 frame did not have once the Unaccounted-for tracker below
   the hand was opened. The row's children drop their own vertical margins;
   the row carries the spacing. */
.dr-top {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: 10px 12px;
    margin: 0 0 10px;
}
.dr-top > * { flex: 1 1 320px; min-width: 0; }
/* Two classes, over .dr-reserve's and .panel's own margins. */
.dr-top > .dr-opp,
.dr-top > .dr-reserve { margin: 0; }
.dr-top > [hidden] { display: none; }
/* ---------- the reserve ---------- */
.dr-reserve {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 6px 0 10px;
    padding: 8px 12px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.09);
}
.dr-reservecard .card { --cw: 46px; }
.dr-reservetext { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.dr-reservetitle { font-weight: 700; letter-spacing: 0.03em; }
.dr-reservenote { font-size: 12px; opacity: 0.75; }

/* ---------- the table stage ---------- */
.dr-table { margin: 4px 0 10px; }
.dr-piles {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: flex-start;
}
.dr-pile {
    flex: 1 1 240px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 10px 12px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid rgba(255, 255, 255, 0.1);
}
.dr-pile .cardrow { min-height: 1px; }
.dr-pile .card { --cw: 62px; }
.dr-pilehead { font-weight: 700; font-size: 14px; }
.dr-pilenote { font-size: 12px; opacity: 0.72; }
.dr-pilebtn { align-self: flex-start; margin-top: 2px; }
/* The visible pair: the accent hue, matching the icon. Its whole meaning is
   "this is the side you can read". */
.dr-pile-up {
    border-color: rgba(255, 209, 102, 0.5);
    background: rgba(255, 209, 102, 0.09);
}
.dr-pile-up .dr-pilehead { color: var(--accent, #ffd166); }
/* The hidden pair, and the opponent's undivided four: cool and flat. */
.dr-pile-down, .dr-pile-hidden {
    border-color: rgba(120, 170, 220, 0.35);
    background: rgba(43, 93, 138, 0.14);
}
.dr-pile-trade { flex-basis: 100%; }

/* A card in the face-down pile: the splitter knows it perfectly well, so it
   stays a readable face - the fade is what says the OPPONENT does not. */
.card.dr-faded { opacity: 0.45; }
.card.dr-faded:hover { opacity: 0.75; }

/* Reserve status, one line per player: whether they have been to the reserve
   yet, and what they did there. */
.dr-reservestate { font-size: 12px; }
.dr-reservestate.dr-turn { font-weight: 700; color: var(--accent, #ffd166); }
.dr-reservestate.dr-waiting { opacity: 0.6; }
.dr-reservestate.dr-swapped { color: #9ad9ff; }
.dr-reservestate.dr-kept { opacity: 0.8; }

/* ---------- the hand: cards the opponent has already seen ---------- */
/* A ring rather than a wash: it must be legible at a glance without making the
   card itself harder to read, and it sits on cards that are also selectable. */
.card.dr-seen {
    box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.85), 0 2px 6px rgba(0, 0, 0, 0.35);
}

/* ---------- tracker 1: what you know about their hand ---------- */
/* It sits at the TOP of the play area, across the table from you, and the cards
   are near full size: this is the hand you are playing against, not a footnote,
   and reading it is most of the skill in the game. */
.panel.dr-opp { padding: 10px 12px; }
/* The opponent's hand is a hand, so it centers like #hand-cards does site-wide
   (this game's row is its own id, which the shared rule cannot reach). */
.dr-opp .cardrow { gap: 4px; justify-content: center; }
.dr-opp .card { --cw: 56px; }
.dr-note { font-size: 12px; opacity: 0.75; margin-top: 6px; line-height: 1.35; }

/* ---------- tracker 2: every card you cannot place ---------- */
.panel.dr-pool { padding: 8px 12px; }
.dr-pooltoggle {
    display: block;
    width: 100%;
    text-align: left;
    padding: 2px 0;
    font: inherit;
    font-weight: 700;
    color: inherit;
    background: none;
    border: 0;
    cursor: pointer;
}
.dr-pooltoggle:hover { color: var(--accent, #ffd166); }
.dr-pool .cardrow { gap: 3px; margin-top: 6px; }
.dr-pool .card { --cw: 30px; }

/* ---------- the showdown ---------- */
/* Full-size cards, so the two five-card hands are shown the way they are shown
   everywhere else rather than shrunk into a summary table - this is the moment
   the game is decided. Each player's two hands stack on one row. */
.result-table.dr-result { display: flex; flex-direction: column; gap: 10px; }
.dr-result .result-row {
    display: grid;
    grid-template-columns: minmax(72px, auto) 1fr 1fr;
    gap: 12px;
    align-items: start;
    padding: 8px 10px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.05);
}
.dr-result .result-row.dr-lost { background: rgba(212, 0, 0, 0.16); }
.dr-result .result-label { align-self: center; font-weight: 700; }
/* The weaker hand is the ONLY one the rules look at, so it is framed and the
   stronger one is not. */
.dr-result .result-cell.dr-decider {
    padding: 6px;
    border-radius: 8px;
    border: 1px solid rgba(255, 209, 102, 0.55);
    background: rgba(255, 209, 102, 0.08);
}
.dr-result .cardrow .card { --cw: 52px; }
.dr-result .result-handname { font-size: 12px; margin-top: 4px; }

.dr-error { color: #ffb3b3; font-size: 13px; margin-top: 4px; }

@media (max-width: 720px) {
    .dr-pile { flex-basis: 100%; }
    .dr-result .result-row { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
    .dr-pile .card { --cw: 50px; }
    .dr-result .cardrow .card { --cw: 40px; }
    .dr-reservecard .card { --cw: 38px; }
    .dr-opp .card { --cw: 42px; }
}

/* ---------- Regicide ----------
 *
 * Prefix policy: every Regicide-specific class is `rg-` (matching `cascade-`,
 * `dracula-`, `rr-`). Shared classes are reused as-is and get no rules here:
 * hand cards select with `.card.sel` and arm with `.clickable`, the pile
 * counts are `.pile-chip`s, the panels sit on `.panel`'s palette variables,
 * the buttons are `.btn`, the result panel is the shared `.winner-banner`
 * family, and the log is the shared `.logline`.
 */

/* The current royal: its card beside a stat column. The card is a size up
   from the default so the thing you are fighting reads as the centerpiece. */
.rg-enemy-row { display: flex; gap: 16px; align-items: flex-start; }
.rg-enemy-row > .card { --cw: 88px; flex: none; }
.rg-enemy-stats { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.rg-enemy-name { margin: 0 0 2px; }
.rg-statline { font-size: 14px; }
.rg-immune { opacity: 0.75; }

/* Damage dealt so far, as a filling bar: red creeping over the dark track. */
.rg-healthbar {
    width: min(320px, 100%);
    height: 10px;
    border-radius: 5px;
    background: rgba(0, 0, 0, 0.35);
    overflow: hidden;
}
.rg-healthfill {
    height: 100%;
    border-radius: 5px;
    background: #d45050;
    transition: width 300ms ease;
}

/* The strip of cards played against this enemy. Always rendered, with a
   one-card min-height, so the hand below never shifts when a play lands. */
.rg-played-title {
    font-size: 11px;
    opacity: 0.65;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    margin-top: 10px;
}
.rg-played { min-height: 64px; margin-top: 4px; }
.rg-played-gap { width: 10px; flex: none; }

/* The Jesters beside the table: spent ones gray out and go inert. */
.rg-jester-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.rg-jester-used { filter: grayscale(1) brightness(0.55); }
.rg-jester-note { font-size: 13px; opacity: 0.8; max-width: 420px; }

/* The hand row: cards left, the action column (button + guidance) right. */
.rg-handrow { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
.rg-handrow #hand-cards { flex: 1; min-width: 0; }
.rg-actions {
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex: none;
    width: 220px;
}
.rg-action-note { font-size: 13px; opacity: 0.8; }

.rg-result-line { margin: 6px 0 10px; }
.rg-note { font-size: 13px; opacity: 0.75; }

/* The pile chips are real buttons (clicking one opens the pile viewer), so
   undo the browser's button font and cursor; the chip look itself comes from
   the shared .pile-chip rule. */
.rg-pilebtn { font: inherit; font-size: 13px; color: inherit; cursor: pointer; }
.rg-pilebtn:hover, .rg-pilebtn:focus-visible { opacity: 1; border-color: var(--accent); }

/* --- pile viewer ---------------------------------------------------------
   Modal overlay showing what a clicked pile holds. Public information only,
   so the backdrop stays translucent. The z-index recipe is the Dracula
   snapshot viewer's: 500 keeps it UNDER the pass-and-play handoff gate (600)
   and under the card-flight layers (900/1000). */
.rg-pile {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: rgba(0, 0, 0, 0.6);
}
.rg-pile-card {
    background: var(--felt-dark);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
    padding: 18px 22px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-width: min(640px, calc(100vw - 32px));
    max-height: calc(100vh - 48px);
    overflow-y: auto;
}
.rg-pile-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    font-weight: 700;
}
.rg-pile-label {
    font-size: 11px;
    opacity: 0.65;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    margin-top: 4px;
}
.rg-pile-card .cardrow { gap: 4px; }
.rg-pile-card .card.mini { --cw: 40px; }
.rg-pile-note { font-size: 13px; opacity: 0.75; max-width: 460px; }

@media (max-width: 560px) {
    .rg-enemy-row > .card { --cw: 68px; }
    .rg-played { min-height: 48px; }
    .rg-actions { width: 100%; }
    .rg-pile-card .card.mini { --cw: 32px; }
}
/* ==========================================================================
   Regicide: Crown Duels
   (src/crownduels/, pages/crownduels.mjs)
   ==========================================================================
   Two-player zone-plotting duel. Every shared class (.card, .cardrow,
   .panel, .pile-chip, .logline, .result-actions, .winner-banner, .btn, .sel,
   .card.simple.back) is reused as-is; only what is genuinely this game's own
   lives here, all of it prefixed `rcd-`. The board is two COURTS (opponent
   above, viewer below) around a fight panel that itemizes the round's
   strength/block arithmetic. */
.rcd-court {
    padding: 8px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    margin-bottom: 10px;
}
/* Centered on the board axis, like the deck chips and the hand: the court
   panels run the full width of the play column, so left-aligned contents left
   the whole board hugging one edge. Every row below centers for the same
   reason; none of them changes a container's size, so nothing re-centers
   mid-game. */
.rcd-courthead {
    display: flex;
    gap: 14px;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    margin-bottom: 8px;
}
.rcd-courtname { font-weight: 700; font-size: 14px; }
.rcd-handcount { opacity: 0.8; font-size: 13px; font-variant-numeric: tabular-nums; }
/* ---------- the royal ---------- */
.rcd-royal {
    display: flex;
    gap: 8px;
    align-items: center;
    padding: 3px 6px;
    border-radius: 8px;
}
.rcd-royal.rcd-corrupt {
    background: rgba(140, 60, 170, 0.16);
    outline: 1px solid rgba(170, 90, 200, 0.55);
}
.card.rcd-royalcard { --cw: 46px; }
/* The corrupt royal turns 180 degrees, as the rulebook suggests. `rotate`,
   never `transform`, so a hover lift or pop-in cannot turn it back upright
   (ui-conventions, "A sideways card turns with rotate"). */
.rcd-corrupt .card.rcd-royalcard { rotate: 180deg; }
.card.rcd-royalcard.rcd-fallen { filter: grayscale(1) brightness(0.6); }
.rcd-royalinfo { display: flex; flex-direction: column; gap: 3px; }
.rcd-royalname { font-size: 12px; font-weight: 700; }
/* HP pips: one per vitality point; `hit` = an HP card turned sideways. */
.rcd-pips { display: flex; gap: 3px; }
.rcd-pip {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    border: 1px solid var(--panel-border);
    background: var(--green, #2e8b57);
}
.rcd-pip.hit { background: rgba(255, 255, 255, 0.08); border-color: var(--panel-border); }
.rcd-badges { display: flex; gap: 5px; }
.rcd-badge {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.4px;
    padding: 1px 6px;
    border-radius: 999px;
    background: rgba(170, 90, 200, 0.3);
    border: 1px solid rgba(170, 90, 200, 0.6);
}
.rcd-badge.rcd-jester {
    background: rgba(255, 209, 102, 0.18);
    border-color: rgba(255, 209, 102, 0.55);
}
/* ---------- the four zones ---------- */
.rcd-zones {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 8px;
}
.rcd-zone {
    display: block;
    width: 100%;
    text-align: left;
    padding: 6px 8px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.03);
    color: inherit;
    font: inherit;
}
button.rcd-zone { cursor: pointer; }
.rcd-zone.lit {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
    background: rgba(255, 255, 255, 0.06);
}
.rcd-zonetitle {
    font-size: 11px;
    font-weight: 700;
    opacity: 0.8;
    margin-bottom: 4px;
    /* .rcd-zone sets text-align: left for the button reset, so the heading and
       the tip under it opt back out. */
    text-align: center;
}
/* Which suit does what, in the zone it does it in. Small and dim: a reminder
   to glance at, not a line to read every turn. The GLYPH is set bigger than
   the words and the red suits are tinted, because at 10px a club and a spade
   are the same dark blob and telling them apart is the only job this line has. */
.rcd-zonetip {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: center;
    gap: 2px 4px;
    font-size: 10px;
    line-height: 1.35;
    opacity: 0.72;
    text-align: center;
    margin-bottom: 5px;
}
.rcd-tipsuit { font-size: 13px; line-height: 1; }
.rcd-tipsuit.red { color: var(--red); }
.rcd-tipsuit.red.diamond { color: var(--red-diamond); }
.rcd-tipsuit.club { color: var(--blue-club); }
.rcd-tipsep { opacity: 0.35; }
.rcd-zonecards {
    --cw: 42px;
    min-height: calc(42px * 89 / 64);
    justify-content: center;
}
/* A face-down card whose identity the viewer knows: a dimmed, tilted face. */
.card.rcd-facedown { opacity: 0.62; }
/* Scoped under its row so a flight ghost (a clone in <body>) is not tilted. */
.rcd-zonecards .card.rcd-facedown { rotate: 4deg; }
/* ---------- the fight panel ---------- */
.rcd-fight {
    padding: 8px 12px;
    border: 1px solid var(--accent);
    border-radius: 10px;
    background: var(--panel);
    margin-bottom: 10px;
}
.rcd-fighttitle {
    font-weight: 700;
    font-size: 13px;
    margin-bottom: 6px;
    text-align: center;
}
.rcd-fightcols { display: flex; gap: 28px; flex-wrap: wrap; justify-content: center; }
/* Held to the width the arithmetic needs, NOT to half the panel: the rows are
   label-left/value-right, so a column stretched to 360px put a number six
   centimeters from the word it belongs to and the two stopped reading as a
   pair. */
.rcd-fightcol {
    flex: 0 1 250px;
    max-width: 250px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: 12px;
}
.rcd-fightname { font-weight: 700; font-size: 13px; margin-bottom: 2px; text-align: center; }
.rcd-fightline { display: flex; justify-content: space-between; gap: 10px; }
.rcd-fightlabel { opacity: 0.75; }
.rcd-fightvalue { font-variant-numeric: tabular-nums; }
.rcd-fightline.rcd-fighttotal { font-weight: 700; }
.rcd-fightline.rcd-fighthit .rcd-fightvalue { color: var(--red); font-weight: 700; }
.rcd-fightline.rcd-fightmiss { opacity: 0.65; }
.rcd-fightline.rcd-spellnote { color: var(--accent); }
/* ---------- the fight replay strip ----------
   The cards that produced the arithmetic, kept beside it. They are copies of
   the report's snapshots, so they carry no data-card-id (see replayCardEl) and
   the card animator never sees them. */
.rcd-fightstrip {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin-bottom: 6px;
}
.rcd-striprow { display: flex; align-items: flex-start; gap: 6px; justify-content: center; }
.rcd-striplabel {
    font-size: 10px;
    opacity: 0.6;
    min-width: 44px;
    text-align: right;
    padding-top: 10px;
    flex: 0 0 auto;
}
.rcd-stripcards { --cw: 34px; gap: 5px; flex-wrap: wrap; justify-content: center; }
.rcd-stripslot { display: flex; flex-direction: column; align-items: center; gap: 1px; }
/* What one heart added this round, doubled or not. */
.rcd-stripadd {
    font-size: 9px;
    font-weight: 700;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
/* A heart that fired: marked for as long as the panel stands, so an
   interrupted pulse still leaves the explanation on screen. */
.card.rcd-replaycard.rcd-ignited {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}
/* A card a Shatter destroyed: it is in the discard pile now. */
.card.rcd-replaycard.rcd-broken { opacity: 0.45; filter: grayscale(0.7); }
/* ---------- the spells that resolved ---------- */
.rcd-spellresults {
    margin-top: 8px;
    padding-top: 6px;
    border-top: 1px solid var(--panel-border);
    display: flex;
    flex-direction: column;
    gap: 4px;
    font-size: 12px;
}
.rcd-spelltitle { font-weight: 700; font-size: 12px; text-align: center; opacity: 0.8; }
.rcd-spellrow {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: center;
    border-radius: 6px;
    padding: 2px 4px;
}
.rcd-spelllabel { font-weight: 700; color: var(--accent); }
.rcd-spelltext { opacity: 0.85; }
/* ---------- the one-shot pulse (see pulse() in def.ts) ---------- */
.rcd-firing { animation: rcd-fire 900ms ease-out; }
@keyframes rcd-fire {
    from { box-shadow: 0 0 0 0 rgba(255, 209, 102, 0.9); }
    to { box-shadow: 0 0 0 12px rgba(255, 209, 102, 0); }
}
body.motion-off .rcd-firing { animation: none; }
/* ---------- hand panel and actions ---------- */
.rcd-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    flex-wrap: wrap;
    justify-content: center;
}
/* Sized to the cards, not flex-grown: growing gave the row all the width the
   action column did not take, so a three-card hand centered inside that box
   and sat visibly left of the panel's middle. Shrink-to-fit centers the cards
   and the notes together. */
.rcd-handrow #hand-cards { flex: 0 1 auto; }
.rcd-actions {
    flex: 0 1 220px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}
.rcd-actionnote { font-size: 12px; opacity: 0.85; text-align: center; }
.logline.rcd-logfight { color: var(--accent); }

/* ==== Agent Hunter. Everything shared (.panel, .cardrow, .card, .btn,
   .logline, .result-actions, .winner-banner, .sel, .lit, .clickable,
   .card.simple.back, .card.empty) is reused as-is; only what is genuinely
   this game's own lives here, all of it prefixed `ah-`. The board is two
   SIDES (opponent above, viewer below): a head line with the public rank
   strip, then the three safehouse slots carrying their pinned clues. ==== */
.ah-side {
    padding: 8px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.03);
    margin-bottom: 10px;
}
.ah-sidehead {
    display: flex;
    gap: 12px;
    align-items: baseline;
    flex-wrap: wrap;
    justify-content: center;
    margin-bottom: 6px;
    font-size: 13px;
}
.ah-sidename { font-weight: 700; font-size: 14px; }
/* The cards this player has attacked with FROM HAND, face up. No card
   backs for the hidden rest of the hand: they only took up space. */
.ah-played {
    display: flex;
    gap: 4px;
    align-items: center;
    flex-wrap: wrap;
    justify-content: center;
    margin-bottom: 6px;
    min-height: 18px;
}
.ah-playedlabel { font-size: 11px; opacity: 0.7; }
.card.ah-handmini { --cw: 26px; }
/* The public rank strip: which of this player's ten numbers are face up. */
.ah-rankstrip { display: flex; gap: 2px; }
.ah-rankcell {
    min-width: 18px;
    padding: 1px 2px;
    text-align: center;
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    border: 1px solid var(--panel-border);
    border-radius: 4px;
    opacity: 0.9;
}
.ah-rankcell.ah-shown {
    opacity: 0.45;
    text-decoration: line-through;
    background: rgba(255, 255, 255, 0.06);
}
/* ---------- the three safehouse slots ---------- */
.ah-slotrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    flex-wrap: wrap;
    justify-content: center;
}
.ah-slot {
    display: flex;
    flex-direction: column;
    gap: 4px;
    align-items: center;
    padding: 6px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    min-width: 96px;
}
.ah-slot-captured { opacity: 0.75; background: rgba(0, 0, 0, 0.18); }
.ah-slotcap {
    font-size: 11px;
    opacity: 0.8;
    display: flex;
    gap: 6px;
    align-items: baseline;
}
.ah-badge { color: var(--accent); font-weight: 700; cursor: help; }
.ah-slotcardbox { display: flex; gap: 6px; align-items: center; }
.card.ah-slotcard { --cw: 56px; }
/* Tilts use `rotate`, never `transform`, and are scoped under the slot box so
   a flight ghost (a clone in <body>) matches nothing here (ui-conventions, "A
   sideways card turns with rotate"). */
.card.ah-slotcard.ah-fallen { filter: grayscale(0.7); }
.ah-slotcardbox .card.ah-slotcard.ah-fallen { rotate: 6deg; }
/* The exact-match card pinned on a fallen slot. */
.card.ah-capturepin { --cw: 40px; }
.ah-slotcardbox .card.ah-capturepin { rotate: -8deg; }
.ah-capturenote {
    font-size: 11px;
    font-weight: 700;
    opacity: 0.85;
    cursor: help;
}
.ah-slotstate {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    opacity: 0.75;
}
/* ---------- pinned clues (the house rule: everything stays visible) ---- */
.ah-cluerow {
    display: flex;
    gap: 5px;
    flex-wrap: wrap;
    justify-content: center;
    max-width: 132px;
}
.ah-clue {
    display: inline-flex;
    gap: 2px;
    align-items: center;
    cursor: help;
}
.card.ah-cluecard { --cw: 26px; }
.ah-cluetext {
    font-size: 11px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    padding: 1px 3px;
    border: 1px solid var(--panel-border);
    border-radius: 4px;
}
.ah-clue.ah-stale { opacity: 0.4; }
.ah-clue.ah-stale .ah-cluetext { text-decoration: line-through; }
/* A switched slot hops once, so a blind swap reads even though both cards
   involved stay face down. Rendering is idempotent repaints: the class is
   added by the animation pass right after the switch renders and is gone
   on the next repaint, so the keyframes play exactly once. */
@keyframes ah-swap {
    0% { transform: translateY(0); }
    35% { transform: translateY(-12px) rotate(-5deg); }
    70% { transform: translateY(2px) rotate(2deg); }
    100% { transform: translateY(0); }
}
.ah-slot.ah-swapped .ah-slotcard { animation: ah-swap 500ms ease; }
/* ---------- what a tap does, spelled out on each slot ---------- */
/* Switch targets reuse the shared .lit ring but recolored, so "switch
   here" (blue) never looks like "attack here" (the gold .lit accent). */
.card.ah-slotcard.ah-switch-target.lit { outline-color: #4aa3ff; }
.ah-slotaction {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    padding: 1px 7px;
    border-radius: 999px;
    white-space: nowrap;
}
.ah-action-attack { color: #1a1a1a; background: var(--accent); }
.ah-action-switch { color: #fff; background: #2f7fd1; }
.ah-action-armed {
    color: var(--accent);
    border: 1px solid var(--accent);
    background: transparent;
}
.btn.ah-fromhere { font-size: 11px; padding: 2px 8px; }
/* Public (opponent-seen) hand cards wear a small blue dot in the corner. */
.card.ah-public { position: relative; }
.card.ah-public::after {
    content: '';
    position: absolute;
    top: 3px;
    right: 3px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: #4aa3ff;
    box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.45);
}
/* ---------- the hand and the action column ---------- */
.panel.ah-handpanel .ah-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    justify-content: center;
}
.panel.ah-handpanel #hand-cards { flex: 0 1 auto; }
.ah-actions {
    flex: 0 0 244px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: stretch;
}
.ah-actionnote { font-size: 13px; opacity: 0.9; }
@media (max-width: 720px) {
    /* The action column gives up its own column before the hand does. */
    .panel.ah-handpanel .ah-handrow { flex-wrap: wrap; }
    .ah-actions { flex: 1 1 100%; }
    .ah-rankstrip { flex-basis: 100%; justify-content: center; }
    .card.ah-slotcard { --cw: 48px; }
    .ah-slot { min-width: 84px; }
    .ah-slotrow { gap: 8px; }
}
/* ==========================================================================
   The Emissary
   (src/emissary/, pages/emissary.mjs)

   Prefix policy: every Emissary-specific class is `em-` (matching `rg-`,
   `ah-`, `cascade-`). Shared classes are reused as-is and get no rules here:
   the deck and discard counts are `.pile-chip`s, the panels sit on `.panel`'s
   palette variables, hand cards select with `.card.sel` and arm with
   `.clickable`, the hint flash is the shared `.card.hinted`, the buttons are
   `.btn`, the result panel opens on the shared `.winner-banner`, the log is the
   shared `.logline`, and a settled visit's picture is the shared `.logpic`.

   HEIGHT IS THE BUDGET. The whole board (kingdoms, visit, hand, court) is
   meant to fit a 1280x800 window without scrolling: the court used to end 350px
   below the fold. So no panel carries a title the status line already says, no
   note a rules section already says, and every panel is painted in every phase
   at one height, so nothing below it moves.
   ========================================================================== */
.em-title { margin: 0 0 8px; font-size: 15px; }
/* The setup screen's note on the version this follows. */
.em-note { font-size: 12.5px; opacity: 0.75; max-width: 70ch; margin: 8px 0 0; }
.em-label {
    font-size: 10.5px;
    opacity: 0.65;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    white-space: nowrap;
}
/* A suit's colored pip on a dark plate. The shared four-color palette is tuned
   for card labels and is deliberately pale - --blue-club (#d6e0eb) against
   --ink (#f2efe9) is a 5% lightness difference, so a bare club pip on felt was
   indistinguishable from a spade one. The plate gives all four hues something to
   read against without inventing a fifth palette. */
.em-pip-plate {
    font-weight: 700;
    line-height: 1;
    padding: 1px 5px;
    border-radius: 5px;
    background: rgba(0, 0, 0, 0.38);
    cursor: help;
}
.em-pip-plate.red { color: var(--red); }
.em-pip-plate.red.diamond { color: var(--red-diamond); }
.em-pip-plate.club { color: var(--blue-club); }
/* ---------- the line of eight kingdoms ---------- */
/* A horizontal strip that scrolls rather than wraps: the LINE is the whole
   point (a kingdom's place in it is the number it demands), so it must never
   reflow into two rows where the 5th sits under the 1st. Not centered, because
   it scrolls (ui-conventions: a scrolling strip must not center); its cells
   share the width, so on any screen that fits it there is no slack to place. */
.panel.em-kingdoms { padding: 8px 10px; margin-bottom: 10px; }
.em-row {
    display: flex;
    gap: 8px;
    align-items: stretch;
    overflow-x: auto;
}
.em-kingdom {
    flex: 1 1 0;
    min-width: 76px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 5px 4px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.16);
}
.em-kingdom .card { --cw: 44px; }
.em-k-need {
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
/* The pip, and beside it the state word or the hint's odds. One line, always
   present, so asking for advice or pledging a kingdom grows nothing. */
.em-k-state {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    min-height: 19px;
    font-size: 11.5px;
    white-space: nowrap;
}
.em-k-pip { font-size: 15px; }
.em-k-word { opacity: 0.8; }
/* A pledged ruler is a supporter now, counted in the court: the kingdom keeps
   only their label, at the card's height. */
.em-k-gone {
    display: flex;
    align-items: center;
    justify-content: center;
    height: calc(44px * 89 / 64);
    font-size: 15px;
}
.em-k-odds {
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--accent);
    cursor: help;
}
/* The kingdom you are in now. */
.em-kingdom.em-here {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.12);
}
/* A kingdom you may pick (a visit, or a politics swap): its card is the
   control, so the whole cell says so. */
.em-kingdom.em-pickable {
    border-color: rgba(255, 209, 102, 0.55);
    border-style: dashed;
}
/* A pledged kingdom drops its number and its topic, so its two lines sit in
   the middle of the cell. Every cell keeps an open kingdom's height (the need
   line, the card, the state line, and the gaps), so the row is the same height
   when all eight are pledged. */
.em-kingdom { min-height: calc(44px * 89 / 64 + 52px); }
.em-kingdom.em-pledged { border-color: rgba(120, 220, 150, 0.5); justify-content: center; }
.em-kingdom.em-pledged .em-k-word { color: #8fe0aa; }
.em-kingdom.em-failed { opacity: 0.55; }
.em-kingdom.em-failed .em-k-word { color: var(--red); }
/* The hint's recommendation, on the same gold the rest of the app uses for
   "this is the suggested one". */
.em-kingdom.em-best { box-shadow: 0 0 0 2px var(--accent) inset; }
/* ---------- the visit, beside the deck ----------
 *
 * Two panels in one row. The visit takes the slack; the deck grid is its own
 * fixed width (a 4x8 layout that never reflows). The play area is at most 852px
 * on a 1280px window (a 1200px shell, less padding, less the 300px log column)
 * and the pair needs about 600, so they share a row down to a ~640px play area
 * and stack below that, which is right on a phone.
 */
.em-visitrow {
    /* The deck grid's width, fixed so the visit panel can mirror it (below). */
    --em-deckw: 250px;
    display: flex;
    gap: 14px;
    align-items: stretch;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.em-visitrow > .panel { margin-bottom: 0; }
.em-visitrow > .em-debate { flex: 1 1 300px; min-width: 0; }
.em-visitrow > .em-deckgrid { flex: 0 0 var(--em-deckw); padding: 10px 12px; }
/* ---------- the visit ---------- */
/* A column centered top to bottom: the deck grid beside it sets the row's
   height, and the visit sits in the middle of it rather than at the top.

   CENTERED ON THE HAND, not on its own box. The visit is what the player is
   watching, and centered in a panel that stops short of the deck grid it sat
   left of the hand and the court below, which are centered on the whole board.
   So the left padding mirrors what is on the right of the visit (the deck grid
   and the gap), which puts the content's center on the row's center:
   padding-left = deck width + gap + this panel's right padding, here 278px.
   `100% - 650px` (a percentage padding reads the ROW's width) gives that space
   back as the row narrows, so the content keeps the 370px its one line of
   trick, tally and button needs; on a narrow window it is centered in its own
   panel instead, and on a phone, where the deck grid wraps below, the padding
   is the ordinary 14px. */
.panel.em-debate {
    padding: 10px 14px;
    padding-left: clamp(14px, calc(100% - 650px), calc(var(--em-deckw) + 28px));
    display: flex;
    flex-direction: column;
    justify-content: center;
}
.em-visit-name {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: nowrap;
    white-space: nowrap;
    gap: 4px 10px;
}
.em-trumpchip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
}
.em-trump-pip { font-size: 15px; }
/* The settled visit kept on show between kingdoms: readable, but quieter
   than a visit in play. */
.panel.em-debate.em-past .em-visit-body { opacity: 0.8; }
/* The trick, then the tally with the turn's button under it, centered. The
   button's slot stands empty when there is no button, so neither column changes
   size between phases. */
.em-visit-body {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 10px 20px;
}
.em-visit-side {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}
/* The table: the ruler's card waiting for an answer, and beside it the last
   debate settled. Every slot is always painted, empty or full, so the row never
   changes size. The pair is read-only and 40px, so it is on the simplified atlas
   (ui-conventions, "Small cards use the simplified sprite"). */
.em-trick { display: flex; align-items: flex-end; gap: 12px; }
.em-trick .em-label { letter-spacing: 0.2px; }
.em-trick-side {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.card.em-trickcard { --cw: 60px; }
.em-lastpair { display: flex; gap: 4px; }
.card.em-lastcard { --cw: 40px; }
/* The last debate's winner, ringed, as in the log's pictures. */
.card.em-trick-won { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The tally: won-of-target big, and the debates still to come as pips. This
   is the arithmetic the game is played on, so it gets the biggest type on the
   page. A fixed width, so the pips coming and going move nothing. */
.em-tally {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    min-width: 150px;
}
/* A fixed height, so the dash shown before the first visit and the count shown
   after it make the same line. */
.em-tally-line { display: flex; align-items: baseline; gap: 6px; height: 28px; }
.em-tally-num {
    font-size: 26px;
    font-weight: 800;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}
.em-tally-num.em-tally-exact { color: var(--accent); }
.em-tally-num.em-tally-over { color: var(--red); text-decoration: line-through; }
.em-tally-of { font-size: 14px; white-space: nowrap; }
/* One pip per debate left: FILLED for one that must be won, HOLLOW for one
   that must be lost. Short (more wins owed than debates left) is dashed red;
   over the target turns every hollow pip red. Fill and outline carry the
   meaning, so none of it rests on color alone. */
.em-track {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 4px;
    max-width: 190px;
    /* One fixed line, so the pips coming and going, or the words that replace
       them between kingdoms, move nothing. A long hand (military can reach a
       dozen cards) shrinks the pips rather than wrapping them. */
    height: 16px;
    cursor: help;
}
.em-track-pip {
    flex: 0 1 12px;
    min-width: 5px;
    aspect-ratio: 1;
    border-radius: 50%;
    box-sizing: border-box;
}
.em-track-win { background: #8fe0aa; border: 2px solid #8fe0aa; }
.em-track-lose { border: 2px solid rgba(242, 239, 233, 0.5); }
.em-track-lose.em-track-over { border-color: var(--red); }
.em-track-short { border: 2px dashed var(--red); }
.em-track-done { font-size: 12.5px; line-height: 16px; opacity: 0.85; cursor: default; }
.em-track-done.em-track-pledged { color: #8fe0aa; }
/* The turn's button, under the tally. The slot keeps its size whether or not a
   button is in it, so "Let the ruler speak" going away when the ruler speaks
   moves nothing. */
.em-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 170px;
    /* Taller than the primary button at every breakpoint, touch included. */
    min-height: 40px;
}
/* ---------- the court: one compact slot per ability suit ----------
 *
 * Small, and BELOW the hand: an advisor is spent once or twice in a whole
 * kingdom, so it does not earn space above the cards you touch every debate.
 * One slot per suit, the Jack beside its text - supporters are counted as
 * crowns in the matching slot rather than getting cards of their own.
 */
.panel.em-court { padding: 8px 12px 10px; }
.panel.em-court .em-title { margin-bottom: 6px; }
.em-court-row {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: center;
}
.em-ability {
    flex: 0 1 216px;
    min-width: 0;
    box-sizing: border-box;
    display: flex;
    align-items: flex-start;
    gap: 7px;
    padding: 5px 7px;
    border: 1px solid var(--panel-border);
    border-radius: 9px;
    background: rgba(0, 0, 0, 0.16);
}
.em-ability .card.em-jack { --cw: 34px; flex: none; }
.em-ability-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 1 1 auto;
}
.em-ability-head { display: flex; align-items: center; gap: 6px; }
.em-ability-name {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.4px;
}
/* Two lines at most, so every slot has one height whatever its ability does. */
.em-ability-effect {
    font-size: 10.5px;
    line-height: 1.25;
    opacity: 0.78;
    height: calc(2 * 1.25em);
    overflow: hidden;
}
.em-ability-act { min-height: 22px; display: flex; align-items: center; }
/* A touch screen grows every small button to 36px (the coarse-pointer block in
   src/duelpoker/style.css), so the slot reserves that, or the court would grow
   a line each time the ability window opened. */
@media (pointer: coarse) {
    .em-ability-act { min-height: 36px; }
}
.em-ability .btn.small { padding: 2px 8px; font-size: 11.5px; }
/* The suit's charges as pips: a dot for the advisor, a crown per supporter
   favor, hollow once spent. SHAPE carries which kind of charge it is and FILL
   carries whether it is still there, so neither meaning rests on color - the
   words live on the row's title/aria-label. */
.em-ability-charges {
    display: flex;
    gap: 3px;
    align-items: center;
    line-height: 1;
    cursor: help;
}
.em-pip { font-size: 12px; color: var(--accent); }
.em-pip.em-pip-off { color: var(--ink); opacity: 0.35; }
/* The Jack is face down for this kingdom. The slot itself only grays out when
   nothing of that suit is left at all - a spent Jack with a favor behind it is
   still live. */
.card.em-facedown { filter: grayscale(1) brightness(0.55); }
.em-ability.em-spent { opacity: 0.5; }
/* Face up, but dead in this position - no trump to discard, no club to draw on,
   nowhere left to swap. Dimmed less than a spent one, because it comes back the
   moment the hand changes. */
.em-ability.em-idle { opacity: 0.7; }
.em-ability.em-idle .em-ability-effect { font-style: italic; }
.em-ability.em-best { box-shadow: 0 0 0 2px var(--accent) inset; }
/* ---------- the hand ---------- */
.panel.em-handpanel { padding: 8px 16px 10px; margin-bottom: 10px; }
/* The row every decision is made on, so the faces are full size rather than
   the `mini` used by the reference strips. */
.card.em-handcard { --cw: 76px; }
/* An answer that cannot be played, because the topic must be followed. Dimmed
   rather than removed: seeing what you are NOT allowed to answer with is half
   of learning the game. */
.card.em-dead { opacity: 0.35; filter: saturate(0.4); }
.card.em-dead:hover { transform: none; }
/* A legal answer that WINS. The whole game is deciding whether to take this
   debate, so which cards would take it must not need working out. The inset
   ring composes with the .sel/.hinted outlines, which sit outside the card. */
.card.em-wins { box-shadow: inset 0 0 0 3px rgba(143, 224, 170, 0.95); }
/* ---------- the deck: its counts, and what it still holds ----------
 *
 * The 32-card deck as a fixed suit-by-rank grid, seen cards blanked in place -
 * so the grid's height never changes, and a cell's POSITION still identifies its
 * card once the face has gone.
 */
.em-deckgrid { overflow-x: auto; }
.em-deck-head {
    display: flex;
    justify-content: center;
    gap: 6px;
    margin-bottom: 4px;
}
.em-deck-head .pile-chip { padding: 2px 8px; font-size: 12px; white-space: nowrap; }
.em-deck-head .pile-chip::before { width: 9px; height: 12px; }
.em-deck-grid {
    display: grid;
    /* A label column, then Ace..8. */
    grid-template-columns: auto repeat(8, auto);
    gap: 2px;
    justify-content: center;
    align-items: center;
    width: max-content;
    margin: 0 auto;
}
.em-deck-grid .card.em-deckcard { --cw: 22px; }
.em-deck-rank {
    font-size: 10.5px;
    font-weight: 700;
    opacity: 0.7;
    text-align: center;
    font-variant-numeric: tabular-nums;
}
.em-deck-corner { width: 26px; }
/* Every label is padded as the lit trump one is, so lighting a row (or a
   politics swap moving the light) changes no column's width. */
.em-deck-suit {
    display: flex;
    align-items: baseline;
    gap: 2px;
    min-width: 30px;
    padding: 1px 4px;
    border-radius: 5px;
    cursor: help;
}
.em-deck-pip { font-size: 14px; font-weight: 700; }
.em-deck-suit.red .em-deck-pip { color: var(--red); }
.em-deck-suit.red.diamond .em-deck-pip { color: var(--red-diamond); }
.em-deck-suit.club .em-deck-pip { color: var(--blue-club); }
.em-deck-count {
    font-size: 12px;
    font-weight: 700;
    opacity: 0.8;
    font-variant-numeric: tabular-nums;
}
/* The trump suit's row is the one that decides debates, so its label is lit. */
.em-deck-suit.em-deck-trump { background: rgba(255, 209, 102, 0.16); }
/* A card already seen: the cell keeps its place, and its outline says a card
   belongs here, but the face is gone. */
.em-deck-grid .card.em-deck-gone {
    background: rgba(0, 0, 0, 0.22);
    border: 1px dashed rgba(255, 255, 255, 0.16);
    box-shadow: none;
}
/* ---------- the log: one line per debate, drawn as its two cards ---------- */
#log-entries .logline.em-logtrick {
    display: flex;
    align-items: center;
    gap: 4px;
}
.em-logtrick-n {
    min-width: 5.2em;
    font-variant-numeric: tabular-nums;
    opacity: 0.85;
}
.em-logtrick .card.logpic-card { --cw: 22px; flex: none; }
.em-logtrick .card.logpic-won { outline: 2px solid var(--accent); outline-offset: 0; }
/* The card that lost the debate, faded a little: the ring says who won, and
   this says it from the other side without hiding the card. The shared
   `logpic-lost` (0.45) is stronger than wanted here, so the visit's pictures
   use the same 0.6. */
.em-logtrick .card.em-loglost,
.logpic.em-logpic .card.logpic-lost { opacity: 0.6; }
/* The court card used, on its own line between the debates. */
.em-logcourt-note { font-size: 11.5px; opacity: 0.75; }
/* The visit's two pictures (debates, then the court used) share a label width,
   so their strips start at the same x. */
.logpic.em-logpic .logpic-name { min-width: 3.2em; }
.logpic.em-logpic-court { margin-top: 2px; }
/* ---------- the finished tour ---------- */
.em-result-line { margin: 6px 0 10px; }
.em-result-table { display: flex; flex-direction: column; gap: 2px; margin: 10px 0; }
.em-result-row {
    display: grid;
    grid-template-columns: 46px 46px 78px 96px 1fr;
    gap: 8px;
    align-items: center;
    padding: 4px 8px;
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.16);
    font-size: 13px;
}
.em-result-row.em-pledged { background: rgba(120, 220, 150, 0.14); }
.em-result-row.em-failed { background: rgba(255, 156, 156, 0.14); }
.em-result-num { font-weight: 700; font-variant-numeric: tabular-nums; }
.em-result-need, .em-result-got, .em-result-favor { opacity: 0.85; }
@media (max-width: 720px) {
    /* Two court slots a row, and the deck grid on a row of its own once the
       visit row has wrapped. */
    .em-ability { flex: 1 1 calc(50% - 4px); }
    .em-ability-effect { height: calc(3 * 1.25em); }
    .em-visitrow > .em-deckgrid { flex: 1 1 100%; }
}
/* Reserve the status line's tallest shape: one line on a wide screen, where
   every status fits; two on a phone, where most of them wrap. */
.status.em-status { line-height: 26px; }
@media (max-width: 560px) {
    .status.em-status { font-size: 16px; line-height: 22px; min-height: 44px; }
    /* The kingdom's name and its trump on two lines, always, so a long name
       or the first visit's shorter title moves nothing. */
    .em-visit-name { flex-direction: column; height: 42px; gap: 2px; }
    /* One step down again, so the hand keeps to two rows and the visit's three
       parts to two lines. */
    .card.em-trickcard { --cw: 50px; }
    .card.em-lastcard { --cw: 36px; }
    .card.em-handcard { --cw: 58px; }
    .em-tally-num { font-size: 22px; }
    .em-tally { min-width: 120px; }
    .em-visit-body { gap: 8px 12px; }
    .em-trick .em-label { font-size: 9.5px; }
    .em-kingdom { min-width: 70px; }
    .em-deck-grid .card.em-deckcard { --cw: 22px; }
    .em-deck-corner { width: 22px; }
    .em-result-row { grid-template-columns: 38px 40px 62px 1fr; font-size: 12px; }
    .em-result-favor { grid-column: 1 / -1; }
}
/* ==== Deck of Spies (deckofspies.html). A solitaire deck-builder.
   Everything shared (.panel, .cardrow, .card, .btn, .logline, .result-actions,
   .winner-banner, .card.sel, .card.clickable, .card.simple.back, .card.empty)
   is reused as-is; only what is genuinely this game's own lives here, all of it
   prefixed `dos-`. The layout runs top to bottom: the Spy Deck row, your two
   piles, your hand, then the contextual action bar.
   ========================================================================= */

/* All four piles share ONE row (Spy Deck + Spy Discard, then your Draw Deck and
   discard), so the board costs one card-height of vertical space instead of two.
   Wraps to two rows only when the viewport cannot hold it. */
.dos-table {
    display: flex;
    justify-content: center;
    align-items: stretch;
    flex-wrap: wrap;
    gap: 12px;
    margin-bottom: 10px;
}
.dos-table > .panel { margin: 0; }

.dos-spy {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    flex-wrap: wrap;
    gap: 18px;
}
.dos-piles {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: 18px;
}

/* A pile: label, a card slot (back, face-up top, or empty), and a count. */
.dos-pile { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.dos-pile-label {
    font-size: 11px; opacity: 0.7;
    text-transform: uppercase; letter-spacing: 0.4px;
}
.dos-pile-slot { min-height: calc(var(--cw) * 89 / 64); }
.dos-pile-count { font-size: 12px; opacity: 0.75; }

/* The spy on top of the Spy Deck is the turn's focus - give it a gold ring. */
.dos-spy-card { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Static (non-interactive) faces sit flat - no hover lift. */
.dos-static { cursor: default; }

.dos-spy-info {
    align-self: center;
    display: flex; flex-direction: column; gap: 4px;
    min-width: 160px;
}
.dos-spy-value { font-size: 17px; font-weight: 700; }
.dos-spy-suit { font-size: 13px; opacity: 0.9; }
.dos-suit { font-weight: 700; }
.dos-suit.red { color: #ff6a6a; }
/* Per-suit refinement at this game's red intensity - see :root. */
.dos-suit.red.diamond { color: #ff926a; }
.dos-suit.club { color: var(--blue-club); }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .dos-suit.red.diamond { color: var(--fc-diamond); }

.dos-hand-title { margin: 0 0 8px; text-align: center; }
.dos-hand-row { gap: 6px; justify-content: center; }

.dos-actions { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.dos-selinfo { font-size: 14px; opacity: 0.92; text-align: center; }
.dos-danger-text { color: #ff8a8a; font-weight: 600; }
/* The reason a selection was refused - most often the no-padding rule, which
   names the exact card that is not needed. */
.dos-why { opacity: 0.8; font-style: italic; max-width: 46em; }
.dos-buttons { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.dos-acepick { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }

/* Assassination is a forced, destructive move: a red button says so. */
.btn.dos-danger {
    background: #7a1f1f;
    border-color: #a83232;
    color: #ffecec;
}
.btn.dos-danger:hover:not(:disabled) { background: #9a2a2a; }

.dos-result-summary {
    margin: 10px 0;
    display: flex; flex-direction: column; align-items: center; gap: 4px;
}
.dos-result-line { font-size: 14px; }
.dos-result-verdict { font-weight: 600; opacity: 0.9; }

@media (max-width: 560px) {
    .dos-spy, .dos-piles, .dos-table { gap: 10px; }
    .dos-spy-info { min-width: 0; flex-basis: 100%; }
}

/* Deck of Spies - reference panels (spies still to face; your whole deck). */
.dos-ref-title { margin: 0 0 8px; text-align: center; }
.dos-ref-group { margin-bottom: 8px; }
.dos-ref-grouplabel {
    font-size: 11px; opacity: 0.7; text-align: center;
    text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 3px;
}
.dos-ref-empty { font-size: 12px; opacity: 0.5; text-align: center; }
.dos-reference .cardrow.mini { flex-wrap: wrap; gap: 3px; justify-content: center; }
/* A Key Spy (J/Q/K) already in your deck: ring it so progress is visible. */
.card.dos-key { outline: 2px solid var(--accent); outline-offset: 1px; }
/* ---------- gridcannon (solitaire) ---------- */

/* The 5x5 board: a 3x3 grid of stacks ringed by twelve royal slots, with the
   four outer corners left empty. Cards size down via a local --cw so all five
   columns fit comfortably. */
.gc-board {
    --cw: 52px;
    display: grid;
    grid-template-columns: repeat(5, var(--cw));
    grid-auto-rows: calc(var(--cw) * 89 / 64);
    gap: 6px;
    width: fit-content;
    /* Centered (the Cuttle treatment). The tracks are fixed, so the
       fit-content box is a constant size and never re-centers mid-game. */
    margin: 12px auto;
}
.gc-corner { }
.gc-cell, .gc-slot { position: relative; }
.gc-cell .card, .gc-slot .card { --cw: 52px; }

/* Stack depth badge on a multi-card cell. */
.gc-stack-badge {
    position: absolute;
    bottom: 3px; right: 3px;
    min-width: 15px; height: 15px;
    padding: 0 3px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.7);
    color: #fff;
    font-size: 11px; line-height: 15px; text-align: center;
}

/* An empty royal slot: a faint dashed placeholder so the ring reads as twelve
   waiting positions rather than blank space. */
.gc-slot-empty { opacity: 0.3; }

/* A dead royal is left face-down in place, marked with a red cross. */
.gc-dead { filter: grayscale(0.4) brightness(0.8); }
.gc-dead-mark {
    position: absolute;
    inset: 0;
    display: flex; align-items: center; justify-content: center;
    color: #d40000;
    font-size: 26px; font-weight: 700;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
    pointer-events: none;
}

/* Living royal badge. It shows the damage the royal STILL NEEDS given the cards
   already in its attack line, not its total health - see slotNode. Red once
   armor has been added (armor only ever makes a royal harder to kill); gold at
   0, where the shot is loaded and placing on the far cell kills it. */
.gc-hp {
    position: absolute;
    top: -6px; right: -6px;
    min-width: 18px; height: 18px;
    padding: 0 4px;
    border-radius: 9px;
    background: #1c1c1c;
    border: 1px solid var(--panel-border);
    color: #fff;
    font-size: 12px; line-height: 18px; text-align: center; font-weight: 600;
}
.gc-hp-armored { background: #8a1a1a; border-color: #d46; }
/* Last, so a loaded shot reads gold even on an armored royal. */
.gc-hp-ready {
    background: var(--accent); border-color: #8a6a00; color: var(--accent-ink);
}

/* A placement that would KILL a royal: the gold ring the drawn-card note points
   at. Deliberately a different channel from `.lit` (which only says "legal"),
   and paired with the 0 badge on the royal it would kill. */
.gc-kill {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 10px 2px rgba(255, 209, 102, 0.55);
}

/* Cells the card in hand cannot go on: dimmed so the eye skips them. Applied
   only while something is actually being aimed. */
.gc-nofit { opacity: 0.35; filter: saturate(0.5); }

/* The stack whose top card the player has picked as a Reassignment source. */
.gc-joker-from { outline: 3px solid var(--accent); outline-offset: 1px; }

/* The drawn card awaiting placement. */
.gc-drawn { }
.gc-drawn-title, .gc-ploys-title { margin: 0 0 8px; font-size: 15px; }
.gc-drawn-row { display: flex; gap: 14px; align-items: center; justify-content: center; }
.gc-drawn-card { --cw: 62px; }
.gc-drawn-info { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
.gc-drawn-note { font-size: 14px; opacity: 0.9; max-width: 320px; }

/* The controls and the card rows share the board's center axis, so the
   centered board does not sit above left-stuck furniture. */
.gc-actions { display: flex; gap: 8px; margin: 8px 0; flex-wrap: wrap; justify-content: center; }

/* Collected ploys: aces (Extractions) and jokers (Reassignments). */
.gc-ploys-row { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; }
.gc-ploy {
    display: flex; flex-direction: column; align-items: center; gap: 5px;
    padding: 8px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.2);
}
.gc-ploy.used { opacity: 0.4; }
.gc-ploy-card { --cw: 42px; }
.gc-ploy-kind { font-size: 12px; opacity: 0.85; }
.gc-ploy-spent { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; }

/* The reserved drawn-card slot: an empty outline holding the panel's height
   between draws, so drawing a card never moves the board. */
.gc-drawn-slot { opacity: 0.35; }
/* The "this placement kills a royal" line, in the accent that the ring uses. */
.gc-drawn-kill { font-size: 14px; color: var(--accent); font-weight: 600; max-width: 320px; }

/* ---- the two reference panels (stacks, remaining deck) ---- */

.gc-views { display: flex; gap: 8px; margin: 10px 0; flex-wrap: wrap; justify-content: center; }
.gc-panel-note { font-size: 13px; opacity: 0.75; margin: 0 0 10px; max-width: 640px; }
.gc-stacks-title, .gc-deck-title { margin: 0 0 6px; font-size: 15px; }

/* Stacks: one row per cell, cards listed top-first. */
.gc-stacks-list { display: grid; gap: 6px; }
.gc-stacks-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.gc-stacks-cell { font-size: 13px; opacity: 0.8; min-width: 56px; }
.gc-stacks-empty { font-size: 13px; opacity: 0.6; font-style: italic; }
.gc-stacks-row .card { --cw: 34px; }
/* The top card reads normally; the one under it is ringed, because that is the
   card a Reassignment would expose. */
.gc-stacks-next { outline: 2px solid var(--accent); outline-offset: 1px; }
.gc-stacks-tag {
    font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em;
    color: var(--accent);
}

/* Remaining deck: the card sheet as a grid, drawn cards blanked in place. */
.gc-deck-grid {
    display: grid;
    grid-template-columns: 34px repeat(14, 1fr);
    gap: 3px;
    align-items: center;
    width: fit-content;
}
.gc-deck-grid .card { --cw: 27px; }
.gc-deck-corner, .gc-deck-blank { }
.gc-deck-rank { font-size: 11px; opacity: 0.7; text-align: center; }
.gc-deck-suit { display: flex; align-items: center; gap: 3px; font-size: 13px; }
.gc-deck-suit.red { color: #ff6b6b; }
/* Diamonds at this red’s saturation and lightness, hue turned 16 deg toward
   orange, as platform.css derives every game’s diamond. */
.gc-deck-suit.red.diamond { color: #ff936b; }
.gc-deck-suit.club { color: var(--blue-club); }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .gc-deck-suit.red.diamond { color: var(--fc-diamond); }
.gc-deck-pip { font-size: 15px; }
.gc-deck-count { font-size: 11px; opacity: 0.75; }
/* A card already out of the deck: the cell stays, so position still identifies
   it, but the face is gone. Fixed height keeps the grid from reflowing. */
.gc-deck-gone {
    background-image: none;
    background: rgba(255, 255, 255, 0.05);
    border: 1px dashed var(--panel-border);
    box-shadow: none;
}


/* ---------- Card Capture ----------
 *
 * Prefix policy: every Card Capture-specific class is `cc-` (matching `rg-`,
 * `cascade-`, `gc-`). Shared classes are reused as-is and get no rules here:
 * hand cards select with `.card.sel` and `.clickable`, the four counts are
 * `.pile-chip`s, the panels sit on `.panel`'s palette variables, the buttons
 * are `.btn`, the result banner is the shared `.winner-banner`, and the log is
 * the shared `.logline`.
 *
 * Two cues are game-specific and both are ADDITIVE to `.sel`, because a card
 * can be affordable and aimed at simultaneously: affordability is a green
 * glow (a box-shadow, so it composes with `.sel`'s outline), and "royal, so it
 * can never be handed over" is a red corner wedge (a ::after, so it composes
 * with everything).
 */

/* THE ROW AND THE SUIT STRIP SHARE A LINE on a desktop, so the strip costs no
   vertical band of its own and the hand stays where it was. The board takes the
   space it needs and the strip takes what is left; when the two no longer fit,
   the flex wraps and the strip goes full width under the row. */
.cc-topline { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
.cc-topline > #cc-board { flex: 1 1 420px; }
.cc-topline > #cc-strength { flex: 1 1 200px; }

/* The Enemy Row, laid out as the rules draw it: Position 4 on the left,
   Position 1 on the right. It scrolls rather than wraps - the row is one
   conveyor and a wrapped second line would read as eight positions.

   THE SCROLLER AND THE CENTERING ARE SEPARATE BOXES, and that is a bug fix, not
   tidiness: `justify-content: center` on the scrolling element itself makes
   overflow at the START unreachable and clipped, which ate the first character
   of the leftmost slot's cost line (a Q needing 12 read as "2"). An inline-flex
   row as wide as its content and centered with `margin: 0 auto` overflows to
   the right and scrolls when it does not fit.

   A BLOCK-LEVEL GRID, NOT INLINE-FLEX. The first version of this fix was
   `display: inline-flex; margin: 0 auto`, and auto margins do nothing on an
   inline-level box, so the cards sat 89px left of center at 1280px (measured
   in Chromium). `width: max-content` makes the grid exactly as wide as its
   slots, and `margin: 0 auto` then centers it in the scroller.

   EQUAL COLUMNS. `grid-auto-columns: 1fr` in a max-content grid gives every
   slot the width of the widest one, so the four cards are evenly spaced. As
   flex items each slot was as wide as its own cost line ("14 short by 10" is
   wider than "5 ready"), and the card centers drifted 85 to 96px apart. */
.cc-board-title { margin: 0 0 10px; }
.cc-row-scroll { overflow-x: auto; padding-bottom: 4px; }
.cc-row {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    width: max-content;
    margin: 0 auto;
    gap: 10px;
    align-items: end;
}
.cc-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    flex: none;
}
/* Only the front slot carries a label, so the others keep an empty line of the
   same height and the four cards stay aligned. */
.cc-slot-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    opacity: 0.6;
    white-space: nowrap;
    min-height: 1em;
}
/* Position 1 is where the enemy takes from, so it is the one worth reading
   first: full-strength label and a hairline under the slot. */
.cc-slot-front .cc-slot-label { opacity: 0.95; font-weight: 700; }
.cc-slot-front { border-bottom: 2px solid var(--accent); padding-bottom: 4px; }
.cc-slot-cost {
    font-size: 12px;
    opacity: 0.75;
    white-space: nowrap;
}
/* The cost line carries the answer, so it is styled to be read at a glance:
   a green pill when the target is reachable, a plain dim one when it is not. */
.cc-slot-cost.cc-ready {
    opacity: 1;
    color: #0d2f16;
    background: #6fc27d;
    font-weight: 700;
    border-radius: 999px;
    padding: 1px 8px;
}
.cc-slot-cost.cc-short { opacity: 0.6; }
.cc-board-note { font-size: 12px; opacity: 0.65; margin-top: 10px; max-width: 620px; }

/* The status line, reserved at the tallest wrap an ordinary phase line takes at
   each width, so the hand does not move between phases. Measured in Chromium:
   at 390px the Capture Phase line takes three lines and the Discard Phase line
   two, and the hand dropped 25px each time the phase changed. A Suggest hint can
   still run longer; it is a deliberate, seven-second message. */
.status.cc-status { line-height: 1.3; min-height: 1.3em; }
@media (max-width: 1100px) { .status.cc-status { min-height: 2.6em; } }
@media (max-width: 720px) { .status.cc-status { min-height: 3.9em; } }

/* The row's verdict, in words: how many targets are reachable, or that none is.
   The none case is the one that changes what the player does next, so it is the
   one that gets color. */
/* Always present and one line tall, empty outside the Capture Phase, so the
   board is the same height in every phase and the hand below never jumps. Both
   texts are short enough to stay on one line down to a 320px screen. */
.cc-verdict {
    font-size: 13px;
    margin-top: 10px;
    max-width: 620px;
    min-height: 1.4em;
    line-height: 1.4;
}
.cc-verdict-some { opacity: 0.8; }
.cc-verdict-none {
    font-weight: 600;
    color: var(--accent);
}

/* Affordable right now. A box-shadow rather than an outline so it stacks with
   `.card.sel`'s outline when the card is also the one being aimed at. */
.card.cc-affordable {
    box-shadow: 0 0 0 3px #6fc27d, 0 0 12px 3px rgba(111, 194, 125, 0.55);
}

/* A royal: must be captured, and may never be sacrificed or handed over.
   Marked in both places it matters - in the row (what you still owe) and in
   your hand (what you cannot spend your way out of). */
.card.cc-royal,
.card.cc-royal-target { position: relative; }
/* A FLYING CLONE KEEPS THE TAG, so it must not keep the `position: relative`
   that the tag needs. ui/animate.ts places a ghost with `position: fixed` and
   inline left/top; `.card.cc-royal` is two classes and sits later in the sheet,
   so without this it would win and the ghost would be laid out in flow instead,
   flying from the wrong place. Three classes, so this wins in turn - and
   `position: fixed` is itself a containing block, so the corner tag still
   anchors to the card. (The tag used to be named in ghostStripClasses instead,
   which is what made it blink off for the length of every animation.) */
.card.cc-royal.card-fly,
.card.cc-royal-target.card-fly { position: fixed; }
.card.cc-royal::after,
.card.cc-royal-target::after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    width: 28%;
    height: 28%;
    border-top-right-radius: 6%;
    background: linear-gradient(225deg, var(--red) 0 55%, transparent 55%);
    pointer-events: none;
}

/* The hand row: cards left, the action column (buttons + guidance) right. The
   hand is centered in the space it gets (the site-wide #hand-cards rule),
   matching the row above it, so the two card groups line up on one axis. */
.cc-handrow { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
.cc-handrow #hand-cards { flex: 1; min-width: 0; }
.cc-actions {
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex: none;
    width: 260px;
}
.cc-note { font-size: 13px; opacity: 0.85; }

/* One action: its button, then a fixed line saying what it does to the enemy
   and what it costs you. That pair of facts is where Give up and Sacrifice
   differ, so it sits right under each button rather than in the rules. The
   caption is centered under its full-width button, like the label inside it. */
.cc-action { display: flex; flex-direction: column; gap: 3px; }
.cc-action-desc {
    font-size: 11.5px;
    line-height: 1.3;
    opacity: 0.72;
    text-align: center;
}

/* An ARMED action: pressed before its selection was finished, and waiting to be
   told what to tap. It already carries `.primary`, so this adds the "pending"
   read on top rather than restating the fill. */
.btn.cc-armed {
    outline: 2px solid var(--ink);
    outline-offset: 2px;
}

/* The four piles, opened by the Show piles button. Sorted by suit, never in
   pile order - see renderPilesPanel in src/cardcapture/def.ts. */
.cc-piles { display: flex; flex-direction: column; gap: 14px; }
.cc-pile-title {
    margin: 0 0 6px;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    opacity: 0.75;
}
.cc-piles .card.mini { --cw: 34px; }
.cc-pile-empty { font-size: 13px; opacity: 0.55; }
.cc-pile-note { font-size: 12px; opacity: 0.6; max-width: 620px; }

/* Your four suits: what each is worth. Read while deciding whether a sacrifice
   is safe, so it is always on screen during play rather than behind a toggle -
   which is also why it has to be SMALL. Everything is centered: it is four
   numbers, not prose, and a left-rag column of them beside a centered row of
   cards read as a mistake. The reach ceiling and the biggest card still
   standing live in each chip's tooltip. */
.cc-strength { padding: 8px 12px; text-align: center; }
.cc-strength-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    margin-bottom: 6px;
}
.cc-strength-title { margin: 0; font-size: 13px; }
.cc-strength-jokers { font-size: 11px; opacity: 0.6; }
/* Narrow pills, wrapping to two rows when the strip sits beside the board. */
.cc-strength-grid {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    justify-content: center;
}
.cc-suit {
    display: flex;
    align-items: baseline;
    gap: 5px;
    padding: 2px 9px;
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.18);
}
.cc-suit-sym { font-size: 14px; line-height: 1; }
.cc-suit-red .cc-suit-sym { color: var(--red); }
.cc-suit-total { font-size: 17px; font-weight: 800; }
.cc-suit-cards { font-size: 11px; opacity: 0.55; }
.cc-suit-cards::before { content: '/ '; }
/* A royal in this suit is now past reach, so it can never be captured and never
   be given up either: the run is unwinnable. The only warning state there is -
   see renderStrength for why a stranded non-royal cannot occur. */
.cc-suit-stranded { border-color: var(--red); background: rgba(255, 60, 60, 0.12); }
.cc-suit-stranded .cc-suit-total { color: var(--red); }
/* Only the warning gets a line; the all-clear version said the same thing every
   round for a whole game. */
.cc-strength-warn {
    font-size: 12px;
    font-weight: 600;
    color: var(--red);
    margin-top: 8px;
}

/* The end of a run: the four numbers that describe it. "Still standing" is the
   distance from victory, so it leads and is the emphasized one on a loss. */
.cc-result-line { margin: 0 0 14px; max-width: 620px; }
.cc-result-stats { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 4px; }
.cc-stat { text-align: center; }
.cc-stat-value { font-size: 22px; font-weight: 800; }
.cc-stat-primary .cc-stat-value { font-size: 34px; color: var(--accent); }
.cc-stat-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    opacity: 0.65;
}
.cc-stat-note { font-size: 11px; opacity: 0.55; }

/* The position a lost run ended on: the hand, the row, and the arithmetic that
   shows no move existed. The hand panel is gone by then, so this is the only
   place those cards are painted. */
.cc-final {
    margin: 16px 0;
    padding: 12px 14px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.18);
}
.cc-final-title { margin: 0 0 10px; font-size: 14px; }
.cc-final-row { margin-bottom: 10px; }
.cc-final-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    opacity: 0.6;
    margin-bottom: 4px;
}
.cc-final-empty { font-size: 13px; opacity: 0.7; }
.cc-final-targets { display: flex; gap: 14px; flex-wrap: wrap; }
.cc-final-target { display: flex; align-items: center; gap: 8px; }
.cc-final-why { font-size: 12px; opacity: 0.8; max-width: 240px; }
.cc-final-reasons { margin: 8px 0 0; padding-left: 20px; font-size: 12px; opacity: 0.8; }

/* "The Expert got further, what did it do?" One button per finished bot, and its
   whole run as a scrollable log. Same deal and same draws as yours, so the two
   logs compare line by line. */
.cc-runs { margin: 14px 0; }
.cc-runs-controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.cc-runs-label { font-size: 12px; opacity: 0.7; }
.cc-runs-summary { font-size: 13px; font-weight: 600; margin: 10px 0 4px; }
.cc-runs-log {
    max-height: 260px;
    overflow-y: auto;
    padding: 8px 10px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.22);
    font-size: 12px;
}

@media (max-width: 720px) {
    /* The action column stops being a column beside the hand and becomes a
       block under it; four positions no longer fit at full card size, so the row
       steps down and keeps scrolling. */
    .cc-actions { width: 100%; }
    .cc-row .card { --cw: 58px; }
}

/* ==== Club Fight (clubfight.html). A solitaire grid-clearing puzzle.
   Everything shared is reused as-is: the .board-grid / .colbtns / .rowbtns /
   .cells frame and .linebtn come straight from the Grid Poker draft board, and
   .panel, .card, .card.sel, .card.clickable, .card.empty, .card.hinted, .btn,
   .logline, .winner-banner and .result-actions need nothing new. Only what is
   genuinely this game's own lives here, all of it prefixed `cf-`.

   THE WHOLE COLUMN IS CENTERED. A turn is read top to bottom - counters, the
   blocked actions, the grid, your clubs, the six actions - and every one of
   those blocks is content-width, so left-aligning them left the grid stranded
   against one edge with the eye traveling sideways between steps.

   ONE HEIGHT IS RESERVED RATHER THAN FITTED: .cf-status holds two lines whether
   it needs them or not, because it sits ABOVE the board and sizing it to its
   current content is what made the board slide out from under the pointer
   mid-turn. Nothing below the board is reserved, since growing there moves
   nothing anyone is aiming at.
   ========================================================================= */
/* The platform centers both shared text lines. What is left here is the height:
   .cf-status reserves the two lines a narrow screen can need, so the board below
   never moves. The long text (the hint) deliberately lives in the action panel
   instead. */
.status.cf-status {
    display: flex; align-items: center; justify-content: center;
    min-height: 50px;
}

/* The four pile counters. Each is a passive wrapper (so a coach highlight can
   land on it) around the button that opens the pile. */
.cf-pile-line { justify-content: center; }
.cf-pilewrap { display: inline-flex; }
.btn.cf-pilechip { font-variant-numeric: tabular-nums; }
.btn.cf-pilechip.selected { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
/* An opened pile, spread sorted on the simplified art: small, dense, and
   unmistakably a reference rather than part of the board. */
.cf-pile-view { margin-top: 12px; }
.cf-peek-head {
    display: flex; align-items: baseline; justify-content: center;
    flex-wrap: wrap; gap: 10px;
    margin-bottom: 8px;
}
.cf-peek-title { margin: 0; font-size: 15px; }
.cf-peek-note { font-size: 12px; opacity: 0.7; }
.cf-peek-row { justify-content: center; gap: 4px; }
.cf-peek-card { --cw: 38px; cursor: default; }
/* The blockable actions and the suit counts gating them. Always on screen,
   because "three of a suit switches an action off" is the whole tension. */
.cf-blocks {
    display: flex; flex-wrap: wrap; gap: 8px;
    justify-content: center;
    margin: 4px 0 10px;
}
.cf-block {
    display: flex; align-items: baseline; gap: 6px;
    padding: 4px 10px;
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.2);
    font-size: 13px;
}
/* A blocked action is struck through and reddened - unmissable, and it reads
   the same way in a screenshot as it does live. */
.cf-block.blocked {
    border-color: #a83232;
    background: #4a1414;
}
.cf-block.blocked .cf-block-name { text-decoration: line-through; opacity: 0.75; }
.cf-block-name { font-weight: 600; }
.cf-block-suit { font-variant-numeric: tabular-nums; }
.cf-block-suit.red { color: #ff6a6a; }
/* Per-suit refinement at this game's red intensity - see :root. */
.cf-block-suit.red.diamond { color: #ff926a; }
.cf-block-suit.club { color: var(--blue-club); }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .cf-block-suit.red.diamond { color: var(--fc-diamond); }
/* The board: the shared 2x2 board-grid frame at a size that leaves room for the
   hand and the action panel below it on a phone.

   THE CARDS ARE WHAT IS CENTERED, not the frame. The shared frame puts the row
   arrows in a 46px column on the left, so centering the whole box left the nine
   cards 26px right of the column every other block is centered on (measured). A
   third, empty 46px track on the right balances the arrows, so the cards sit on
   the axis and the arrows hang off to the left of it. Keep this track equal to
   the shared rule's first one (.board-grid in src/shared/styles/platform.css). */
.cf-board {
    --cw: 62px;
    margin-inline: auto;
    margin-bottom: 4px;
    grid-template-columns: 46px auto 46px;
}
/* The Run's Discard, in a row RESERVED under the grid: the one place that is on
   screen whenever the grid is, on a phone too (see renderRunBar). The height is
   held whether or not the button is there, so its arrival and departure move
   nothing; the board gives up most of its own bottom margin to pay for it. */
.cf-run-bar {
    display: flex; align-items: center; justify-content: center;
    min-height: 40px;
    margin-bottom: 6px;
}
.cf-run-bar .btn { white-space: nowrap; }
.cf-cell { position: relative; }
.cf-card { --cw: 62px; }
/* A TAPPED card is ringed in place, overriding the shared .card.sel lift.
   The lift is a `transform`, and the card animator measures each card with
   getBoundingClientRect - which reports the TRANSFORMED box - so a 5px lift read
   as a 5px move and flew a ghost for a card that had not gone anywhere. That is
   what "sometimes when I click a card it slides down into place" was. Ringing
   without moving keeps the affordance and leaves the geometry alone. */
.cf-card.sel, .cf-hand-row .card.sel, .cf-display .card.sel {
    transform: none;
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 0 4px rgba(255, 209, 102, 0.25);
}
/* Cards the aimed (or hovered) action would clear. Distinct from .sel (what the
   player tapped) so a Sum in progress shows both "picked" and "will go". */
.card.cf-doomed {
    outline: 3px solid #ff6a6a;
    outline-offset: 1px;
    filter: brightness(0.82) saturate(0.8);
}
/* Cards THIS action cannot touch: pushed back so the reachable ones read as the
   only live cards on the board. Not armed either, so there is nothing to click
   and no pointer cursor to promise otherwise. */
.card.cf-dim {
    opacity: 0.3;
    filter: grayscale(0.7);
    cursor: default;
}
/* A Joker: no suit, no value, and only Any or Line can shift it. */
.cf-joker-mark {
    position: absolute;
    bottom: 3px; left: 50%;
    transform: translateX(-50%);
    padding: 0 4px;
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.72);
    color: #fff;
    font-size: 10px; line-height: 14px;
    text-transform: uppercase; letter-spacing: 0.04em;
    pointer-events: none;
    white-space: nowrap;
}
.card.cf-dim + .cf-joker-mark { opacity: 0.35; }
/* The row and column arrows are DRAWN (a masked shape in the button's text
   color), so they sit exactly on their button's center on every device instead
   of wherever a font's arrow glyph happens to land. The path is symmetric about
   the middle of its 24px box; the column arrow is the same shape turned. Hover
   and disabled still work, because the shape is painted in currentColor and
   the button's opacity applies to it. */
.cf-linebtn { display: flex; align-items: center; justify-content: center; padding: 0; }
.cf-arrow {
    display: block;
    width: 20px; height: 20px;
    background-color: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 12h15M13.5 6l6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 12h15M13.5 6l6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
}
.cf-arrow-down { transform: rotate(90deg); }
/* The hand of clubs. */
/* Your clubs and the action chooser share a line where both fit at their
   natural width, and stack in the same order where they do not (a phone, a
   hand grown wide). Stacked on a desktop they were 33px past the app layout's
   1280x720 frame at its 0.8 floor, with half the width of each row empty. */
.cf-play {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: 12px;
    margin-bottom: 14px;
}
.cf-play > .panel { flex: 1 1 auto; min-width: 0; margin: 0; }
.cf-play > [hidden] { display: none; }
.cf-hand-head {
    display: flex; align-items: baseline; justify-content: center;
    flex-wrap: wrap; gap: 10px;
    margin-bottom: 8px;
}
.cf-hand-title { margin: 0; }
.cf-hand-note { font-size: 12px; opacity: 0.75; }
.cf-hand-row { gap: 8px; justify-content: center; }
/* The action chooser: the six kinds, each with the count it can clear beneath
   it, then the target picker for whichever one is being aimed. */
.cf-actions {
    display: flex; flex-direction: column; align-items: center; gap: 10px;
    margin-top: 12px;
    text-align: center;
}
.cf-actions-title { font-size: 14px; font-weight: 600; }
.cf-kinds { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
/* One action: the button, and under it how many cards it can take. The count is
   what lets six actions be compared by looking instead of by trying. */
.cf-kindcell { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.cf-kind { min-width: 86px; }
/* The chosen action stays gold UNDER THE POINTER too. `.btn:hover:not(:disabled)`
   out-specifies a two-class rule (three against two), so without the hover arm
   the button you just pressed repainted in the translucent hover gray while its
   text kept the dark accent ink - dark on dark, which read as switched off at the
   exact moment it was switched on. */
.btn.cf-kind.selected,
.btn.cf-kind.selected:hover:not(:disabled) {
    background: var(--accent); color: var(--accent-ink); border-color: transparent; font-weight: 600;
}
.cf-kindcount {
    font-size: 11px; opacity: 0.8;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
}
.cf-kindcount-off { opacity: 0.45; }
/* "blocked by" plus the blocking suit's glyph, at full strength: the glyph is the
   point of the line, so it is not dimmed with the button above it. Colors match
   the block chips over the board. */
.cf-kindcount-blocked { opacity: 0.85; white-space: nowrap; }
.cf-kindsuit { font-size: 14px; font-weight: 700; }
.cf-kindsuit.red { color: #ff6a6a; }
.cf-kindsuit.red.diamond { color: #ff926a; }
.cf-kindsuit.club { color: var(--blue-club); }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .cf-kindsuit.red.diamond { color: var(--fc-diamond); }
/* Disabled-because-blocked reads differently from disabled-because-nothing-fits. */
.btn.cf-blocked:disabled { border-color: #a83232; text-decoration: line-through; }
/* The target picker. NOT height-reserved, deliberately: it is the last block in
   the panel and the panel is below the board, so nothing the player is aiming at
   sits underneath it. Reserving here bought no stability and showed as an empty
   band under the six buttons. */
.cf-aim {
    display: flex; flex-direction: column; align-items: center; gap: 10px;
    justify-content: center;
}
.cf-subchoice, .cf-buttons { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.cf-hint { font-size: 13px; opacity: 0.85; }
/* The hint, which is the one line long enough to wrap - hence its home here
   below the board rather than in the status line above it. */
.cf-hintline {
    font-size: 13px;
    padding: 6px 12px;
    border: 1px solid var(--accent);
    border-radius: 8px;
    background: rgba(255, 209, 102, 0.12);
    max-width: 46ch;
}
/* ---- the cooperative two-player variant ---------------------------------
   Three zones the solitaire has no use for: the opening question, the shared
   display card, and the partner's hand (face down until both decks are dry).
   All three are centered with the rest of the column. */
.cf-choose { margin-top: 10px; display: flex; flex-direction: column; align-items: center; gap: 10px; }
/* The display sits between the board and your hand, because that is where it
   sits in the turn: one of the four cards you may play. */
.cf-display {
    display: flex; flex-direction: column; align-items: center; gap: 3px;
    margin: 12px 0 4px;
}
.cf-zone-label {
    font-size: 11px; opacity: 0.7;
    text-transform: uppercase; letter-spacing: 0.4px;
}
.cf-zone-note { font-size: 11px; opacity: 0.65; align-self: center; }
/* A card nobody can act on right now sits flat: no pointer, no hover lift. */
.cf-static { cursor: default; }
/* The partner's hand: one compact line up with the counters, on the simplified
   art. Reference material rather than something to act on, so it is smaller than
   your own hand and never competes with it for the eye. */
.cf-partner {
    display: flex; align-items: center; justify-content: center;
    flex-wrap: wrap; gap: 8px;
    margin: 2px 0 8px;
    font-size: 12px;
}
.cf-partner-label {
    font-size: 11px; opacity: 0.7;
    text-transform: uppercase; letter-spacing: 0.4px;
}
.cf-partner-note { font-size: 11px; opacity: 0.65; }
.cf-partner-row { gap: 3px; align-items: center; }
.cf-partner-row .card { --cw: 26px; }
/* The first-player buttons, named so a coach highlight can point at one. */
.cf-first-0, .cf-first-1 { min-width: 120px; }
.cf-result-summary { margin: 10px 0; display: flex; flex-direction: column; gap: 4px; }
.cf-result-line { font-size: 14px; }
.cf-result-verdict { font-weight: 600; opacity: 0.9; }
@media (max-width: 560px) {
    .cf-board { --cw: 46px; }
    .cf-card { --cw: 46px; }
    .cf-kindcell { flex: 1 1 30%; }
    .cf-kind { min-width: 0; width: 100%; }
    .cf-peek-card { --cw: 32px; }
}
/* ==========================================================================
   Scoundrel
   (src/scoundrel/, pages/scoundrel.mjs)
   ==========================================================================
   Solo dungeon crawl. Every shared class (.card, .cardrow, .mini, .panel,
   .pile-chip, .logline, .winner-banner, .btn, .sel, .clickable, .controls)
   is reused as-is; only what is genuinely this game's own lives here, all of
   it prefixed `sc-`. The layout reads top to bottom the way a turn does:
   the expandable pile panel, health, the room (cards + the selected card's
   actions), then the gear line (weapon stack + backpacks). */

.sc-health { display: flex; flex-direction: column; gap: 6px; max-width: 420px; }
.sc-health-label { font-weight: 600; font-size: 14px; }
.sc-healthbar {
    height: 14px; border-radius: 7px; overflow: hidden;
    background: rgba(0, 0, 0, 0.35);
    border: 1px solid rgba(255, 255, 255, 0.15);
}
.sc-healthfill {
    height: 100%; border-radius: 7px;
    background: linear-gradient(90deg, #2e7d46, #58b06e);
    transition: width 250ms ease;
}
.sc-healthfill.low { background: linear-gradient(90deg, #a83232, #d45050); }

.sc-room-title, .sc-gear-title { margin: 0 0 8px; font-size: 15px; }

.sc-room-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-start; }
.sc-room-slot { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.sc-room-slot > .card { --cw: 84px; }
.sc-slot-note { font-size: 12px; opacity: 0.8; }
.card.sc-pinned { outline: 3px dashed #b98a3c; outline-offset: 2px; }

.sc-actions {
    display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
    margin-top: 12px; min-height: 34px;
}
.sc-action-note { font-size: 12.5px; opacity: 0.8; flex-basis: 100%; }

.sc-gear { display: flex; flex-wrap: wrap; gap: 28px; align-items: flex-start; }
.sc-gear-col { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.sc-gear-note { font-size: 12.5px; opacity: 0.8; max-width: 32em; }

/* The weapon with its kills staggered down-right, so its own value stays
   visible - the table layout the rules sheet describes. */
.sc-weapon-stack { position: relative; display: block; min-height: 118px; --sw: 84px; }
.sc-weapon-stack > .card { --cw: var(--sw); }
.sc-weapon-stack > .card.sc-slain {
    position: absolute; top: 0; left: 0;
    transform: translate(calc(var(--stagger) * 20px), calc(var(--stagger) * 24px));
}
.sc-weapon-stack:has(.sc-slain) { min-height: 200px; }

.sc-pack-row { display: flex; gap: 18px; align-items: flex-start; }
.sc-pack { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.card.sc-spent { filter: grayscale(0.7); opacity: 0.75; }
.card.sc-carried { outline: 2px solid rgba(255, 255, 255, 0.55); outline-offset: 1px; }

.sc-result-line { font-size: 14px; margin: 8px 0; }
.sc-note { font-size: 13px; opacity: 0.85; }

@media (max-width: 560px) {
    .sc-room-slot > .card { --cw: 64px; }
    .sc-weapon-stack { --sw: 64px; }
    .sc-actions .btn { flex: 1 1 100%; }
}


/* The pile chips are disclosure buttons (see renderPiles): the shared
   .pile-chip carries the look, these add the affordance and the panel the two
   of them expand into. */
.sc-pile-btn { cursor: pointer; font: inherit; color: inherit; }
.sc-pile-btn:hover { opacity: 1; border-color: var(--accent); }
.sc-pile-btn[aria-expanded="true"] { opacity: 1; border-color: var(--accent); }
.sc-piles { display: flex; flex-direction: column; gap: 10px; }
.sc-piles-title { margin: 0; font-size: 15px; }
.sc-piles-note { font-size: 12.5px; opacity: 0.8; }
.sc-piles-group { display: flex; flex-direction: column; gap: 4px; }
.sc-piles-label { font-size: 12.5px; font-weight: 600; opacity: 0.85; }
/* =========================================================================
   The Fox in the Forest (foxforest)
   Layout intent: the board is three quiet panels in a row - the decree card
   (trump), the trick, and the trick tracker - and nothing above the hand may
   change height mid-round. A settled trick stays on display until the next
   lead, so the caption line reserves its height; the action column sits
   INSIDE the hand panel so a Fox/Woodcutter decision never moves the cards.
   The scoreboard is the shared .progress-score widget (opt-in class
   .fox-scoreboard above).
   ========================================================================= */

/* Two lines' worth, always: the wording grows when it is explaining a power
   ("AI led the Monarch (J♦) - play the ♦ Ace or your highest ♦.") and the
   shared .status reserves only one line (the truc-status recipe). */
.status.fox-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.fox-status { min-height: 4.4em; }
}

/* "Trick 4 of 13" - a quiet counter beside the scores. */
.fox-progress-tricks {
    font-size: 13px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.6;
}
/* The variant flag: the 8s bite, and that is worth a standing reminder. */
.fox-poison-flag {
    margin-left: auto;
    padding: 3px 10px;
    border-radius: 999px;
    font-size: 12px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: #b7f7c3;
    background: rgba(38, 120, 60, 0.25);
    border: 1px solid rgba(120, 220, 140, 0.4);
}

/* --- the board: decree · trick · tracker --------------------------------- */

.fox-board {
    display: flex;
    gap: 12px;
    align-items: stretch;
    flex-wrap: wrap;
    margin-bottom: 12px;
}
.fox-decreebox,
.fox-trickbox,
.fox-trackerbox {
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
.fox-trickbox { flex: 1 1 240px; }
.fox-trackerbox { flex: 1 1 300px; }
.fox-boxtitle {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    margin-bottom: 6px;
}
.fox-decree-row { display: flex; gap: 10px; align-items: center; }
.fox-decree-meta { display: flex; flex-direction: column; gap: 6px; }
.fox-trump { font-size: 14px; font-weight: 700; white-space: nowrap; }

/* The trick: a gutter of seat names, one card cell per seat, and a "led"
   flag column - fixed grid, so nothing shifts as cards land. */
.fox-trick-grid {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 10px;
    row-gap: 6px;
}
.fox-seat {
    justify-self: end;
    font-size: 13px;
    font-weight: 600;
    opacity: 0.7;
    white-space: nowrap;
}
.fox-cell { display: flex; justify-content: center; }
/* Stagger the leader's card left and the answer right, so who led reads at
   a glance even before the flag is noticed. */
.fox-cell.fox-led { transform: translateX(-8px); }
.fox-cell.fox-follow { transform: translateX(8px); }
.fox-flag {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.5;
    min-width: 26px;
}
.card.fox-winning {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    border-radius: 6px;
}
/* A settled trick stays on display until the next lead, but visibly OVER:
   both cards fade (the winner keeps its ring, a shade dimmer with the rest). */
.fox-cell .card.fox-settled { opacity: 0.55; }
/* One line, reserved: "Trick 7: AI takes it · +1 for the 7" etc. */
.fox-caption {
    margin-top: 8px;
    font-size: 13px;
    opacity: 0.8;
    min-height: 2.6em;
}

/* --- the trick tracker: tricks won, and what the count is worth ---------- */

.fox-track { margin-bottom: 8px; }
.fox-track:last-child { margin-bottom: 0; }
.fox-track-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 3px;
}
.fox-track-name { font-size: 13px; font-weight: 700; }
.fox-track-worth { font-size: 12px; opacity: 0.75; }
.fox-track-strip { display: flex; gap: 2px; }
.fox-band {
    flex: 1 1 0;
    text-align: center;
    font-size: 10px;
    font-variant-numeric: tabular-nums;
    padding: 2px 0;
    border-radius: 4px;
    opacity: 0.55;
    background: rgba(255, 255, 255, 0.07);
    border: 1px solid transparent;
}
/* The bands, told apart by tint: Humble and Victorious pay the same 6 and
   share the warm gold; Defeated is neutral; Greedy is the warning. */
.fox-band-humble { background: rgba(255, 209, 102, 0.16); }
.fox-band-defeated { background: rgba(255, 255, 255, 0.07); }
.fox-band-victorious { background: rgba(255, 209, 102, 0.16); }
.fox-band-greedy { background: rgba(214, 69, 65, 0.22); }
.fox-band.fox-here {
    opacity: 1;
    font-weight: 800;
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}

/* --- the hand panel: cards, with the action strip beneath ----------------- */

/*
 * A 13-card hand is the widest thing on the page, so it gets the panel's
 * WHOLE width: the actions sit in a reserved strip UNDERNEATH rather than in
 * a side column (an earlier 210px column pushed the hand onto a second row
 * far too early). Appearing content still never moves the cards - it is
 * below them, and the strip reserves its height so the cheat sheet under it
 * holds still too.
 */
.fox-handrow {
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-height: calc(var(--cw) * 89 / 64);
}
/* Sized so a fresh hand of thirteen fits ONE ROW in the full-width column
   (1200px page - 300px log - gaps/padding leaves ~820px): 13 x 58 + 12 x 5.
   Held constant as the hand shrinks, so cards never change size mid-round. */
.fox-handrow #hand-cards { gap: 5px; }
.fox-handrow #hand-cards .card { --cw: 58px; }
.fox-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    min-height: 32px;
}
.fox-actions-note { font-size: 12px; opacity: 0.7; line-height: 1.4; }
/* The hint flash can land on the "Keep the decree" button. */
.fox-actions .btn.hinted {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}

/* A card the follow rules exclude right now: visible, inert, dimmed. */
.card.fox-mute { opacity: 0.45; }
/* The Woodcutter's fresh draw, marked while its owner picks the tuck. */
.card.fox-drawn {
    outline: 2px dashed var(--accent);
    outline-offset: 2px;
}

/* --- power markers on the card faces -------------------------------------- */

/* Anchors for the corner badges. */
.fox-board .card,
.fox-handrow .card { position: relative; }
/* Every ability card wears its power in the top-right corner (the corner
   the sprite art leaves clear): the cards that ARE points show the number
   (+1 on a 7, -1 on a Poison 8), the rest the power's glyph - the same one
   printed beside that power in the cheat sheet. */
.fox-pip {
    position: absolute;
    top: 2px;
    right: 2px;
    font-size: 10px;
    font-weight: 800;
    line-height: 1;
    padding: 2px 3px;
    border-radius: 6px;
    border: 1px solid rgba(0, 0, 0, 0.45);
    pointer-events: none;
}
.fox-pip-gain { background: var(--accent); color: #3a2c00; }
.fox-pip-loss { background: #2f7d43; color: #eaffea; }
/* Glyph badges get a FIXED box with flex centering: the arrows' metrics vary
   wildly (the take-back arrow sits low, the cycle rides high), and padding
   alone left them drifting around the pill. */
.fox-pip-power {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 14px;
    height: 14px;
    padding: 0;
    background: var(--accent);
    color: #3a2c00;
    font-size: 11px;
}
/* The 5's cycle arrow draws small for its em size; give it more. */
.fox-pip-power.fox-pip-g5 { font-size: 14px; }

/* --- the cheat sheet ------------------------------------------------------ */

.fox-cheat { padding: 10px 14px; }
.fox-cheat-title {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    margin-bottom: 6px;
}
.fox-cheat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
    gap: 4px 18px;
}
.fox-cheat-row {
    display: grid;
    grid-template-columns: 16px 22px 84px minmax(0, 1fr);
    gap: 8px;
    font-size: 12px;
    line-height: 1.45;
    opacity: 0.85;
}
.fox-cheat-row > b { text-align: center; color: var(--accent); }
/* The same badge the card faces wear, as the legend that explains it. A
   fixed flex-centered box, like .fox-pip-power, so every glyph sits centered. */
.fox-cheat-glyph {
    justify-self: center;
    align-self: start;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 17px;
    height: 17px;
    font-size: 10px;
    font-weight: 800;
    line-height: 1;
    border-radius: 6px;
    border: 1px solid rgba(0, 0, 0, 0.45);
    background: var(--accent);
    color: #3a2c00;
}
.fox-cheat-glyph.fox-glyph-big { font-size: 14px; }
#fox-power-8 .fox-cheat-glyph { background: #2f7d43; color: #eaffea; }
.fox-cheat-name { font-weight: 700; }

/* --- the round summary ---------------------------------------------------- */

/* A narrow, centered card: rows of small numbers for two seats, not the poker
   siblings' grid of five-card hands. */
.fox-summary { max-width: 460px; margin-left: auto; margin-right: auto; text-align: center; }
.fox-summary .result-actions { justify-content: center; flex-wrap: wrap; }
.fox-summary .result-note { text-align: center; }
/* EXPLICIT two seat columns (the truc-result recipe), never auto-fit: the
   shared .result-row wraps a column whose content ("9 (Victorious)") wants
   more than its minmax, which put the two names on different rows. */
.result-table.fox-result { width: fit-content; gap: 1px; margin: 0 auto; }
.result-table.fox-result .result-row {
    grid-template-columns: auto repeat(2, minmax(5.5em, auto));
    gap: 2px 18px;
}
.result-table.fox-result .result-cell {
    text-align: center;
    font-variant-numeric: tabular-nums;
}
.result-table.fox-result .result-label { text-align: left; white-space: nowrap; }
.result-table.fox-result .result-row.totals .result-cell {
    font-size: 20px;
    font-weight: 800;
}
/* The winner's star is a badge, not name text, so the name stays centered
   over its column of numbers (the truc-won recipe). */
.fox-result .result-cell.fox-won { position: relative; color: var(--accent); }
.fox-result .result-cell.fox-won::after {
    content: '\2605';
    position: absolute;
    margin-left: 4px;
}
/* Fox/Woodcutter/decree lines sit quieter than card plays in the log. */
.logline.fox-logpower { opacity: 0.75; }

/* --- the scoring table in the Cheat_Sheet ---------------------------------- */
/* Keyed on the table's own class, not the rules wrapper, so it styles the
   Rules_Summary and the Rules_Page alike. */
.fox-bandtable { border-collapse: collapse; margin: 6px 0 10px; }
.fox-bandtable th,
.fox-bandtable td {
    text-align: left;
    padding: 3px 12px 3px 0;
    vertical-align: baseline;
}
.fox-bandtable th {
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.55;
}
.fox-bandtable .fox-band-value {
    text-align: right;
    white-space: nowrap;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

@media (max-width: 720px) {
    /* Stack the three board panels; the tracker keeps its full strip. */
    .fox-board { flex-direction: column; }
}

/* ---------- hub discovery: player chips, search/filter bar, Recommended ----------
 * The bar itself is built by dist/shared/hub.js into the generated #hubbar
 * mount; the .gameplayers chip is static HTML from tools/gen-pages.mjs. */

/* The tile's meta chips (players / time / complexity / category). */
/*
 * A 2x2 GRID, not a wrapping row.
 *
 * Two columns fold the four chips into a block whose height matches the 44px
 * icon, so the chips cost no tile height, and leave the blurb the wide column
 * it needs to stay inside two lines (one 4-across row would halve the text
 * width and push most blurbs to three). Grid rather than flex-wrap because the
 * chips' widths vary with their text - a "Trick-taking" chip is nearly twice a
 * "Poker" one - and any fixed max-width tuned for the narrow case spills the
 * wide case onto a third row. Columns size to content, so 2x2 always holds.
 */
.gamemeta {
    display: grid;
    grid-template-columns: auto auto;
    justify-content: end;
    justify-items: end;
    align-content: center;
    gap: 5px;
    flex: none;
    margin-left: auto;
}
.gamechip, .gameplayers {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: none;
    padding: 2px 9px;
    border-radius: 999px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    color: rgba(255, 255, 255, 0.75);
    font-size: 12.5px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
/* fill only: the clock carries its own stroke/stroke-width ATTRIBUTES, and a
   CSS stroke declaration here would override them (CSS beats presentation
   attributes) and erase the icon. */
.gameplayers-icon, .gamechip-icon { width: 13px; height: 13px; fill: currentColor; opacity: 0.85; }
/* The two WORD chips. Complexity ("Simple") is a measure, so it stays quiet -
   lower contrast, no fill; category ("Trick-taking") is a kind, so it is tinted
   and can be scanned down the column. Different enough that two words sitting
   side by side do not read as one phrase. */
.gameweight {
    background: none;
    color: rgba(255, 255, 255, 0.62);
    font-weight: 500;
}
.gamecat {
    background: rgba(255, 209, 102, 0.1);
    border-color: rgba(255, 209, 102, 0.3);
    color: rgba(255, 226, 160, 0.9);
    font-weight: 600;
}

/* The "Because you played … · rated 7.2" line on Recommended tiles. */
.gamerec {
    display: block;
    margin-top: 3px;
    color: var(--accent);
    font-size: 12px;
}

/* The favorite heart: the deck's own U+2665 in the shared --red (the
   .cardlabel.red precedent), hollow and quiet until toggled. The button sits
   on the <li>, outside the tile's <a> - nested interactive content is invalid
   HTML - vertically centered in a right-edge slot that .gamelink reserves
   with padding, so it stays clear of the chip block at every width. The
   .soon placeholders keep the original 14px: they get no heart. */
.gamelist li { position: relative; }
.gamelink { padding-right: 42px; }
.gamefav {
    position: absolute;
    top: 50%;
    right: 4px;
    transform: translateY(-50%);
    width: 34px;
    height: 34px;
    appearance: none;
    background: none;
    border: none;
    border-radius: 8px;
    padding: 0;
    color: rgba(255, 255, 255, 0.4);
    font-size: 19px;
    line-height: 1;
    cursor: pointer;
}
.gamefav:hover { color: var(--red); background: rgba(255, 156, 156, 0.1); }
.gamefav.favorited { color: var(--red); }

#hubbar { display: flex; flex-direction: column; gap: 10px; margin: 16px 0 4px; }
#hubbar:empty { display: none; }

/* The two tabs, plus the Random game button pushed to the far end (see the
   comment on randomBtn in src/shared/hub.ts for why it rides this row). Centered
   rather than stretched so the button keeps its own height; the tabs are the
   tallest item, so their selected underline still meets the row's border. */
.hubtabs { display: flex; gap: 0; align-items: center; border-bottom: 1px solid var(--panel-border); }
.hubtabs .hubrandom { margin-left: auto; }
.hubtab {
    appearance: none;
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    padding: 7px 14px;
    color: rgba(255, 255, 255, 0.7);
    font: inherit;
    font-size: 14.5px;
    font-weight: 600;
    cursor: pointer;
}
.hubtab:hover { color: var(--ink); }
.hubtab.selected { color: var(--accent); border-bottom-color: var(--accent); }

.hubsearch {
    width: 100%;
    padding: 8px 12px;
    border-radius: 10px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
    color: var(--ink);
    font: inherit;
    font-size: 14.5px;
}
.hubsearch::placeholder { color: rgba(255, 255, 255, 0.45); }
.hubsearch:focus { outline: none; border-color: var(--accent); }

/* Filter chips sit FLUSH with the panel behind them: no fill of their own, just
   a quiet outline. Thirty-odd of these share one narrow column, and giving each
   a --panel fill on top of the card's own --panel stacked two translucent whites
   into a raised cell, so the filters read as busier than the game list they are
   there to serve. Only the SELECTED chip earns a fill. */
.hubchips { display: flex; flex-wrap: wrap; gap: 7px; }
.hubchip {
    appearance: none;
    padding: 4px 12px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.11);
    background: none;
    color: rgba(255, 255, 255, 0.8);
    font: inherit;
    font-size: 13px;
    cursor: pointer;
}
.hubchip:hover { border-color: var(--accent); }

/* The Players / Length / Complexity / Origin filters and the sort row:
   segmented chips, not native selects. A select paints its own chrome, so the
   dropdowns that used to sit here came out as system-colored boxes on macOS and
   iOS no matter what this file said. These share the .hubchip vocabulary, only
   quieter and tighter - a filter REFINEMENT rather than a category choice - so
   the panel reads as one control surface at every width. */
.hubseg { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px; }
.hubseglabel {
    margin-right: 2px;
    color: rgba(255, 255, 255, 0.55);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.4px;
    text-transform: uppercase;
}
.hubsegbtn {
    appearance: none;
    min-width: 32px;
    padding: 4px 10px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.11);
    background: none;
    color: rgba(255, 255, 255, 0.72);
    font: inherit;
    font-size: 12.5px;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
}
.hubsegbtn:hover { color: var(--ink); border-color: var(--accent); }

/*
 * ONE selected treatment for both control types.
 *
 * The category chips used to fill solid gold while the segmented refinements
 * took a muted tint. That read as a deliberate distinction while they lived in
 * different parts of the bar, but the sidebar stacks all six groups in one
 * column, where two different "on" states just look like a mistake.
 *
 * No font-weight change in either: bolding reflowed the chip, which nudged
 * every chip after it in the row and moved the next target out from under the
 * cursor. Fill, border and ink carry the state instead.
 */
.hubchip.selected,
.hubsegbtn.selected {
    background: rgba(255, 209, 102, 0.18);
    border-color: var(--accent);
    color: var(--accent);
}

/*
 * ...except "Any" / "All", which is the option that filters NOTHING.
 *
 * Five of the six groups start on it, so painting those as active filters lit
 * up the whole panel on first load and drew the eye to the five choices doing
 * no work. Neutral here means every accent chip you see is a filter you
 * actually applied. It still reads as the current choice - brighter ink and a
 * firmer border than its unselected siblings - just not as a narrowing one.
 */
.hubchip.hubany.selected,
.hubsegbtn.hubany.selected {
    background: none;
    /* 0.45, not 0.3: with no fill and no weight change, the border is the only
       thing separating "this is the current choice" from its siblings at 0.11,
       and 0.3 measured close to the floor of legibility at a glance. */
    border-color: rgba(255, 255, 255, 0.45);
    color: var(--ink);
}

.hubnote { margin: 0; font-size: 13px; color: rgba(255, 255, 255, 0.6); }

/* ---------- the filter sidebar ----------
 *
 * Six filter groups is more than the bar can carry in a row of chips, so they
 * move OFF the reading path: a column beside the list where there is room for
 * one, and behind a Filters button where there is not. The default narrow view
 * is therefore quieter than the wrapping chip rows it replaces - a search box,
 * one row of sort chips, and the list.
 *
 * The two-column grid waits for the .ready class that dist/shared/hub.js adds
 * once it has filled the panel, and the card only widens (.wide) when the panel
 * exists, so a browser with no JS - or one holding an index.html from before
 * this markup existed - still gets the plain 640px list.
 */
.hublayout { display: block; }
#hubside:empty { display: none; }

/* Collapsed by default; the button below reveals it. */
/*
 * Selected by ID, not by a class, because the id is the actual contract: the
 * generator emits <div id="hubside">, hub.ts finds it with getElementById, and
 * the Filters button points at it with aria-controls. These rules were written
 * as `.hubside` first and every one of them was DEAD - the element carries no
 * such class - which left the panel an unstyled wrapping stack: no column
 * grids, no 28px group gaps, labels inline with their chips, and a Filters
 * button that toggled a class nothing listened to. Keep these keyed to #hubside
 * so the styling cannot drift from the element the script actually finds.
 */
#hubside { display: none; }
#hubside.open { display: grid; gap: 28px; margin: 6px 0 16px; }

.hubgroup { display: block; }
/* Same voice as .hubseglabel, so a chip cloud's heading and a segmented
   control's inline label read as the same kind of thing. */
.hubgrouptitle {
    margin: 0 0 7px;
    color: rgba(255, 255, 255, 0.55);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.4px;
    text-transform: uppercase;
}

/*
 * Every filter group in the column is a fixed-column GRID, not a wrapping row.
 *
 * Wrapping left ragged holes wherever the next label was a few pixels too wide:
 * Category's first row held "All" and "Poker" and then stopped, because
 * "Trick-taking" could not follow them, so a third of the row sat empty. The
 * column count comes from --filter-cols, set per group by dist/shared/hub.js
 * (it inherits, so it reaches this grid whether the grid IS the block or is
 * nested in a .hubgroup), chosen so each group divides evenly.
 *
 * The vertical rhythm is the point of the explicit gaps: 7px between chips, the
 * same 7px from a label to the chips it labels, and 28px between groups. Those
 * used to be 7, 16 and 22 - close enough that a group's own heading looked as
 * far from its chips as the next group's chips did, and the sections read as one
 * undifferentiated stack.
 */
#hubside .hubchips,
#hubside .hubseg {
    display: grid;
    grid-template-columns: repeat(var(--filter-cols, 2), minmax(0, 1fr));
    align-items: stretch;
    gap: 7px;
}
/* The label spans the row and leans on the grid's own row-gap for its spacing,
   so there is exactly one number controlling label-to-chips. */
#hubside .hubseglabel { grid-column: 1 / -1; margin: 0; }
#hubside .hubchip,
#hubside .hubsegbtn {
    min-width: 0;
    padding: 5px 10px;
    font-size: 12.5px;
    text-align: center;
    /* Cells are sized for the longest label in the group; a chip that wrapped
       would make its whole row taller than its neighbors. */
    white-space: nowrap;
}

/* A choice that would empty the list stays clickable but stops advertising
   itself. Never dimmed while selected: that state has to keep reading as ON,
   which is exactly how a player sees they have filtered themselves into a
   corner. */
.hubchip.empty:not(.selected), .hubsegbtn.empty:not(.selected) { opacity: 0.35; }
.hubchip.empty:not(.selected):hover { border-color: var(--panel-border); }

/*
 * Category and Features (.hubpairs) pair up two to a row where their labels
 * allow it, and a label too long for half the column takes the whole row. These
 * two groups are a wrapping FLEX row rather than the grid the others use,
 * because which chips can pair is a question about rendered text width. A grid
 * cannot ask it, and the script cannot measure it while the panel is hidden
 * behind the Filters button.
 *
 *   - Every chip asks for half the row. It is 4px off half rather than 3.5px so
 *     two halves plus the 7px gap can never round past 100% and wrap.
 *   - No chip goes narrower than its own label (min-width: max-content), so a
 *     label wider than half cannot share a line and wraps onto one of its own.
 *   - flex-grow hands out what is left, so a pair splits its row evenly and a
 *     chip alone on a line, long or not, fills it. That keeps the promise the
 *     fixed-column grids make: no holes.
 *
 * Measured in Chromium and WebKit at the 240px column, where half is 116px:
 * every Category label pairs, and in Features "Open information" (124px) takes
 * a row and leaves Deck-building a row of its own. Pairs form in list order
 * (CATEGORY_CHIPS in src/shared/hublink.ts, FACETS in src/shared/catalog.ts), so a
 * new chip's position in its list decides what it shares a row with. On a
 * phone the panel is wider and every chip pairs.
 *
 * Neither group prints a count on its chips, so the tooltip and the `empty`
 * dimming do that job instead.
 */
#hubside .hubpairs {
    display: flex;
    flex-wrap: wrap;
}
#hubside .hubpairs .hubchip {
    flex: 1 1 calc(50% - 4px);
    min-width: max-content;
}

.hubclear {
    /* justify-self for the sidebar's grid, align-self for the fallback path
       where the filters stack inside the flex-column #hubbar instead. */
    justify-self: start;
    align-self: start;
    appearance: none;
    background: none;
    border: none;
    padding: 2px 0;
    color: var(--accent);
    font: inherit;
    font-size: 13px;
    text-align: left;
    text-decoration: underline;
    cursor: pointer;
}

/* ---------- the top strip: search row, sort row ---------- */

.hubsearchrow { display: flex; align-items: center; gap: 8px; }
.hubsearchrow .hubsearch { flex: 1 1 auto; }
/* Random game lands here on a device with no play history, which has no tab
   strip to ride (see randomBtn in src/shared/hub.ts). Stretched to the search
   box's height: its own padding makes it 3px shorter on a desktop and 7px on
   a phone, which reads as misaligned beside a box it is sitting next to. */
.hubsearchrow .hubrandom { align-self: stretch; }
.hubtoggle {
    flex: none;
    appearance: none;
    padding: 8px 14px;
    border-radius: 10px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
    color: var(--ink);
    font: inherit;
    font-size: 13.5px;
    font-weight: 600;
    cursor: pointer;
}
.hubtoggle:hover { border-color: var(--accent); }
.hubtoggle[aria-expanded='true'] { border-color: var(--accent); color: var(--accent); }

/* The designer pin, "By Reiner Knizia ×": the one control for the filter a game
   page's "N more games" link opens (src/shared/hub.ts). It wears the SELECTED
   chip treatment, because it is exactly that, a filter in force; the × says a
   press removes it. The row exists only while a designer is pinned. */
.hubpinrow { display: flex; }
.hubpin {
    appearance: none;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 4px 8px 4px 12px;
    border-radius: 999px;
    border: 1px solid var(--accent);
    background: rgba(255, 209, 102, 0.18);
    color: var(--accent);
    font: inherit;
    font-size: 13px;
    white-space: nowrap;
    cursor: pointer;
}
.hubpinx { font-size: 16px; line-height: 1; opacity: 0.8; }
.hubpin:hover .hubpinx { opacity: 1; }

.hubsortrow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
/* Empty until its chip is the selected one, so it must not reserve space. */
.hubsortarrow:not(:empty) { margin-left: 3px; font-size: 11px; }
/* The result count at the right end of the sort row, and EMPTY unless a search
   or filter is narrowing the list (apply() in src/shared/hub.ts). An auto margin
   on the span itself, because the .hubsortend box it used to share with the
   Random game button has no second occupant now. */
.hubcount {
    margin-left: auto;
    color: rgba(255, 255, 255, 0.55);
    font-size: 12.5px;
    font-variant-numeric: tabular-nums;
}
/* Matches the Filters toggle's outline vocabulary, with the die glyph carrying
   the "chance" idea and the accent reserved for hover. */
.hubrandom {
    flex: none;
    appearance: none;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 7px 13px;
    border-radius: 10px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
    color: var(--ink);
    font: inherit;
    font-size: 13.5px;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
}
.hubrandom:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.hubrandom:disabled { opacity: 0.4; cursor: default; }
.hubdie { width: 15px; height: 15px; flex: none; }

@media (min-width: 900px) {
    /* Room for a column beside the list: widen the card by the width the
       sidebar takes, so the list column keeps roughly its 640px reading
       measure rather than losing a third of it to the filters. */
    .setup-card.hub.wide { max-width: 1080px; }
    .hublayout.ready {
        display: grid;
        grid-template-columns: 240px minmax(0, 1fr);
        gap: 22px;
        align-items: start;
    }
    /* Always open here, whatever the button was last clicked to. */
    #hubside, #hubside.open { display: grid; gap: 28px; margin: 0; }
    .hubtoggle { display: none; }
}

@media (max-width: 720px) {
    .gamechip, .gameplayers { padding: 2px 7px; font-size: 11.5px; }
    /* A phone tile has no room for a 2x2 chip block beside the text, so the
       chips move BELOW the blurb as one full-width row: four small chips fit
       across, costing a single line instead of squeezing the blurb into five.
       The ICON still shares the first row with the name and blurb - it used to
       take `flex: 1 1 100%` on .gametext as its cue to wrap, which stranded the
       icon alone on a line above the text and made every tile a row taller for
       nothing. Only the chips carry a 100% basis now, so only the chips wrap. */
    .gamelink { flex-wrap: wrap; }
    .gametext { flex: 1 1 0; order: 1; }
    .gameicon { order: 0; }
    .gamemeta {
        flex: 1 1 100%;
        order: 2;
        /* One row of four small chips, left-aligned under the blurb. */
        grid-template-columns: repeat(4, auto);
        justify-content: start;
        justify-items: start;
        margin-left: 0;
        margin-top: 2px;
    }
    .hubtab { padding: 7px 10px; }
}

/* On a phone the Random game button loses its text and becomes the die alone.
   Measured at 320px: the two tabs and a labeled button want ~333px of a 256px
   content box, so the label is what makes three controls fit on the tab row
   instead of wrapping to a fourth line - which is the whole reason the button
   moved up here. The title and aria-label are untouched, so the control still
   announces itself as "Open a random game from the ones shown", the same trade
   the game toolbar's icon buttons make with .btnlabel. */
@media (max-width: 560px) {
    .hubrandomlabel { display: none; }
    .hubrandom { padding: 7px 9px; }
}


/* The Card abilities legend (pages/foxforest.mjs): each line opens with the
   power's mark (.ability-mark), so it carries no bullet. */
.fox-abilities { list-style: none; padding-left: 0; }
/* =========================================================================
   Pop!
   Seven four-card tricks played alternately from a hidden hand and a public
   display, with a pop cube naming the round's top prize. Layout follows the
   Le Truc recipe throughout: the CUBE rides at the right-hand end of the
   scoreboard so betting never changes any row's height, the status line
   reserves two lines, the trick row always paints all four slots plus a
   caption line, and the pop controls keep their column inside the hand panel
   whether or not they hold a decision.
   ========================================================================= */

/* --- a status line that does not resize ----------------------------------- */
.status.pop-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.pop-status { min-height: 4.4em; }
}

/* --- the scoreboard line --------------------------------------------------- */

/* Pop! opts into the shared scoreboard widget in platform.css (the
   `.progress.*-scoreboard` list), which is what makes this line a centered
   flex row with a gap at all. Before it joined, `Round 5`, `You 3/7`,
   `Bot 4/7` and `Trick 3 of 7` were inline boxes appended with no text nodes
   between them, so they sat flush against each other with no separation
   whatsoever - and `margin-left: auto` on the trump chip had no flex container
   to push against, so the trump/cube pair never reached the right-hand end
   either.

   The gap is widened past the shared 10px here: this line carries SIX things
   (round, two score chips, the trick counter, the trump flip, the cube), which
   is more than any other game puts in it, and at 10px the pills read as one
   run. The row gap stays tighter so the wrapped phone layout does not grow
   taller than the two lines it reserves. */
.progress.pop-scoreboard { gap: 8px 18px; }

/* --- the pop cube, at the right-hand end of the scoreboard ----------------- */
/* The trump chip (see below) carries the margin-left: auto that pins the
   right-hand pair; the cube just follows it. */
.pop-cube {
    display: inline-flex;
    align-items: baseline;
    gap: 7px;
    padding: 4px 14px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.28);
    border: 1px solid var(--panel-border);
}
/* Once the cube has been popped it is the most important number on the
   screen, so it stops being furniture and lights up. */
.pop-cube.raised {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.14);
    box-shadow: 0 0 0 1px var(--accent);
}
/* A pop awaiting an answer pulses, because it is blocking the game. */
.pop-cube.pending {
    border-color: var(--accent);
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.pop-cube-label {
    font-size: 11px;
    opacity: 0.7;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
.pop-cube-value {
    font-size: 24px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.pop-cube.raised .pop-cube-value { color: var(--accent); }
.pop-cube-offer { font-size: 15px; font-weight: 700; color: var(--accent); }

.pop-progress-tricks {
    font-size: 13px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.6;
}
@media (max-width: 620px) {
    /* Too narrow to sit beside the scores: the trump-and-cube pair wraps to
       its own line, still above the board, still never changing height. */
    .pop-trumpchip { margin-left: 0; flex-basis: 48%; justify-content: center; }
    .pop-cube { flex-basis: 48%; justify-content: center; }
}

/* --- the board ------------------------------------------------------------ */

/* One column: opponent display, trick, own display. The trump flip lives in
   the scoreboard line as a chip. On narrow screens nothing needs to drop
   underneath. */
.pop-board {
    display: flex;
    align-items: stretch;
    gap: 12px;
    margin-bottom: 12px;
}
.pop-main {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 12px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}

/* A display strip: header line, then the two face-up cards and the face-down
   count. Cards mid-sized - they are read but not aimed at. */
.pop-strip { border-radius: 10px; padding: 4px 6px; }
.pop-striphead {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
}
.pop-stripname { font-size: 13px; font-weight: 600; opacity: 0.75; }
/* The strip header carries ONE number: tricks won. The race to four is the
   round, so it reads like a score, not like furniture. */
.pop-striptricks {
    font-size: 13px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    opacity: 0.85;
}
/* Four banked: the top prize is settled, and the tally says so. */
.pop-striptricks.pop-topprize { color: var(--accent); opacity: 1; }
/* Centered: the displays are the table's fixed furniture, and centering them
   lines both players' cards up over the trick between them. */
.pop-striprow { align-items: center; justify-content: center; gap: 8px; }
/* A card the acting player cannot play right now: wrong area or wrong suit.
   The legal moves are the bright ones. */
.pop-board .card.pop-faded,
#hand-cards .card.pop-faded {
    opacity: 0.35;
    filter: saturate(0.55);
}
.pop-striprow .card { --cw: 64px; flex: none; }
/* The demanded area glows: "same area, then the other area" teaches itself. */
.pop-strip.pop-demand {
    outline: 2px solid rgba(255, 209, 102, 0.55);
    outline-offset: 1px;
}
/* The face-down remainder: a card-back chip wearing its count. */
.pop-pile {
    width: 44px;
    height: calc(64px * 89 / 64);
    border-radius: 6px;
    background: #39516e;
    border: 1.5px solid rgba(0, 0, 0, 0.35);
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0.85;
}
.pop-pilecount { font-size: 16px; font-weight: 800; color: rgba(255, 255, 255, 0.85); }

/* The trick: four slots in play order, full-size cards - the trick is what
   is being read. WHOSE slot needs no name: the viewer's slots sit a step
   LOWER (their side of the table) and the opponent's a step higher, with the
   area caption riding the outer edge - above the opponent's slots, below the
   viewer's. The slot the game is waiting on is marked. */
.pop-trickrow {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 14px;
    /* Room for the stagger, so the shifted cells never collide with the
       strips above and below. */
    padding: 14px 0;
}
.pop-trickcell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.pop-trickcell.pop-theirs { transform: translateY(-12px); }
.pop-trickcell.pop-mine { transform: translateY(12px); }
.pop-trickcell .card { --cw: 76px; flex: none; }
.pop-trickcell.pop-next .card.empty {
    outline: 2px dashed rgba(255, 209, 102, 0.6);
    outline-offset: 2px;
}
.pop-trickcap {
    font-size: 11px;
    opacity: 0.7;
    text-align: center;
    line-height: 1.3;
    min-height: 1.4em;
    text-transform: capitalize;
}
/* The card that took a just-settled trick, held on the table until the next
   lead sweeps it toward the winner. */
.card.pop-winning { outline: 3px solid var(--accent); outline-offset: 2px; }
/* One reserved line about the previous trick, so "who just took that?" never
   needs the log. */
.pop-lasttrick {
    text-align: center;
    font-size: 12px;
    opacity: 0.85;
    min-height: 1.5em;
}

/* The trump flip, as a CHIP in the scoreboard line beside the cube: the
   simplified small card face and one glyph, in the corner a player already
   checks for the cube. (It used to be a side box, which cost the whole
   board a column for two words.) */
.pop-trumpchip {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.28);
    border: 1px solid var(--panel-border);
}
.pop-trumpchip .card { --cw: 26px; flex: none; }
.pop-trumpchip-label {
    font-size: 11px;
    opacity: 0.7;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
.pop-trumpchip-text { font-size: 15px; font-weight: 700; }

/* --- the hand panel + action bar (the Le Truc recipe) ---------------------- */
.pop-handrow {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    min-height: calc(var(--cw) * 89 / 64);
}
.pop-handrow #hand-cards { flex: 1 1 auto; }
.pop-actions {
    flex: 0 0 232px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 0;
    border-radius: 12px;
    border: 1px solid transparent;
}
.pop-actions.filled {
    padding: 9px 11px;
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 209, 102, 0.35);
}
.pop-actions .btn { width: 100%; }
.pop-actions-note { font-size: 12px; opacity: 0.8; line-height: 1.35; }
@media (max-width: 760px) {
    .pop-handrow { flex-wrap: wrap; }
    .pop-actions { flex: 1 1 100%; }

    /*
     * A POP AWAITING AN ANSWER IS PINNED TO THE VIEWPORT, on phones only.
     *
     * The controls live inside the hand panel (the Le Truc recipe: a column
     * beside the cards, so an offer appearing never slides the hand). That is
     * right on a desktop, where the whole page is in view - and wrong on a
     * phone, where the panel sits below the status line, the scoreboard and
     * the three board rows. The player was told "Bot pops!" at the top of the
     * screen and the Accept and Fold buttons were a scroll away, off the
     * bottom: reported as not seeing the accept button until later.
     *
     * Pinning rather than reordering, because a pop BLOCKS the game: nothing
     * else can be pressed until it is answered, so the one decision on offer
     * belongs over the content instead of somewhere in it. It also leaves the
     * board exactly where it was, which matters here - whether to accept is
     * read off the tricks and the cube, so scrolling the buttons into view
     * would have taken the evidence off screen to reach them.
     *
     * Only `.pop-blocking` (set by renderActions for the respond phase) gets
     * this. The ordinary Pop! button stays in the panel: it is an option, not
     * a demand, and a bar pinned over the board for every turn would be in
     * the way.
     *
     * z-index 300 is over the board (30) and under every overlay and dialog
     * (400+), so the handoff gate still covers it.
     */
    .pop-actions.pop-blocking {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 300;
        margin: 0;
        border-radius: 14px 14px 0 0;
        border: 1px solid rgba(255, 209, 102, 0.55);
        border-bottom: none;
        background: rgba(18, 18, 24, 0.97);
        box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.55);
        /* Clear of the home-bar inset on a phone, and of the notch sides. */
        padding: 11px max(12px, env(safe-area-inset-left))
            calc(11px + env(safe-area-inset-bottom))
            max(12px, env(safe-area-inset-right));
    }
    /* The bar is content-width in the middle of a wide phone rather than two
       buttons stretched edge to edge. */
    .pop-actions.pop-blocking .btn,
    .pop-actions.pop-blocking .pop-actions-note {
        max-width: 420px;
        margin-inline: auto;
    }
    .pop-actions.pop-blocking .pop-actions-note { text-align: center; }
}
.pop-actions .btn.hinted {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}
/* The Pop! button once the top prize is certain on visible information
   (four tricks banked, or three plus a trick nothing can take away): the
   pop is free money, so the button turns primary and glows. */
.pop-actions .btn.pop-armed {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
    animation: hintpulse 1.4s ease-in-out infinite alternate;
}

/* --- the Jokers, told apart ------------------------------------------------ */
/* Two ranks apart in this game, one face on the sprite sheet - so every
   painted Joker wears a ribbon saying which it is. The Big Joker's is gold. */
/* Scoped through the class this game's page puts on #hand-panel, because the id
   belongs to every page: .card has no position of its own, so unscoped this made
   every game's hand cards a containing block for their absolutely positioned
   badges. */
.pop-board .card,
.pop-handpanel #hand-cards .card {
    position: relative;
}
.pop-jokertag {
    position: absolute;
    left: 50%;
    bottom: 14%;
    transform: translateX(-50%);
    padding: 1px 5px;
    border-radius: 6px;
    font-size: 9px;
    font-weight: 800;
    letter-spacing: 0.06em;
    background: rgba(40, 40, 60, 0.85);
    color: rgba(255, 255, 255, 0.9);
    border: 1px solid rgba(255, 255, 255, 0.35);
    pointer-events: none;
    white-space: nowrap;
}
.pop-jokertag.big {
    background: rgba(90, 62, 0, 0.92);
    color: #ffd166;
    border-color: #ffd166;
    font-size: 10px;
}

/* --- log chips and quiet lines --------------------------------------------- */
/* The Jokers get a chip color of their own - they are the deck's celebrities. */
.cardlabel.pop-joker { color: #c9a0ff; }
/* Display flips are bookkeeping: present, but quiet. */
.logline.pop-logflip { opacity: 0.6; }
.logline.pop-logbet { font-weight: 600; }

/* --- the round summary (the Le Truc narrow table) --------------------------- */
.panel.pop-summary {
    max-width: 380px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
.panel.pop-summary .result-actions { justify-content: center; flex-wrap: wrap; }
.panel.pop-summary .result-note { text-align: center; }
.result-table.pop-result { width: fit-content; gap: 1px; margin: 0 auto; }
.result-table.pop-result .result-row {
    grid-template-columns: auto repeat(2, minmax(3.6em, auto));
    gap: 2px 22px;
    align-items: baseline;
}
.result-table.pop-result .result-cell {
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.result-table.pop-result .result-label { text-align: left; white-space: nowrap; }
.result-table.pop-result .result-cell.pop-won {
    position: relative;
    color: var(--accent);
}
.result-table.pop-result .result-cell.pop-won::after {
    content: '\2605';
    position: absolute;
    right: 0;
    top: -0.95em;
    font-size: 11px;
}

/* ==========================================================================
   Aces
   ==========================================================================
   Two-player climbing/shedding race with wild Aces. Every shared class
   (.card, .cardrow, .panel, .pile-chip, .logline, .result-actions,
   .winner-banner, ...) is reused as-is, so only what is genuinely Aces' own
   lives here - all of it prefixed `ace-`.

   The layout job here: ONE PILE, TWO PHASES. The same hand panel serves the
   draw phase (pick cards to exchange) and the play phase (pick cards, choose
   a reading), so the action column carries the phase's meaning and the board
   above stays put - nothing jumps when the round moves from drawing to
   playing. */

/* EVERYTHING SITS ON ONE CENTERED AXIS (the Cuttle treatment): the
   scoreboard, the opponent's backs, the pile and the hand tray all center on
   the same vertical line, so the eye tracks one column down the board instead
   of a left-aligned ragged edge. */

.ace-board { margin-bottom: 12px; }

/* ---------- the scoreboard ---------- */
.ace-score {
    display: flex;
    gap: 10px;
    align-items: stretch;
    flex-wrap: wrap;
    justify-content: center;
    margin-bottom: 10px;
}
.ace-scorepill {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 6px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    min-width: 128px;
}
.ace-scorepill.active { border-color: var(--accent); }
.ace-scorename { font-weight: 700; font-size: 13px; }
.ace-scorepoints {
    font-size: 22px;
    font-weight: 800;
    line-height: 1.1;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}
.ace-scorenote { font-size: 12px; opacity: 0.75; }

/* ---------- the pile chips ---------- */
.ace-piles .pile-chip::before { content: none; }

/* ---------- the opponent ---------- */
.ace-opp {
    padding: 8px 10px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.14);
    margin-bottom: 12px;
}
.ace-opphead {
    display: flex;
    gap: 10px;
    align-items: baseline;
    flex-wrap: wrap;
    justify-content: center;
    margin-bottom: 6px;
}
.ace-oppname { font-weight: 700; }
.ace-oppcount { font-size: 13px; opacity: 0.8; }
.ace-oppdrew {
    font-size: 12px;
    font-weight: 700;
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-ink);
}
.ace-opprow { flex-wrap: wrap; gap: 4px; align-items: center; justify-content: center; }
/* The opponent's hidden hand: one narrow back per card, overlapped - the only
   thing this row communicates is HOW MANY. */
.card.ace-oppback { --cw: 30px; margin-right: -14px; }
.card.ace-oppback:last-of-type { margin-right: 0; }

/* ---------- the pile ---------- */
.ace-table { margin-top: 4px; }
.ace-tablelabel {
    font-size: 14px;
    margin-bottom: 6px;
    min-height: 2.6em;
    text-align: center;
}
.ace-tablelabel b { color: var(--accent); }
/* The min-height reserves one pile's worth of room so the board cannot jump
   mid-round. def.ts hides the row outright once the game is over, where there
   is no next play to hold still for and the reservation would only push the
   result panel down ([hidden] carries !important, so no rule is needed here
   to beat the flex display). */
.ace-pilerow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    flex-wrap: wrap;
    justify-content: center;
    min-height: 104px;
}
.ace-play { display: flex; flex-direction: column; gap: 4px; opacity: 0.62; }
/* The last combination played is the one that has to be beaten, so it is the
   only one at full strength. */
.ace-play.top { opacity: 1; }
.ace-playwho { font-size: 12px; }
.ace-play.top .ace-playwho { font-weight: 700; color: var(--accent); }
.ace-playcards { gap: 5px; justify-content: center; }
.ace-playcards .card { --cw: 52px; }
.ace-empty { font-size: 13px; opacity: 0.6; font-style: italic; }

/* ---------- the hand ---------- */
.panel.ace-handpanel .ace-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    justify-content: center;
}
/* The tray grows to fill the row; the site-wide #hand-cards rule centers its
   cards inside it, so a four-card hand sits under the pile. */
.panel.ace-handpanel #hand-cards { flex: 1 1 auto; }

/* ---------- the action column ---------- */
.ace-actions {
    flex: 0 0 232px;
    display: flex;
    flex-direction: column;
    gap: 7px;
}
.ace-actions .btn { width: 100%; }
.ace-actionnote { font-size: 12px; opacity: 0.85; line-height: 1.35; }
.ace-actionnote.bad { color: var(--red); opacity: 1; }
.ace-actionnote.ace-hint { color: var(--accent); opacity: 1; }

/* ---------- the round and game summary ----------
   Painted into the SHARED #result-panel (so the shell's Review game / Share
   result controls join the same actions row), and styled through the
   `ace-result` class def.ts puts on it rather than through the id, which
   every game's page carries. */
#result-panel.ace-result {
    max-width: 560px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
#result-panel.ace-result .result-actions { justify-content: center; flex-wrap: wrap; }
.ace-scoretable { margin: 10px 0; font-size: 14px; }
.ace-scoreline { padding: 3px 0; }
.ace-scoreline.totals {
    margin-top: 6px;
    padding-top: 6px;
    border-top: 1px solid var(--panel-border);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

/* A hand card that cannot take part in ANY legal answer to the combo on the
   pile. Dimmed rather than hidden or disabled: the cards stay tappable,
   because selecting one is how the action column gets to explain why it does
   not work - and a dead card that IS selected comes back to full strength
   (the Less Than Three recipe). */
.card.ace-dead { opacity: 0.4; filter: grayscale(0.55); }
.card.ace-dead.sel,
.card.ace-dead.hinted { opacity: 1; filter: none; }

/* ---------- the log ---------- */
.logline.ace-logcleared { color: var(--accent); }


@media (max-width: 720px) {
    /* The action column gives up its own column before the hand does. */
    .panel.ace-handpanel .ace-handrow { flex-wrap: wrap; }
    .ace-actions { flex: 1 1 100%; }
    .ace-playcards .card { --cw: 44px; }
}

/* ==========================================================================
   Swoop
   ==========================================================================
   Three-to-eight-player shedding with two to four packs at once. Every shared
   class (.card, .cardrow, .panel, .panel-head, .pile-chip, .card.empty,
   .card.simple.back, .card.sel, .card.hinted, .card.clickable, .progress-score,
   .logline, .btn, .result-actions, .winner-banner, .result-note, ...) is reused
   as-is, so only what is genuinely Swoop's own lives here - all of it prefixed
   `swoop-`.

   THE LAYOUT PROBLEM this solves: a Swoop table can seat EIGHT, and each seat
   carries a four-column table row on top of a hand count, so the opponent strip
   has to shrink gracefully from three wide panels to eight small ones without
   the pile below it ever moving. Hence fixed-basis panels that wrap, tiny cards
   in the seat rows, and a pile whose fan is capped rather than unbounded - the
   pile can genuinely reach thirty cards, and it must not be allowed to push the
   hand off the screen. */

/* ---------- everything above the hand, height-reserved ----------
   The status line and the scoreboard are the two other things that sit above the
   player's cards and change as the deal runs, so they get the same treatment as
   the middle row. Scoped to `swoop-` classes the renderer adds, because both ids
   are shared with every other game on the site. */
/* reserved: two lines. The status sentence is long ("Play a 6 or lower to land
   free, anything higher and you take the pile first") and wraps at narrower
   widths, and one extra line here moves the whole board. */
/* In `lh`, the element's own line height, rather than an assumed 1.3: the line
   height is inherited, and at 1.4 a two-line status came out 3px taller than
   the reservation, which moved the hand by 4px at 1024px wide. */
#status-line.swoop-status { min-height: 2lh; }
/* Eight seats' score chips would WRAP to a second row, and would start and stop
   wrapping as a score crossed from two digits to three. Scrolled instead, so the
   scoreboard is exactly one row deep for every table size. */
.progress.swoop-scoreboard {
    flex-wrap: nowrap;
    overflow-x: auto;
    justify-content: flex-start;
}
.progress.swoop-scoreboard > * { flex: none; }

/* ---------- the opponents' strip ---------- */
.swoop-table {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.panel.swoop-seat {
    flex: 1 1 168px;
    max-width: 260px;
    padding: 7px 9px;
    margin: 0;
}
/* The seat that must act. Border only: at eight seats a glow on one panel
   pushes nothing around, which is the point. */
.panel.swoop-seat.swoop-acting {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}
/* Everything in a seat panel is centered on the panel: name and score as one
   unit, the hand count, the four stacks, and the total under them. */
.swoop-seat-head {
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 8px;
}
.swoop-seat-name { font-weight: 700; font-size: 13px; }
.swoop-seat-score {
    font-size: 16px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: var(--accent);
}
.swoop-seat-hand {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 6px;
    margin: 4px 0 5px;
    min-height: 26px;
}
.swoop-seat-count { font-size: 12px; opacity: 0.8; }
.swoop-seat-held { font-size: 12px; opacity: 0.62; text-align: center; }
/* One back standing in for a whole hand: the row says HOW MANY, in words. */
.card.swoop-tiny { --cw: 18px; }

/* ---------- a table row: four little STACKS of face-up over mystery ----------
   Each column is drawn as the physical stack it is: the mystery card splayed
   out behind, its cover in front. That replaces a caption reading "one under",
   and it is the surface a player checks most often - how many blind cards each
   seat still has to get through is public, and now it is countable at a glance
   instead of readable. The splay goes down-and-right, so a row of four stacks
   still reads left to right. */
/* --cw LIVES ON THE CONTAINER, not on `.card`, for the reason the steering doc
   gives: the reservations below are computed from it, and a rule on `.card` sets
   it too late for any ancestor to read. (Getting this wrong is not theoretical -
   these reservations were first written against a `.card` rule, so they silently
   computed from the :root --cw of ~74px and reserved 109px for a row of 30px
   minis. 67px of dead space directly above the player's hand.) */
.swoop-cols {
    display: flex;
    justify-content: center;
    gap: 8px;
    align-items: flex-start;
    --swoop-splay: 7px;
}
.swoop-col { display: flex; flex-direction: column; align-items: center; }
/* A FIXED-SIZE BOX holding both cards absolutely: one card wide plus the splay,
   one card tall plus the splay. Sizing it from its contents instead made the
   column 7px taller in some states than others, so the hand crept up and down by
   6px as the row emptied - small, but the same bug as a big one. */
.swoop-stack {
    position: relative;
    width: calc(var(--cw) + var(--swoop-splay));
    height: calc(var(--cw) * 89 / 64 + var(--swoop-splay));
}
.swoop-stack > .card { position: absolute; left: 0; top: 0; z-index: 1; }
.swoop-stack > .card.swoop-under { left: var(--swoop-splay); top: var(--swoop-splay); z-index: 0; }
/* A lone mystery card has nothing in front of it, so it sits square. */
.swoop-stack > .card.swoop-under:only-child { left: 0; top: 0; }
.swoop-collabel {
    font-size: 10px;
    opacity: 0.55;
    line-height: 1.1;
    text-align: center;
}
.panel.swoop-seat .swoop-cols { --cw: 30px; --swoop-splay: 5px; }
.panel.swoop-minepanel .swoop-cols { --cw: 52px; }
/* `.card.swoop-upcard` is deliberately style-free: it is a semantic hook on
   every face-up table card (the app test and any future styling key on it),
   while the SIZE comes from the .swoop-cols rules above. */
/* An UNCOVERED mystery card: pressable, and marked as the risk it is. A covered
   one needs no marking, because the splayed corner already says it is there. */
.card.swoop-mystery { box-shadow: 0 0 0 2px rgba(212, 56, 12, 0.75); }
/* A mystery card opened up at the end of a deal: its owner's, and already
   counted against them, so it reads as spent rather than live. */
.card.swoop-revealed { opacity: 0.85; box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3); }
/* A face-up card on YOUR row that out-ranks the pile gets the hand's fade, but on
   the whole stack rather than the card: at half opacity the face would let the
   mystery back behind it show through. Group opacity composites the stack first,
   so the face stays opaque over the back and the pair dims as one. */
.swoop-stack.swoop-faded { opacity: 0.5; filter: grayscale(0.4); }
.swoop-stack > .card.swoop-costly { opacity: 1; filter: none; }

/* ---------- the middle: the pile and the controls, SIDE BY SIDE ----------
   THE WHOLE POINT OF THIS ROW IS THAT IT DOES NOT MOVE. Everything in it sits
   above the player's own cards, so any of it changing height shoves the hand
   down while somebody is reaching for a card. Every variable part inside the two
   columns has a reserved height of its own (marked `reserved:` below), so the
   row's height is a constant per breakpoint rather than a function of how the
   deal is going.

   Out-of-play is NOT in this row any more. It was a third column, and a swoop
   that took a big pile filled it with a row of cards whose min-content width
   stretched the column until the pile was squeezed to a sliver. It sits below the
   hand now, where growing moves nothing the player is reaching for.

   The one deliberate exception is the end of a deal, where the action column
   grows to hold the score report. The hand is empty by then and the player is
   reading, not reaching. */
.swoop-middle {
    display: flex;
    gap: 12px;
    align-items: stretch;
    /* reserved: the tallest ordinary mid-play shape, so the row cannot SHRINK
       either. An action column with little in it would otherwise pull the hand
       up as readily as a full one pushed it down. */
    min-height: 226px;
    margin-bottom: 10px;
}
.panel.swoop-pilepanel { flex: 1 1 320px; min-width: 0; margin: 0; }
/* reserved: one row, scrolled rather than wrapped. The chips' text changes with
   the pile ("Pile: 12 cards . 118 pts"), and a chip strip that wraps when a total
   reaches three digits moves the whole board with it. */
.swoop-chips {
    display: flex;
    gap: 8px;
    flex-wrap: nowrap;
    overflow-x: auto;
    margin-bottom: 8px;
    min-height: 28px;
}
.swoop-chips > * { flex: none; }
/* The never-dealt count: true, useful for counting, and not what anyone is
   looking at - so it says so. */
.pile-chip.swoop-chip-quiet { opacity: 0.55; }
.pile-chip.swoop-chip-quiet::before { content: none; }
/* The fan. Overlapped so a long pile still fits, and the buried count carries
   the rest: the rules only ask that the equal cards ON TOP be countable.
   NOWRAP AND SCROLLED, not wrapped: the pile grows all deal, and a strip that
   wrapped to a second row would push everything below it down exactly as the
   round got interesting. reserved: one card row, in terms of --cw so it tracks
   the fluid card scaling. */
.swoop-fan {
    --cw: 56px;
    flex-wrap: nowrap;
    align-items: center;
    gap: 0;
    overflow-x: auto;
    min-height: calc(var(--cw) * 89 / 64 + 8px);
}
.card.swoop-piled { margin-right: -22px; }
.card.swoop-piled:last-of-type { margin-right: 0; }
/* The equal cards on top - the group a swoop is built out of. Fully offset so
   they can be counted at a glance, and ringed as one. */
.card.swoop-ontop {
    margin-right: 2px;
    box-shadow: 0 0 0 2px var(--accent);
    z-index: 1;
}
.swoop-buried { font-size: 12px; opacity: 0.6; margin-right: 8px; }
.swoop-empty { font-size: 13px; opacity: 0.65; font-style: italic; }
/* reserved: two lines, because the sentence changes with the top card (and gets
   longer when equal cards pile up on it). */
.swoop-pilenote {
    margin-top: 8px;
    font-size: 13px;
    line-height: 1.35;
    min-height: calc(2 * 1.35 * 13px);
}

/* ---------- the swoop banner, in the pile's own row ----------
   A swoop is resolved inside the same engine call as the play that caused it,
   so the repaint after it shows an empty pile and the cards that made the set
   were never on screen at all. The banner says in words that the biggest event
   in the game just happened, and it says it where the pile was: the fan's row is
   reserved at one card tall whether it holds cards, the empty state, or this, so
   a swoop changes what the row SAYS and never how big it is. */
.swoop-banner {
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
    padding: 7px 10px;
    border-radius: 10px;
    border: 1px solid var(--accent);
    background: rgba(255, 209, 102, 0.12);
}
/* Inside the fan, which is a nowrap flex row: take the width, and let the text
   wrap inside it instead of widening the row. */
.swoop-fan > .swoop-banner { flex: 1 1 auto; min-width: 0; }
.swoop-banner-word {
    font-size: 17px;
    font-weight: 800;
    letter-spacing: 0.06em;
    color: var(--accent);
    flex: none;
}
.swoop-banner-text { font-size: 12.5px; line-height: 1.35; flex: 1 1 200px; }

/* ---------- the action column ---------- */
.swoop-actions {
    flex: 0 0 244px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
/* Controls first and on their own, so no amount of prose can move a button. */
.swoop-controls { display: flex; flex-direction: column; gap: 7px; }
.swoop-controls .btn { width: 100%; }
/* reserved: a FIXED height that scrolls inside. The prose here stacks up (a
   selection's price, a hint, and the dead-deal warning together), and on a phone
   each of those is two or three lines - so left to grow it moved the player's
   cards by 14px at 420px and by 87px when the column was full width. */
.swoop-notes {
    display: flex;
    flex-direction: column;
    gap: 6px;
    height: 5.8em;
    overflow-y: auto;
}
/* The end of a deal is the exception: the report IS the thing being read, the
   hand is empty, and nobody is reaching for a card. */
.swoop-notes.swoop-notes-report { height: auto; overflow: visible; }
.swoop-note { font-size: 12px; opacity: 0.85; line-height: 1.35; }
.swoop-note.bad { color: var(--red); opacity: 1; }
/* Turning a mystery card over cannot be taken back, so the warning is loud. */
.swoop-note.swoop-warn { color: var(--accent); opacity: 1; font-weight: 700; }
.swoop-note.swoop-hint { color: var(--accent); opacity: 1; }
/* A turned-up mystery card, face up in its own column (any seat's) while it
   waits to be played, ringed as the card that must go next. */
.card.swoop-flipped { box-shadow: 0 0 0 2px var(--accent); }
/* Turning a mystery card over: a quarter turn on its edge and back, face up.
   def.ts adds it once, on the repaint the card is turned, and to the copy that
   revealAndFly holds over the column before the flight. That hold
   (REVEAL_HOLD_MS) must outlast this, because dropping the class is what frees
   `transform` for the flight. */
.swoop-turnup { animation: swoop-turnup 0.26s ease-out; }
@keyframes swoop-turnup {
    from { transform: perspective(400px) rotateY(90deg); }
    to { transform: none; }
}
body.motion-off .swoop-turnup { animation: none; }

/* ---------- the viewer's own row and hand ---------- */
.panel.swoop-minepanel { margin-bottom: 10px; }

/* reserved: two lines, since the wording changes when the deal is scored.
   Centered like the stacks above it and the hand below it. */
.swoop-minenote {
    margin-top: 8px;
    font-size: 12px;
    opacity: 0.7;
    line-height: 1.35;
    text-align: center;
    min-height: calc(2 * 1.35 * 12px);
}

/* ---------- out of play: below the hand ----------
   This box is what a swoop's cards fly INTO (def.ts animate gives them the pile
   as their origin). It sits under the hand, the one place on the board allowed
   to grow, so the row WRAPS rather than scrolling: a thirty-card swoop shows all
   thirty, and the only thing it pushes down is the rules below. Centered on the
   same axis as the hand above it. */
.panel.swoop-outpanel { margin-top: 10px; }
.swoop-outcount { font-size: 12px; opacity: 0.7; white-space: nowrap; }
/* One row tall even when empty, so the first swoop fills the box rather than
   growing it. */
.swoop-outrow {
    --cw: 30px;
    justify-content: center;
    align-items: center;
    gap: 3px;
    flex-wrap: wrap;
    min-height: calc(var(--cw) * 89 / 64 + 6px);
}
/* Gone for the rest of the deal: present, countable, and plainly not in play. */
.card.swoop-burned { opacity: 0.6; }
/* A hand card that OUT-RANKS the pile: legal, but it costs you the whole pile
   first. Dimmed rather than disabled, because late in a deal it is sometimes
   exactly the play - and a selected one comes straight back to full strength. */
.card.swoop-costly { opacity: 0.5; filter: grayscale(0.4); }
.card.swoop-costly.sel,
.card.swoop-costly.hinted { opacity: 1; filter: none; }

/* ---------- the deal report and the final table ---------- */
.swoop-report { font-size: 13px; }
.swoop-report-title { font-weight: 700; margin-bottom: 5px; }
.swoop-report-line { padding: 1px 0; opacity: 0.85; font-variant-numeric: tabular-nums; }
.swoop-final { margin: 10px 0; font-size: 15px; max-width: 320px; }
.swoop-final-row {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 3px 0;
    border-bottom: 1px solid var(--panel-border);
}
.swoop-final-row.swoop-final-winner { font-weight: 800; color: var(--accent); }
.swoop-final-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.swoop-final-score { font-variant-numeric: tabular-nums; }

/* ---------- the log ---------- */
.logline.swoop-logswoop { color: var(--accent); }
.logline.swoop-logpickup { opacity: 0.75; }

/* ---------- the rules panels ---------- */
.swoop-rules .swoop-rulenote { font-size: 13px; opacity: 0.85; }

@media (max-width: 1100px) {
    /* The action column gives up its own column, under the pile. Wrapping
       changes this row's height ONCE, at a viewport width, not while a deal is
       running, which is the distinction that matters.
       Full width it becomes a ROW - buttons at their own size, the note beside
       them - which both reads better than a 700px-wide button and keeps the
       strip one or two lines tall instead of five. reserved: measured at 74px
       for the tallest ordinary shape; without it the strip ran from 16px
       ("Waiting for AI 1") to 160px (a selection, its price and a hint) and
       took the hand 144px down the screen with it. */
    .swoop-middle { flex-wrap: wrap; }
    .swoop-actions {
        flex: 1 1 100%;
        flex-direction: row;
        align-items: flex-start;
        gap: 12px;
    }
    /* Buttons at their own size in a row, which reads better than a 700px-wide
       button and keeps the strip two lines tall instead of five. */
    .swoop-controls { flex: 0 0 auto; flex-direction: row; flex-wrap: wrap; }
    .swoop-controls .btn { width: auto; }
    .swoop-notes { flex: 1 1 240px; height: 4.6em; }
    .swoop-actions .swoop-notes.swoop-notes-report { flex: 1 1 100%; }
}
@media (max-width: 720px) {
    /* re-derived: the status wraps to three lines and the pile note to three at
       this width, so both reservations grow. */
    #status-line.swoop-status { min-height: 3lh; }
    .swoop-pilenote { min-height: calc(3 * 1.35 * 13px); }
    /* re-derived: narrower lines wrap more, so the notes box needs more of them. */
    .swoop-notes { height: 6.4em; }
    .swoop-middle { min-height: 0; }
}
@media (max-width: 720px) {
    .panel.swoop-seat { flex: 1 1 140px; max-width: none; }
    .panel.swoop-seat .swoop-cols { --cw: 24px; --swoop-splay: 4px; }
    .panel.swoop-minepanel .swoop-cols { --cw: 42px; --swoop-splay: 5px; }
    .swoop-fan { --cw: 44px; }
    .card.swoop-piled { margin-right: -18px; }
    .swoop-outrow { --cw: 26px; }
}

/* ==========================================================================
   Twenty
   ==========================================================================
   Two-player counting duel: a shared count climbs 1 to 20 and a turn is
   either a draw or a set of cards totaling it exactly. Every shared class
   (.card, .cardrow, .panel, .progress-score, .result-*, .logline,
   .winner-banner, ...) is reused as-is, so only what is genuinely Twenty's
   own lives here - all of it prefixed `twenty-`.

   The layout job here: ONE NUMBER, AND A HAND BEING TAKEN APART. The count
   is the whole public state, so it sits in the middle of the board flanked
   by the stock (one of the two moves) and the set that made the previous
   count (the only place a King's declared value or a pair discount is ever
   visible). Below it the hand panel carries the action column INSIDE itself,
   so Make / Clear / Draw / Pass appearing and disappearing never shifts the
   cards a player is aiming at. */

/* Two reserved lines: the status text swings between "tap the cards that add
   up to 14" and a full hint sentence, and the board must not step up and
   down the screen as it does (the Le Truc rule). */
.status.twenty-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.twenty-status { min-height: 4.4em; }
}

/* Low score wins here, so the scoreboard needs the reminder its shape does
   not carry - .progress-score.leading is on the SMALLEST number. */
.twenty-scorenote {
    margin-left: auto;
    font-size: 12px;
    opacity: 0.6;
    letter-spacing: 0.03em;
}

/* ---------- the board ---------- */
.twenty-board { margin-bottom: 12px; }
.twenty-middle {
    display: flex;
    align-items: stretch;
    justify-content: center;
    gap: 14px;
    flex-wrap: wrap;
}

/* ---------- the opponent: a count of backs, and nothing more ---------- */
.twenty-opp { margin-bottom: 10px; }
.twenty-opphead {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 4px;
}
.twenty-oppname { font-size: 13px; font-weight: 700; }
.twenty-oppcount {
    font-size: 12px;
    opacity: 0.7;
    font-variant-numeric: tabular-nums;
}
/* Two cards or fewer: they are a turn from going out, which is the one thing
   on this board worth an alarm. */
.twenty-oppcount.twenty-nearly {
    opacity: 1;
    color: var(--accent);
    font-weight: 800;
}
.twenty-oppturn {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    opacity: 0.55;
}
/* Overlapped backs, so a long hand stays one glanceable block. */
.twenty-opprow { gap: 0; min-height: calc(var(--cw) * 89 / 64 * 0.5); }
.twenty-opprow .card { --cw: 34px; margin-right: -20px; }
.twenty-opprow .card:last-child { margin-right: 0; }

/* ---------- the count, the centerpiece ---------- */
.twenty-countbox {
    flex: 0 1 240px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 10px 16px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.28);
    border: 1px solid rgba(255, 209, 102, 0.4);
}
.twenty-counthead {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    opacity: 0.6;
}
.twenty-countvalue {
    font-size: 46px;
    font-weight: 800;
    line-height: 1;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}
.twenty-countfoot { font-size: 11px; opacity: 0.65; text-align: center; }
/* The track: one pip per count from where the hand opened to 20. The only
   thing on screen that says how much hand is left. */
.twenty-track {
    display: flex;
    align-items: center;
    gap: 3px;
    flex-wrap: wrap;
    justify-content: center;
    margin: 2px 0 1px;
}
.twenty-pip {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, 0.4);
}
.twenty-pip.made { background: rgba(255, 255, 255, 0.55); border-color: transparent; }
.twenty-pip.now {
    background: var(--accent);
    border-color: var(--accent);
    box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.3);
}
/* Making 20 ends the hand, so the last pip is marked even before it is due. */
.twenty-pip.last { border-color: #ff8a65; width: 9px; height: 9px; }

/* ---------- the stock ---------- */
.twenty-stock {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    justify-content: center;
}
.twenty-stack { position: relative; }
.twenty-stack .card { --cw: 56px; }
.twenty-stackcount {
    position: absolute;
    right: -6px;
    bottom: -6px;
    padding: 1px 6px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    background: rgba(0, 0, 0, 0.75);
    border: 1px solid rgba(255, 255, 255, 0.3);
    pointer-events: none;
}
.twenty-stacklabel {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    opacity: 0.55;
}

/* ---------- the set that made the last count ---------- */
.twenty-last {
    flex: 0 1 240px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    justify-content: center;
}
.twenty-lasthead { font-size: 12px; font-weight: 700; opacity: 0.85; }
.twenty-lastrow { gap: 5px; min-height: calc(var(--cw) * 89 / 64); }
.twenty-lastrow .card { --cw: 44px; }
/* The arithmetic the cards cannot show. Reserves its line either way, so the
   board does not jump between a play that needed a note and one that did not. */
.twenty-lastnote {
    font-size: 11.5px;
    min-height: 1.3em;
    color: var(--accent);
    opacity: 0.9;
    text-align: center;
}
.twenty-lastpile { font-size: 11px; opacity: 0.5; }

/* ---------- the hand panel + action column (the Le Truc recipe) ---------- */
.twenty-handrow {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    min-height: calc(var(--cw) * 89 / 64);
}
.twenty-handrow #hand-cards { flex: 1 1 auto; }
.twenty-actions {
    flex: 0 0 258px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 0;
    border-radius: 12px;
    border: 1px solid transparent;
}
.twenty-actions.filled {
    padding: 9px 11px;
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 209, 102, 0.35);
}
.twenty-actions .btn { width: 100%; }
.twenty-actions-note { font-size: 12px; opacity: 0.8; line-height: 1.35; }
.twenty-actions .btn.hinted {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}
@media (max-width: 760px) {
    .twenty-handrow { flex-wrap: wrap; }
    .twenty-actions { flex: 1 1 100%; }
}
/* A card no legal set could use: dimmed rather than hidden, because WHICH
   cards are dead is information about the position. */
.card.twenty-faded { opacity: 0.34; filter: saturate(0.5); }

/* ---------- the hand summary and the final table ---------- */
.twenty-summary .result-table { max-width: 520px; }
/* The cards each player was caught with - the evidence for the penalty, and
   the only place either hand is ever shown. */
.twenty-caught .result-cell { padding-top: 4px; padding-bottom: 4px; }
.twenty-caught .cardrow.mini .card { --cw: 30px; }
.twenty-clean { font-size: 13px; font-weight: 700; color: var(--accent); }
.result-cell.twenty-won { color: var(--accent); font-weight: 800; }

/* ---------- the log ---------- */
/* A draw never names a card, so its line is quieter than a played set. */
.logline.twenty-logdraw { opacity: 0.7; }
.twenty-cardlabel { font-weight: 700; }

/* =========================================================================
   Schnapsen (schnapsen)
   Layout intent: the board is three quiet panels - the talon (the sideways
   turn-up under the pack), the trick (its state in the box's title line),
   and the standing (one strip per player) - and nothing above the hand may
   change height mid-deal. On a wide screen they sit in one row; on a phone
   the standing runs across the top and the talon and the trick share the
   row under it. The leader's menu (exchange / close / marriage / go out) is
   a side action column INSIDE the hand panel (the Le Truc recipe), so
   decisions appearing never move the cards. Card points appear ONLY under
   the tracked variant.
   ========================================================================= */

/* Two lines' worth, always: the wording grows when it is explaining the
   strict-phase obligations ("follow ♠, beating it if you can"). */
.status.schnaps-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.schnaps-status { min-height: 4.4em; }
}

/* The tracked variant's running card points, beside the game-point chips. */
.schnaps-trackedpts {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
    margin-left: auto;
    padding: 3px 10px;
    border-radius: 999px;
    background: rgba(38, 120, 60, 0.25);
    border: 1px solid rgba(120, 220, 140, 0.4);
}
.schnaps-trackedlabel {
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.75;
}
.schnaps-trackednum {
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.schnaps-trackednum.mine { color: var(--accent); }
.schnaps-scorename { margin-right: 6px; }
.schnaps-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }

/* --- the board: talon · trick · standing --------------------------------- */

/* The standing comes FIRST in the DOM. On a phone it sits above everything,
   with the talon and the trick sharing the row under it; on a wide screen
   `order` moves it after the other two. Card sizes ride custom properties on
   the board (ui-conventions, drive card size through a property) so every
   reserved height below recomputes with them. */
.schnaps-board {
    --schnaps-cw: 52px;       /* the talon's two cards */
    --schnaps-trick-cw: 80px; /* the trick's cards */
    --schnaps-mini-cw: 24px;  /* the standing's read-only cards */
    display: flex;
    gap: 12px;
    align-items: stretch;
    flex-wrap: wrap;
    margin-bottom: 12px;
}
.schnaps-talonbox,
.schnaps-trickbox,
.schnaps-standingbox {
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
    min-width: 0;
}
.schnaps-talonbox { flex: 0 0 auto; }
.schnaps-trickbox { flex: 1 1 240px; }
.schnaps-standingbox { flex: 1 1 300px; order: 3; }
.schnaps-boxtitle {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    margin-bottom: 6px;
    white-space: nowrap;
}
/* The trick box's title says what is happening ("Trick 3: you to answer"),
   so it is not upper-cased like a plain label, and it is one line, always:
   a wrapped title would grow the box mid-deal. */
.schnaps-trickbox .schnaps-caption {
    text-transform: none;
    letter-spacing: 0;
    font-size: 13px;
    opacity: 0.85;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The talon: the sideways turn-up tucked UNDER the pack's left edge, most of
   its face in the clear, and the trump suit's colored glyph and the count of
   cards left under the pair. */
.schnaps-talon-stack {
    position: relative;
    width: calc(var(--schnaps-cw) * 1.95);
    height: calc(var(--schnaps-cw) * 89 / 64);
    flex: none;
}
.schnaps-talon-stack .card { --cw: var(--schnaps-cw); position: absolute; }
/*
 * THE TURN AND THE CENTERING USE THE INDEPENDENT `rotate` AND `translate`
 * PROPERTIES, NEVER `transform`, and the rule is scoped UNDER the stack.
 * `transform` belongs to animation: the shared pop-in (`card-enter`), a
 * hovered or selected card's lift, and a flight ghost all write it, and a
 * card whose quarter turn lived in `transform` lost it for the length of any
 * of them, painting upright and snapping back. The separate properties
 * compose with `transform` instead of being replaced by it. Scoped under the
 * stack so a ghost (a clone in <body>) matches nothing here: the animator
 * gives a ghost the turn it measured. See ui-conventions, "A sideways card
 * turns with rotate".
 */
.schnaps-talon-stack .schnaps-trump-card {
    top: 50%;
    /* Center the rotated face so its right edge meets the box's: only its
       left quarter slides under the pack. */
    left: calc(100% - var(--schnaps-cw) * 89 / 128);
    translate: -50% -50%;
    rotate: 90deg;
}
.schnaps-talon-stack .schnaps-talon-back {
    top: 0;
    left: 0;
    box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.45);
}
/* A closed talon is ONLY the flipped pack: square it up in the box. */
.schnaps-talon-stack > .schnaps-talon-back:only-child {
    top: 50%;
    left: 50%;
    translate: -50% -50%;
    box-shadow: none;
}
/* "Trump ♠" on one line, and under it "closed" or "empty" once the talon can
   no longer be drawn from. Two short lines, so the phone's narrow talon box
   never has to fit them side by side; the trick box beside it is the taller
   one either way, so the extra line moves nothing. */
.schnaps-talon-under {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    margin-top: 4px;
    white-space: nowrap;
}
/* "Trump", then the suit as big as a label can be, in the suit's own ink (the
   same per-suit palette as the sprites and the log: cards.ts suitTextClass). */
.schnaps-trumpword { font-size: 13px; font-weight: 700; }
.schnaps-trumpglyph { font-size: 22px; line-height: 1; font-weight: 700; vertical-align: -2px; }
/* The face-down pack's count, centered on its back. */
.schnaps-talon-stack .schnaps-talon-back {
    display: flex;
    align-items: center;
    justify-content: center;
}
.schnaps-talon-count {
    padding: 1px 5px;
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    font-size: 13px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
.schnaps-trumpglyph.red { color: var(--red); }
.schnaps-trumpglyph.red.diamond { color: var(--red-diamond); }
.schnaps-trumpglyph.club { color: var(--blue-club); }
.schnaps-talon-note { font-size: 12px; opacity: 0.75; }
.schnaps-talon-note.schnaps-closed {
    color: #ffb3ab;
    font-weight: 700;
    opacity: 1;
}

/* The trick: two fixed slots side by side, the opponent's on the left, each
   card over its player's name. Every slot is sized for the widest thing it
   ever holds (a declared marriage's fanned pair), so nothing shifts as cards
   arrive. */
.schnaps-trick-grid {
    display: flex;
    justify-content: center;
    gap: 10px;
}
.schnaps-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    width: calc(var(--schnaps-trick-cw) * 1.5);
}
.schnaps-cell {
    display: flex;
    justify-content: center;
    min-height: calc(var(--schnaps-trick-cw) * 89 / 64);
}
.schnaps-cell .card { --cw: var(--schnaps-trick-cw); }
.schnaps-seat {
    font-size: 13px;
    font-weight: 600;
    opacity: 0.7;
    white-space: nowrap;
}
.schnaps-seat.schnaps-ledby { opacity: 0.9; }
.schnaps-flag {
    margin-left: 5px;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.6;
}
.card.schnaps-winning {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    border-radius: 6px;
}
/* A settled trick stays on display until the next lead, but visibly OVER. */
.schnaps-cell .card.schnaps-settled { opacity: 0.55; }
/* The declaration itself, shown in the declarer's trick cell until the
   pinned King-or-Queen lead lands: both faces, fanned. */
.schnaps-marriage-show { display: flex; }
.schnaps-marriage-show .card + .card {
    margin-left: calc(var(--schnaps-trick-cw) * -0.5);
}

/* --- the standing: one strip per player, marriages then tricks ----------- */

.schnaps-track {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 6px;
}
.schnaps-track:last-child { margin-bottom: 0; }
.schnaps-track-head {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    min-width: 44px;
    white-space: nowrap;
}
.schnaps-track-name { font-size: 13px; font-weight: 700; }
.schnaps-track-worth {
    font-size: 12px;
    opacity: 0.75;
    font-variant-numeric: tabular-nums;
}
/* Reserved to ONE card's height (plus the chips' frames) and scrolling
   sideways rather than wrapping, so the standing never grows as the deal
   goes on (ui-conventions, a card strip that wraps). def.ts keeps it scrolled
   to the newest end. */
.schnaps-strip {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 6px;
    overflow-x: auto;
    min-height: calc(var(--schnaps-mini-cw) * 89 / 64 + 8px);
    scrollbar-width: none;
}
.schnaps-strip .card { --cw: var(--schnaps-mini-cw); flex: none; }
/* A won trick: its two cards overlapped, in a quiet frame. */
.schnaps-wontrick {
    display: inline-flex;
    flex: none;
    padding: 2px;
    border-radius: 5px;
    background: rgba(255, 255, 255, 0.06);
}
.schnaps-wontrick .card + .card { margin-left: calc(var(--schnaps-mini-cw) * -0.45); }
/* A declared marriage: BOTH faces, framed in gold with the points, so it can
   never be read as a trick. */
.schnaps-meld {
    display: inline-flex;
    flex: none;
    align-items: center;
    gap: 3px;
    padding: 2px 6px 2px 2px;
    border-radius: 6px;
    background: rgba(255, 209, 102, 0.16);
    border: 1px solid rgba(255, 209, 102, 0.6);
}
.schnaps-meld .card + .card { margin-left: calc(var(--schnaps-mini-cw) * -0.3); }
.schnaps-meld-value { font-size: 12px; font-weight: 800; color: var(--accent); }
/* Declared but trickless: promised points, not yet real. */
.schnaps-meld-pending { opacity: 0.55; border-style: dashed; }

/* --- the hand panel + action column (the Le Truc recipe) ----------------- */

.schnaps-handrow {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    min-height: calc(var(--cw) * 89 / 64);
}
.schnaps-handrow #hand-cards { flex: 1 1 auto; }
/* Five cards and never more, so they can be big: a size up from the site's
   hand at every width (scoped through the game's own row, never #hand-cards
   alone - ui-conventions, shared ids need a game class). */
.schnaps-handrow #hand-cards .card { --cw: var(--schnaps-hand-cw, 92px); }
.schnaps-actions {
    flex: 0 0 258px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 0;
    border-radius: 12px;
    border: 1px solid transparent;
}
.schnaps-actions.filled {
    padding: 9px 11px;
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 209, 102, 0.35);
}
.schnaps-actions .btn { width: 100%; }
.schnaps-actions-note { font-size: 12px; opacity: 0.8; line-height: 1.35; }
.schnaps-actions .btn.hinted {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}
@media (max-width: 760px) {
    .schnaps-handrow { flex-wrap: wrap; }
    .schnaps-actions { flex: 1 1 100%; }
}

/* A card the follow rules exclude right now: visible, inert, dimmed. */
.card.schnaps-mute { opacity: 0.45; }
/* Trumps in the viewer's hand wear a quiet gold keel. */
.card.schnaps-trumpcard { box-shadow: 0 3px 0 -1px rgba(255, 209, 102, 0.55); }

/* --- the deal summary ----------------------------------------------------- */

.schnaps-summary {
    max-width: 460px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
.schnaps-summary .result-table { margin-top: 10px; }
.result-cell.schnaps-won { color: var(--accent); font-weight: 800; }
/* The shared .result-row auto-fits 180px seat columns, which WRAPS the
   second seat onto its own row inside this narrow two-seat summary (and
   inside the log dropdowns): pin the shape to label + two seats. */
.schnaps-result .result-row {
    grid-template-columns: minmax(90px, auto) 1fr 1fr;
}

/* --- the log -------------------------------------------------------------- */

/* The leader-menu moves (exchange, close, marriage, claims) carry the deal's
   drama: set them a shade apart from plain card plays. */
.logline.schnaps-logmove { color: var(--accent); }

/* A scored deal's collapsed dropdown, with the overview table and the sentence
   above it, is the platform's now: `.loggroup` and `.loggroup-body` in
   src/shared/styles/platform.css style every game's grouped log the same way. */

/* --- the phone -------------------------------------------------------------
   The standing across the top, the talon and the trick sharing one row under
   it, and the hand's five cards as wide as the screen allows. Every size is a
   property, so each reserved height above follows it. */
@media (max-width: 620px) {
    .schnaps-board {
        --schnaps-cw: 40px;
        --schnaps-trick-cw: min(64px, calc((100vw - 190px) / 3.1));
        --schnaps-mini-cw: 22px;
        gap: 8px;
        flex-wrap: wrap;
    }
    .schnaps-standingbox { order: 0; flex: 1 1 100%; padding: 8px 10px; }
    .schnaps-talonbox { order: 1; padding: 8px 10px; }
    .schnaps-trickbox { order: 2; flex: 1 1 0; padding: 8px 10px; }
    .schnaps-handpanel { --schnaps-hand-cw: min(66px, calc((100vw - 84px) / 5.3)); }
    .schnaps-handrow #hand-cards { gap: 5px; }
}


/* The Examples section's opponent table (pages/schnapsen.mjs). On a phone a
   row of three tricks and the reason under "You win" are wider than the
   sheet, so the cards drop to 18px and the reason wraps under its game
   points. */
@media (max-width: 560px) {
    .cheatsheet .sch-extable .cardex { --cw: 18px; }
    .cheatsheet .sch-extable .exnote { white-space: normal; }
}
/* ------------------------------------------------------------------------- */
/* The Blood of an Englishman                                                */
/* ------------------------------------------------------------------------- */
/*
 * Prefix policy: every game-specific class is `eng-` (matching `sono-`,
 * `truc-`). Shared classes are reused as-is: cards pick up with `.card.sel`,
 * arm with `.clickable`, flash with `.card.hinted`; the action row is a
 * `.controls`; panels are `.panel`; the outcome uses `.winner-banner`,
 * `.result-note` and `.result-actions`.
 *
 * THE BOARD IS FIVE VERTICAL FANS, back at the top and front at the bottom -
 * the published game's table layout. Each buried card shows a strip of its
 * top edge (the negative margin below); the front card alone is fully
 * visible. Fans grow and shrink all game, so the columns are top-aligned and
 * the page simply gets taller - nothing may re-center vertically underneath
 * the player's aim.
 */
.eng-board { display: flex; flex-direction: column; gap: 6px; }
.eng-roleline {
    font-size: 13px;
    opacity: 0.85;
    text-align: center;
    font-variant-numeric: tabular-nums;
}
.eng-legend {
    font-size: 12px;
    opacity: 0.6;
    text-align: center;
}
.eng-castles {
    --eng-cw: min(var(--cw), calc((100vw - 80px) / 5));
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: 10px;
    margin: 4px auto;
}
.eng-stack {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    /* Reserve the chip row's height inside the column via min-height on the
       chip itself, not here: a fan's own growth is expected page growth. */
}
.eng-stackhead, .eng-stackfoot {
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    opacity: 0.55;
    min-height: 16px;
    display: flex;
    align-items: center;
    gap: 6px;
}
.eng-fan {
    display: flex;
    flex-direction: column;
    align-items: center;
}
.eng-fan .card { --cw: var(--eng-cw); }
/* Each buried card peeks a strip of its top edge above the card in front of
   it: card height is --cw * 89/64, and hiding all but 0.38*cw of it keeps
   the index corner legible at every size the board reaches. */
.eng-fan .card + .card { margin-top: calc(var(--eng-cw) * -1.01); }
/* Room to breathe for the selection ring and the hover lift. */
.eng-fan .card.clickable:hover, .eng-fan .card.clickable:focus-visible {
    transform: translateY(-2px);
}
/* Cards the acting player cannot touch right now fade back a step. A
   FILTER, never opacity: fan cards overlap, and a translucent card lets the
   one behind it bleed through - unreadable the moment a turn starts. */
.eng-fan .card.eng-dim { filter: grayscale(0.3) brightness(0.75); }

/* Role badges: a gold star on treasures (echoing their beanstalk seat), red
   crossed swords on the Giant's Queens and Kings, so the three card roles
   read at a glance in a dense fan. Pinned to the TOP-RIGHT corner - the
   strip a buried fan card keeps visible, opposite the index the sprite
   already prints top-left - and sized off the card so they shrink with it. */
.eng-fan .card, .eng-donestalk .card, .eng-slot .card { position: relative; }
.eng-badge {
    position: absolute;
    top: calc(var(--cw) * 0.05);
    right: calc(var(--cw) * 0.05);
    width: calc(var(--cw) * 0.3);
    height: calc(var(--cw) * 0.3);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: calc(var(--cw) * 0.19);
    line-height: 1;
    background: rgba(22, 22, 26, 0.78);
    pointer-events: none; /* taps fall through to the card */
}
.eng-badge.treasurecard {
    color: #ffd166;
    border: 1px solid rgba(255, 209, 102, 0.85);
}
.eng-badge.giantcard {
    color: #ff8a7a;
    border: 1px solid rgba(255, 138, 122, 0.7);
}
.eng-emptyslot { --cw: var(--eng-cw); }

/* Destination chips: the "move here" targets that appear under a fan (and
   on the beanstalk) once a card is picked up. Buttons, so they are keyboard
   reachable like everything else. */
.eng-drop {
    font: inherit;
    font-size: 12px;
    padding: 5px 8px;
    border-radius: 8px;
    border: 1px dashed rgba(255, 209, 102, 0.75);
    background: rgba(255, 209, 102, 0.12);
    color: inherit;
    cursor: pointer;
    max-width: calc(var(--eng-cw) + 16px);
}
.eng-drop:hover, .eng-drop:focus-visible {
    background: rgba(255, 209, 102, 0.25);
}

/* The Giant's front-4 group handle, riding in the stack foot. */
.eng-group {
    font: inherit;
    font-size: 11px;
    padding: 2px 6px;
    border-radius: 6px;
    border: 1px solid rgba(255, 255, 255, 0.35);
    background: rgba(255, 255, 255, 0.08);
    color: inherit;
    cursor: pointer;
    text-transform: none;
    letter-spacing: normal;
}
.eng-group:hover, .eng-group:focus-visible {
    background: rgba(255, 255, 255, 0.18);
}

.eng-actions { min-height: 34px; }
.eng-actionnote { font-size: 12px; opacity: 0.7; align-self: center; }
.eng-error { color: #ff9d9d; font-size: 13px; }
/* The status line runs to two lines while it teaches the turn shapes. */
.eng-status { min-height: 2.6em; }

/* Jack's beanstalks: completed stalks as tight mini fans, the live one at
   full size beside them, all top-aligned in one row that wraps on phones. */
.eng-stalks .eng-treasures { display: flex; gap: 6px; }
.eng-treasure {
    font-size: 12px;
    padding: 2px 8px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.25);
    opacity: 0.7;
}
.eng-treasure.won {
    border-color: rgba(255, 209, 102, 0.8);
    background: rgba(255, 209, 102, 0.14);
    opacity: 1;
}
.eng-stalkrow {
    display: flex;
    align-items: flex-end;
    gap: 22px;
    flex-wrap: wrap;
}
/* A banked beanstalk: a compact sideways fan, index corners showing. */
.eng-donestalk { display: flex; }
.eng-donestalk .card + .card { margin-left: calc(var(--cw) * -0.72); }
.eng-currentstalk {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
}
.eng-stalkcap { font-size: 12px; opacity: 0.7; }
.eng-stalkneed { font-size: 12px; opacity: 0.7; max-width: 300px; }

/* The live beanstalk's seven seats: how tall it must grow, at a glance.
   Empty seats are dashed outlines numbered 1-6 with a star for the
   treasure; when a placeable card is held, the open seats light up as
   card-sized tap targets. */
.eng-slots {
    --eng-slotw: min(56px, calc((100vw - 120px) / 7));
    display: flex;
    gap: 6px;
}
.eng-slot {
    position: relative;
    width: var(--eng-slotw);
    height: calc(var(--eng-slotw) * 89 / 64); /* CARD_ASPECT */
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 8px;
    border: 1px solid transparent;
}
.eng-slot .card { --cw: calc(var(--eng-slotw) - 2px); }
.eng-slot.open {
    border: 1px dashed rgba(255, 255, 255, 0.3);
    background: rgba(255, 255, 255, 0.04);
}
.eng-slot.open.treasure { border-color: rgba(255, 209, 102, 0.55); }
.eng-slot.open.next { border-style: solid; }
.eng-slotnum {
    font-size: 13px;
    font-weight: 600;
    opacity: 0.5;
    font-variant-numeric: tabular-nums;
}
.eng-slot.treasure .eng-slotnum { color: rgba(255, 209, 102, 0.9); opacity: 0.8; }
.eng-slot.legal {
    cursor: pointer;
    border-color: rgba(255, 209, 102, 0.9);
    background: rgba(255, 209, 102, 0.12);
}
.eng-slot.legal .eng-slotnum { opacity: 0.9; }
.eng-slot.legal:hover, .eng-slot.legal:focus-visible {
    background: rgba(255, 209, 102, 0.26);
    outline: none;
}

/* The turn tracker and cheatsheet: one quiet line above the castles. */
.eng-turnline {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    flex-wrap: wrap;
    font-size: 12.5px;
}
.eng-turnwho { font-weight: 600; opacity: 0.9; }
.eng-pips { display: inline-flex; gap: 4px; align-items: center; }
.eng-pip {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: rgba(255, 209, 102, 0.9);
}
.eng-pip.spent {
    background: transparent;
    border: 1px solid rgba(255, 255, 255, 0.35);
}
.eng-cheat { display: inline-flex; gap: 6px; flex-wrap: wrap; }
.eng-cheatchip {
    padding: 2px 8px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.18);
    background: rgba(255, 255, 255, 0.05);
    opacity: 0.8;
}

.eng-discardrow { max-width: 560px; }

/* The result card stays narrow: one banner, one sentence, two buttons. */
.eng-summary { max-width: 460px; margin-left: auto; margin-right: auto; }

@media (max-width: 620px) {
    .eng-castles { --eng-cw: min(52px, calc((100vw - 56px) / 5)); gap: 5px; }
    .eng-fan .card + .card { margin-top: calc(var(--eng-cw) * -1.06); }
}
/* =========================================================================
   Vidrasso (vidrasso)
   Layout intent: the board is a vertical sandwich - the opponent's strip
   and strawman row on top, the trump box and the trick in the middle, the
   viewer's own strip and row just above their hand - so both public rows
   frame the trick the way they frame it at the table. Face-down strawman
   bottoms are `.card.simple.back`, and a covered bottom PEEKS out from
   under its top card (.vid-pile/.vid-under). A settled trick slides to
   its winner's strip (.vid-lasttrick) rather than lingering in the cells;
   turn state is carried by the acting seat's chip (.vid-toact), its side
   border (.vid-active), and the lit status line (.vid-turn). The bid menu
   (trump suit / trump rank buttons) is a side action column INSIDE the
   hand panel (the Schnapsen recipe), so the menu appearing or being
   settled never moves the cards.
   ========================================================================= */
/* Two lines' worth, always: the wording grows when it is explaining the
   follow obligations ("follow ♠ or play a trump..."). */
.status.vid-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.vid-status { min-height: 4.4em; }
}
/* Progress chips: round, race target, both totals, trick counter. */
.vid-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    margin-right: 8px;
    padding: 3px 10px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.18);
    border: 1px solid var(--panel-border);
    font-size: 12px;
    white-space: nowrap;
}
.vid-chip.vid-score { font-weight: 700; font-variant-numeric: tabular-nums; }
/* The acting seat's marker: the one loud chip on the board. */
.vid-chip.vid-toact {
    background: rgba(232, 112, 58, 0.22);
    border-color: rgba(232, 112, 58, 0.65);
    font-weight: 700;
}
/* The status line lights up whenever THIS device is being asked to act. */
.status.vid-status.vid-turn {
    border-left: 3px solid var(--accent);
    padding-left: 10px;
    font-weight: 600;
}
/* --- the board: their row · trump + trick · my row ----------------------- */
.vid-board {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-bottom: 12px;
}
.vid-side {
    padding: 8px 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
.vid-side.vid-mine { background: rgba(38, 120, 60, 0.12); }
/* The side whose turn it is carries the accent, on top of any tint. */
.vid-side.vid-active { border-color: rgba(232, 112, 58, 0.75); }
.vid-seathead {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 6px;
    flex-wrap: wrap;
}
.vid-seatname { font-size: 13px; font-weight: 700; }
/* The last settled trick rides its winner's strip: the pair slides here
   from the trick cells (same card ids, FLIP diff does the moving). */
.vid-lasttrick {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    margin-left: auto;
    padding: 2px 8px;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.18);
    border: 1px solid var(--panel-border);
}
.vid-lasttrick .card { --cw: 30px; }
.vid-lasttrick-label {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.6;
    white-space: nowrap;
}
.vid-lasttrick-pts {
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.vid-row { display: flex; gap: 10px; flex-wrap: wrap; }
.vid-row .card { --cw: 56px; }
/* One pile: the top card sits up-left, and a hidden partner PEEKS out
   from under its bottom-right corner - unmistakably "two cards here".
   The wrapper reserves the peek's offset so rows never jitter. */
.vid-pile {
    position: relative;
    padding: 0 8px 10px 0;
}
.vid-pile > .card { position: relative; z-index: 1; }
.vid-pile > .card.vid-under {
    position: absolute;
    top: 10px;
    left: 8px;
    z-index: 0;
    opacity: 0.9;
}
@media (max-width: 620px) {
    .vid-row .card { --cw: 44px; }
}
.vid-row .card.vid-back { opacity: 0.85; }
/* --- the center: the split bid beside the trick -------------------------- */
.vid-center {
    display: flex;
    gap: 12px;
    align-items: stretch;
    flex-wrap: wrap;
}
.vid-trumpbox,
.vid-trickbox {
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
.vid-trumpbox {
    flex: 0 1 auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
    justify-content: center;
}
.vid-trickbox { flex: 1 1 240px; }
.vid-boxtitle {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    margin-bottom: 6px;
}
.vid-trumpline {
    display: flex;
    align-items: baseline;
    gap: 8px;
    white-space: nowrap;
}
.vid-trumplabel {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    min-width: 78px;
}
.vid-trumpvalue { font-size: 14px; font-weight: 700; }
.vid-chooser {
    font-size: 11px;
    opacity: 0.6;
}
.vid-chooser::before { content: '· '; }
/* The trick: one fixed cell per seat, so nothing shifts as cards land. */
.vid-trick-grid {
    display: flex;
    gap: 18px;
    align-items: flex-start;
}
.vid-trick-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.vid-trick-cell .card { --cw: 56px; }
.vid-trick-name {
    font-size: 12px;
    font-weight: 600;
    opacity: 0.7;
    white-space: nowrap;
}
.vid-trick-name.vid-trick-turn { opacity: 1; color: var(--accent); }
/* --- trump marking and muting in the hand and rows ----------------------- */
.card.vid-trumpcard { outline: 2px solid rgba(232, 112, 58, 0.55); }
.card.vid-mute { opacity: 0.45; }
/* --- the hand panel's action column (the Schnapsen recipe) --------------- */
.vid-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    flex-wrap: wrap;
}
.vid-handrow #hand-cards { flex: 1 1 260px; }
.vid-actions {
    display: none;
    flex-direction: column;
    gap: 10px;
    max-width: 300px;
}
/* The action column only ever fills when THIS device must act, so the
   filled state may shout a little. */
.vid-actions.filled {
    display: flex;
    padding: 10px 12px;
    border-radius: 12px;
    border: 1px solid rgba(232, 112, 58, 0.55);
    background: rgba(232, 112, 58, 0.08);
}
.vid-actionnote { font-size: 13px; opacity: 0.9; font-weight: 600; }
.vid-bidgroup {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-wrap: wrap;
}
.vid-bidlabel {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    flex: 0 0 100%;
}
.vid-bidbtn { min-width: 40px; }
/* --- the round summary and the log --------------------------------------- */
.vid-summary .result-table.vid-result { margin-top: 10px; }
.vid-result .result-cell.vid-won { font-weight: 800; color: var(--accent); }
.logline.vid-logmove { opacity: 0.85; }

/* =========================================================================
   OLD MACDONALD (om-) - strawman rows, the trick, the muck race, the pick
   ========================================================================= */

/* Two lines' worth of status, always: the pick and follow explanations run
   long, and the board must not step up and down as the wording changes. */
.status.om-status, #status-line.om-status { min-height: 2.6em; }

/* --- the scoreboard line -------------------------------------------------- */

#progress-line.om-scoreboard {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    opacity: 1;
}
.om-scorename { font-weight: 600; margin-right: 5px; }
.om-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }
.om-scoretarget { opacity: 0.55; font-size: 11px; margin-left: 1px; }
.om-progress-tricks { font-size: 12px; opacity: 0.7; white-space: nowrap; }

/* The trump/direction chip. It lives BESIDE THE TRICK (.om-trickmeta), not in
   this line: it is read while looking at the two cards on the table. */
.om-pick-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    padding: 3px 10px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.3);
    border: 1px solid rgba(255, 255, 255, 0.2);
    font-size: 12.5px;
    white-space: nowrap;
}
.om-pick-chip.pending { border-color: rgba(255, 209, 102, 0.55); color: var(--accent); }
.om-trump-glyph { font-size: 15px; font-weight: 700; }
.om-trump-glyph.suit-2, .om-trump-glyph.suit-4 { color: var(--red); }
/* Per suit: 4 is Diamonds and 3 is Clubs (the Suit enum in src/shared/cards.ts). */
.om-trump-glyph.suit-4 { color: var(--red-diamond); }
.om-trump-glyph.suit-3 { color: var(--blue-club); }

/* --- the board ------------------------------------------------------------ */

.om-board {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-bottom: 12px;
}

/* One seat's strawman row: a head line, then the six piles. */
.om-strawrow {
    padding: 8px 12px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
.om-strawrow.mine { background: rgba(0, 0, 0, 0.22); }
.om-strawhead {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    margin-bottom: 6px;
}
.om-strawname { font-size: 12.5px; font-weight: 600; opacity: 0.75; }
.om-strawcount { font-size: 11.5px; opacity: 0.55; }
.om-piles {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
}
/* A pile is one card wide. A pile that still hides a card is drawn as a STACK:
   the cover shows as a second card edge offset down and to the right, behind
   the face. No text, no badge - the pile simply looks like two cards, which is
   what it is, and an uncovered pile looks like one. */
.om-pile { position: relative; padding: 0 5px 5px 0; }
.om-pile .card { --cw: 64px; position: relative; z-index: 1; }
.om-pile.om-covered::before {
    content: '';
    position: absolute;
    left: 5px;
    top: 5px;
    /* The face's own box, shifted: same width and the card aspect the sprites
       use (64 x 89), so the edge tracks the card at every --cw. */
    width: var(--cw, 64px);
    height: calc(var(--cw, 64px) * 89 / 64);
    border-radius: 7px;
    background: rgba(12, 16, 22, 0.92);
    border: 1px solid rgba(255, 255, 255, 0.28);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
    z-index: 0;
}
/* The viewer's own row sets --cw smaller on mobile, so the edge reads it from
   the pile rather than from a hard-coded number. */
.om-pile { --cw: 64px; }
/* A top just played: its cover waits, face down, for the trick to finish. */
.card.om-waiting { opacity: 0.85; }

/* A reachable card the follow rule currently forbids. Filter, never opacity:
   these sit in an overlapping row on narrow screens. */
.card.om-unplayable { filter: grayscale(0.55) brightness(0.72); }

/* --- the trick, and the facts that read it -------------------------------- */

/* ONE board panel, holding the trick and the two facts that read it: the grid
   on the left, the trump/direction chip and the muck race beside it. A second
   bordered box for the facts left a wide empty panel next to a small trick;
   inside one panel the trailing space reads like the strawman rows' does. */
.om-trickrow {
    display: flex;
    align-items: center;
    gap: 20px;
    padding: 8px 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
/* NEVER SHRINK. The grid's card column has no business narrowing under the
   staggered cards: it walked them over the seat gutter on the left. */
.om-trickbox { flex: 0 0 auto; }
.om-trickmeta {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
}
/* Seat gutter on the left (the Le Truc lesson: never caption a card with text
   whose width changes), one column of cards, one 'led' flag column. */
/* The card column carries a FLOOR, not a 1fr: the two cards are staggered by
   ±8px, so a column measured on the card alone let the led card sit on top of
   the seat name in the gutter. */
.om-trick-grid {
    display: grid;
    grid-template-columns: auto minmax(84px, auto) auto;
    align-items: center;
    column-gap: 12px;
    row-gap: 6px;
    justify-items: center;
}
.om-seat {
    justify-self: end;
    font-size: 13px;
    font-weight: 600;
    opacity: 0.7;
    white-space: nowrap;
}
.om-cell { display: flex; align-items: center; justify-content: center; }
/* Smaller than the hand: a trick is two cards being READ, not chosen from, and
   at 76px the pair pushed the strawman rows and the hand down the screen. */
.om-cell .card { --cw: 58px; flex: none; }
/* Stagger the led card left and the answer right, on the cell, so the offset
   is there before the answer lands. */
.om-cell.om-led { transform: translateX(-8px); }
.om-cell.om-follow { transform: translateX(8px); }
.om-flag {
    justify-self: start;
    font-size: 10.5px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    opacity: 0.55;
    min-width: 28px;
}
.card.om-winning { outline: 3px solid var(--accent); outline-offset: 2px; }
/* A settled trick stays on display until the next lead, faded so it cannot be
   mistaken for one still being played. */
.card.om-settled { opacity: 0.75; }
/* Reserved caption height: "Trick 12 - you to answer" and "Trick 12: AI takes
   it - pegs 18 points" must occupy the same space. */
.om-caption {
    margin-top: 6px;
    font-size: 12.5px;
    opacity: 0.85;
    text-align: center;
    line-height: 1.3;
    min-height: 1.4em;
}

/* --- the muck race (in the meta column, under the pick chip) -------------- */

/* No Pen display: three cards nobody may look at, that change nothing until
   the last trick awards them, do not earn a permanent slot. */
.om-muckline {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 3px 8px;
    font-size: 12.5px;
}
.om-muckrace { display: inline-flex; align-items: baseline; gap: 8px; }
.om-mucklabel {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
}
.om-muckchip { font-variant-numeric: tabular-nums; }
.om-muckchip.om-muckwon { color: var(--accent); font-weight: 700; }
.om-muckgoal { opacity: 0.55; font-size: 11.5px; }

/* --- the hand, and the pick column beside it ------------------------------ */

/* No reserved height: the row is as tall as what is in it. A hand shrinks from
   nine cards to none over a hand, and holding a full card's height under an
   empty tray left a band of nothing between the board and the log. The row
   cannot jump UP mid-hand either way - cards only leave it. */
.om-handrow {
    display: flex;
    align-items: flex-start;
    gap: 16px;
}
.om-handrow #hand-cards { flex: 1 1 auto; }

.om-actions {
    /* Fixed column, present even when empty: the pick buttons appearing must
       not slide the cards sideways. */
    flex: 0 0 232px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 0;
    border-radius: 12px;
    border: 1px solid transparent;
}
.om-actions.filled {
    padding: 9px 11px;
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 209, 102, 0.35);
}
.om-actions .btn { width: 100%; }
.om-actions-note { font-size: 12px; opacity: 0.8; line-height: 1.35; }

/* The pick's arithmetic: one row per suit, "yours / they show". Tabular
   numerals and fixed columns so the four rows line up as a table. */
.om-suitcounts {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 2px 10px;
    margin-bottom: 4px;
}
.om-suitcounts-head {
    grid-column: 1 / -1;
    font-size: 10.5px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
}
.om-suitcount {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
}
.om-suitcount.om-istrump {
    color: var(--accent);
    font-weight: 700;
}
.om-suitglyph { font-size: 14px; }
.om-suitglyph.suit-2, .om-suitglyph.suit-4 { color: var(--red); }
.om-suitglyph.suit-4 { color: var(--red-diamond); }
.om-suitglyph.suit-3 { color: var(--blue-club); }
.om-suitcount.om-istrump .om-suitglyph { color: inherit; }
.om-suitmine { font-weight: 700; }
.om-suitsep, .om-suitshown { opacity: 0.6; }
.om-suitnote {
    grid-column: 1 / -1;
    font-size: 11.5px;
    opacity: 0.8;
    line-height: 1.3;
}
/* Two suit buttons a row; the direction pair splits the same way. */
.om-pickrow {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 6px;
}
.om-pickrow .btn { width: 100%; padding-left: 6px; padding-right: 6px; }
.om-picksuit.suit-2, .om-picksuit.suit-4 { color: var(--red); }
.om-picksuit.suit-4 { color: var(--red-diamond); }
.om-picksuit.suit-3 { color: var(--blue-club); }
/* The hint flash lands on a BUTTON here, not only on a card (which the shared
   .card.hinted already covers), so the pick buttons need their own ring. */
.om-actions .btn.hinted {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}

@media (max-width: 760px) {
    .om-handrow { flex-wrap: wrap; }
    .om-actions { flex: 1 1 100%; }
    /* The piles stay tappable, so they keep a finger's worth of width. */
    .om-pile { --cw: 52px; }
    .om-pile .card { --cw: 52px; }
    /* The trick is READ, not tapped, so it goes under the hand's mobile size
       (the platform drops hand cards to 45px here) rather than over it. */
    .om-cell .card { --cw: 42px; }
    /* The facts drop under the trick rather than squeezing it. */
    .om-trickrow { flex-wrap: wrap; gap: 8px; }
    .om-trickbox, .om-trickmeta { flex: 1 1 100%; }
    .om-trickmeta { flex-direction: row; flex-wrap: wrap; align-items: baseline; }
    .om-trick-grid { grid-template-columns: auto minmax(64px, auto) auto; }
    /* Four suit rows across two columns is too wide at this size. */
    .om-suitcounts { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .om-suitcount { font-size: 12px; }
}

/* --- the log and the summary ---------------------------------------------- */

/* Old MacDonald's chip in the log: the Joker by its name, gold like the star. */
.cardlabel.om-joker { color: var(--accent); font-weight: 700; }

.om-summary .result-table.om-result { margin-top: 10px; }
.om-result .result-cell.om-won { font-weight: 800; color: var(--accent); }

/*
 * THE SUMMARY TABLE MUST SHOW BOTH SEATS AT EVERY WIDTH. The shared
 * .result-row is `70px repeat(auto-fit, minmax(180px, 1fr))`, so on a phone the
 * two 180px minimums cannot both fit beside the label and auto-fit drops the
 * second seat onto its own line, splitting every row in half. This game's
 * cells hold two or three digits, so they are given a minimum of 0 and allowed
 * to share whatever is there: three real columns, at any width.
 */
@media (max-width: 760px) {
    .om-result .result-row {
        grid-template-columns: minmax(74px, auto) repeat(2, minmax(0, 1fr));
        gap: 6px;
    }
    .om-result .result-label { font-size: 12px; }
    .om-result .result-cell { font-size: 13px; }
    .om-result .result-cell.total { font-size: 18px; }
}

/* =========================================================================
   Duel 52 (duel52)
   Layout intent: three lane COLUMNS side by side, each a vertical strip of
   the opponent's cards hanging from the top (Base outermost) and the
   viewer's growing from the bottom (Base outermost again), the two sides
   split by a divider - the printed table stood upright. Face-down cards
   are `.card.simple.back` buttons whose data-card-id is an identity-free
   nonce; the viewer's OWN knowledge of a face-down card is a corner chip
   (.d52-known). State wears badges rather than prose: damage tilts the
   card (.d52-hurt1/2) and brands hearts-left (.d52-hp), freeze tints blue
   with a snowflake, a pair gets a chain link, a sleeping Base a ribbon.
   Interaction is selection + glow: the selected card (.d52-sel), its legal
   targets (.d52-target), pending-power choices (.d52-choice), pair
   partners (.d52-pairable) and peekable cards (.d52-peekable) each glow
   their own color, and the action bar under the board narrates.
   ========================================================================= */
/* Two lines' worth, always: the wording grows when a power prompt or the
   peek result is being narrated. */
.status.d52-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.d52-status { min-height: 4.4em; }
}
.status.d52-status.d52-turn {
    border-color: rgba(255, 209, 102, 0.7);
    background: rgba(255, 209, 102, 0.12);
}
.d52-chip {
    display: inline-block;
    margin: 0 6px 4px 0;
    padding: 2px 9px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
    font-size: 12.5px;
    white-space: nowrap;
}
.d52-chip.d52-toact {
    border-color: rgba(255, 209, 102, 0.75);
    background: rgba(255, 209, 102, 0.16);
    font-weight: 700;
}
.d52-chip.d52-wonchip {
    border-color: rgba(126, 217, 87, 0.6);
    background: rgba(126, 217, 87, 0.14);
    font-weight: 700;
}

/* ---- piles strip: draw pile, discard fan, opponent hand, set-aside ---- */
.d52-piles {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
    margin: 8px 0 10px;
}
.d52-pile { display: flex; align-items: center; gap: 8px; }
.d52-pile .card { --cw: 34px; }
.d52-pileback.d52-spent { opacity: 0.25; }
.d52-pilelabel { font-size: 12.5px; opacity: 0.85; white-space: nowrap; }
.d52-pilelabel.d52-removed { opacity: 0.5; }
.d52-fan { display: flex; }
.d52-fan .card { --cw: 30px; }
.d52-fan .card + .card { margin-left: calc(var(--cw) * -0.55); }
.d52-oppstrip {
    padding: 3px 10px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
}

/* ---- the board: three lane columns ---- */
.d52-board {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 10px;
    margin: 6px 0 10px;
}
.d52-lane {
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.10);
    padding: 8px 8px 10px;
    min-width: 0;
}
.d52-lane.d52-lanewon { opacity: 0.75; border-color: rgba(126, 217, 87, 0.5); }
.d52-lane.d52-playable { border-color: rgba(255, 209, 102, 0.8); }
.d52-lanehead {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    margin-bottom: 6px;
}
.d52-lanename { font-size: 12.5px; font-weight: 700; opacity: 0.8; }
.d52-side {
    display: flex;
    flex-wrap: wrap;
    align-content: flex-start;
    gap: 7px;
    min-height: calc(var(--d52-cw) * 89 / 64 + 8px);
    padding: 3px 0;
}
.d52-side .card { --cw: var(--d52-cw); position: relative; overflow: visible; }
.d52-lane { --d52-cw: 54px; }
.d52-lanegap {
    height: 0;
    border-top: 2px dashed rgba(255, 255, 255, 0.22);
    margin: 4px 0;
}
.d52-myside.clickable { cursor: pointer; }
.d52-lane.d52-playable .d52-myside {
    outline: 2px dashed rgba(255, 209, 102, 0.65);
    outline-offset: 3px;
    border-radius: 8px;
}
.d52-playbtn { align-self: center; }
.d52-emptyslot { opacity: 0.5; }
.d52-cardbox { display: contents; }

/* ---- board-card state: tilt, badges, knowledge, glows ---- */
/* `rotate`, never `transform`, scoped under the board so a flight ghost (a
   clone in <body>) is not tilted (ui-conventions, "A sideways card turns with
   rotate"). */
#d52-board .d52-hurt1 { rotate: 7deg; }
#d52-board .d52-hurt2 { rotate: 15deg; }
.d52-badge {
    position: absolute;
    z-index: 2;
    font-size: 10px;
    font-weight: 700;
    line-height: 1;
    padding: 2px 4px;
    border-radius: 6px;
    pointer-events: none;
}
.d52-hp {
    left: -4px;
    bottom: -4px;
    background: #a33;
    color: #fff;
    border: 1px solid rgba(0, 0, 0, 0.4);
}
.d52-ice {
    left: -4px;
    top: -4px;
    background: rgba(140, 190, 255, 0.95);
    color: #123;
    border: 1px solid rgba(0, 0, 0, 0.3);
    font-size: 11px;
}
.d52-pairbadge {
    right: -4px;
    top: -4px;
    background: rgba(255, 209, 102, 0.95);
    color: var(--accent-ink);
    border: 1px solid rgba(0, 0, 0, 0.3);
}
/* The ability badge: the rank's power glyph in the bottom-right corner,
   echoing the printed player aid. HP sits bottom-left, ice top-left and
   the pair link top-right, so the corner is free on every card state. */
.d52-picon { display: block; width: 12px; height: 12px; }
/* SIZED FROM THE CARD, not in pixels: the badge is a child of the card,
   so --cw is that card's own width - 54/44/38px on the board, 72-96px in
   the hand. A fixed 12px was a smudge on every one of them, because the
   designers' glyphs carry real detail (the thorn ring, the axes). Roughly
   a third of the card's width, floored so a phone lane stays readable and
   capped so a desktop hand card is not all badge. */
.d52-badge.d52-power {
    right: -4px;
    bottom: -4px;
    padding: 2px;
    border-radius: 7px;
    background: rgba(16, 22, 30, 0.88);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.45);
}
.d52-badge.d52-power .d52-picon {
    width: clamp(14px, calc(var(--cw) * 0.36), 28px);
    height: clamp(14px, calc(var(--cw) * 0.36), 28px);
}
/* Hand cards carry the badge too. The tray needs the positioning context
   the board's cards already have; #hand-cards is a SHARED id, so the rule
   is scoped through the class the renderer adds. */
.d52-hand .card { position: relative; overflow: visible; }
/* Inside the known-card chip and the action bar the glyph rides the text. */
.d52-known .d52-picon {
    display: inline-block;
    /* In em, so it follows the chip's own font size down to the phone. */
    width: 1.3em;
    height: 1.3em;
    margin-left: 3px;
    vertical-align: -0.28em;
    color: #1a1a1a;
}
.d52-selinfo .d52-picon {
    display: inline-block;
    width: 14px;
    height: 14px;
    margin-right: 5px;
    vertical-align: -2px;
}
.d52-ref .d52-picon {
    display: inline-block;
    width: 14px;
    height: 14px;
    margin-right: 4px;
    vertical-align: -2px;
    color: var(--accent);
}
/* The opponent has looked at this face-down card with a 4: the 4's own eye
   in the top-right corner, and a ring while the look is from their latest
   turn. Top-right is free on every face-down card (a pair needs two face-up
   cards), so it never collides with the pair link. */
.d52-seenbadge {
    right: -5px;
    top: -5px;
    padding: 2px;
    border-radius: 7px;
    background: rgba(140, 190, 255, 0.95);
    color: #102033;
    border: 1px solid rgba(0, 0, 0, 0.35);
}
.d52-seenbadge .d52-picon {
    width: clamp(13px, calc(var(--cw) * 0.3), 18px);
    height: clamp(13px, calc(var(--cw) * 0.3), 18px);
}
/* Steady, not pulsing: the board is a full repaint, so an animation would
   restart on every action the viewer takes. */
.card.d52-seenfresh {
    outline: 3px dashed rgba(140, 190, 255, 0.95);
    outline-offset: 2px;
}
.d52-baselabel {
    left: 50%;
    bottom: 4px;
    transform: translateX(-50%);
    background: rgba(0, 0, 0, 0.55);
    color: rgba(255, 255, 255, 0.9);
    letter-spacing: 0.4px;
    white-space: nowrap;
}
.d52-frozen {
    box-shadow: 0 0 0 2px rgba(140, 190, 255, 0.75),
        0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
    filter: saturate(0.6) brightness(1.05);
}
.d52-inpair { box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.55); }
.d52-known {
    position: absolute;
    z-index: 2;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    background: #f2efe9;
    color: #1a1a1a;
    font-size: 13px;
    font-weight: 700;
    padding: 2px 5px;
    border-radius: 6px;
    border: 1px solid rgba(0, 0, 0, 0.35);
    pointer-events: none;
}
/* A card that has already attacked this turn: faded, with a tick. It is a
   fact about the acting seat's own board for the space of one turn, so the
   cards that can still swing are the ones that read as live.
   `filter` rather than `opacity`, because a card carries absolutely
   positioned badges and opacity on the parent would fade the hit-point
   brand and the ability icon to illegibility along with the art. */
.card.d52-spentcard {
    filter: grayscale(0.55) brightness(0.72);
}
.d52-spentbadge {
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    background: rgba(0, 0, 0, 0.62);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.5);
    font-size: 12px;
    padding: 1px 5px;
}
/* Selection and target glows. Each affordance has its own color so a
   board mid-twinstrike still reads: gold = selected, red = attackable,
   green = pair partner, blue = peekable, amber pulse = pending choice. */
.card.d52-sel {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}
.card.d52-target {
    outline: 3px solid rgba(255, 110, 110, 0.9);
    outline-offset: 2px;
    cursor: crosshair;
}
.card.d52-picked {
    outline: 4px solid rgba(255, 60, 60, 1);
    outline-offset: 2px;
}
.card.d52-pairable {
    outline: 3px solid rgba(126, 217, 87, 0.9);
    outline-offset: 2px;
}
.card.d52-peekable {
    outline: 3px solid rgba(140, 190, 255, 0.9);
    outline-offset: 2px;
}
.card.d52-choice {
    outline: 3px solid rgba(255, 176, 59, 0.95);
    outline-offset: 2px;
    animation: d52-pulse 1.1s ease-in-out infinite;
}
@keyframes d52-pulse {
    0%, 100% { outline-color: rgba(255, 176, 59, 0.95); }
    50% { outline-color: rgba(255, 176, 59, 0.35); }
}
body.motion-off .card.d52-choice { animation: none; }

/* ---- the action bar ---- */
.d52-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    min-height: 34px;
    margin: 2px 0 10px;
}
.d52-actions.filled {
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    padding: 7px 10px;
}
.d52-actionnote { font-size: 13px; opacity: 0.9; }
.d52-selinfo { font-size: 13px; }

/* ---- result + reference ---- */
.d52-result .result-cell.d52-won { font-weight: 700; }
.d52-ref { padding-left: 18px; }
.d52-ref li { margin: 5px 0; }
/* The icons are the designers' artwork, used with their permission. */
.d52-credit { font-size: 12px; opacity: 0.7; margin-top: 8px; }

@media (max-width: 720px) {
    .d52-lane { --d52-cw: 44px; padding: 6px 5px 8px; }
    .d52-board { gap: 6px; }
    .d52-side { gap: 6px; }
    .d52-known { font-size: 11px; }
}
@media (max-width: 460px) {
    .d52-lane { --d52-cw: 38px; }
    .d52-badge { font-size: 9px; }
}
/* =========================================================================
 * Seven Waters
 *
 * Prefix policy: every game-specific class is `sevw-` (matching `ctree-`,
 * `cascade-`, `d52-`). Shared classes are reused as-is: draftable tableau
 * cards use `.card` plus `.sevw-take`, empty slots reuse `.card.empty`,
 * face-down cards are `.card.simple.back`, buttons are `.btn`/`.controls`,
 * and the result panel reuses `.winner-banner`/`.result-actions`.
 * ========================================================================= */
/* --- the board's layout --------------------------------------------------- */
/* The goal is ONE SCREEN: the pyramid, your hand, and both parks visible at
   once on a desktop, without scrolling.

   Narrow (a phone, or the single-column page below 950px): the boxes stack in
   source order, pyramid, hand, your park, their park.

   Middle (the board at 600px to 879px): two columns, the pyramid over your
   hand on the left and the two parks stacked on the right. The hand sits in
   the pyramid's column rather than across the whole board, because two
   stacked parks are taller than the pyramid and a full-width hand row under
   both ran past the fold of a 1024x768 screen. The template has three rows
   so the columns stack independently: with two (pyramid beside your park,
   hand beside theirs) each row was as tall as its taller box, and the gaps
   under your park and under the hand put the board 87px past that fold.

   Wide (the board at 880px or more, which a 1280px desktop gives it beside
   the log): the table as the rulebook draws it, your park left of the
   pyramid, theirs right of it, the hand under all three.

   Keyed on the BOARD's width with a container query rather than on the
   viewport, because the log column takes a different share of the viewport at
   each breakpoint of the page around it. */
.sevw-board { container: sevw / inline-size; }
.sevw-grid > .sevw-tableau { margin: 0 auto 12px; }
@container sevw (min-width: 600px) {
    .sevw-grid {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(280px, 330px);
        grid-template-areas: "tableau mine" "tableau theirs" "hand theirs";
        gap: 10px 12px;
        align-items: start;
        margin-bottom: 12px;
    }
    .sevw-grid > .sevw-tableau { grid-area: tableau; margin: 0 auto; }
    .sevw-grid > .sevw-hand { grid-area: hand; margin: 0 auto; }
    /* The hand shares the pyramid's column here, so it takes the phone's
       52px cards: a full seven then needs two rows, and three to five fit
       on one. */
    .sevw-grid > .sevw-hand .cardrow { --cw: 52px; }
    .sevw-grid > .sevw-parks { display: contents; }
    .sevw-park-mine { grid-area: mine; }
    .sevw-park-theirs { grid-area: theirs; }
}
@container sevw (min-width: 880px) {
    .sevw-grid {
        grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
        grid-template-areas: "mine tableau theirs" "hand hand hand";
    }
    .sevw-grid > .sevw-hand .cardrow { --cw: 64px; }
}
/* --- the shared pyramid tableau ------------------------------------------ */
/* The rows OVERLAP vertically and are spaced generously across, which is the
   shape a pyramid of cards has on a table, and together they take about a
   fifth off the pyramid's height so the hand fits on screen beside it.
   --sevw-lap is the overlap as a share of card height (89/64 of --cw).

   0.15 IS MEASURED, NOT CHOSEN. These cards come from the simplified atlas,
   whose art is rank over suit: the rank sits in the top 45% of the cell and
   the suit pip runs from 55% to 94% of it (measured off assets/cards-simple.png).
   So an overlap much past 0.15 cuts the pip in half, and a covered card then
   reads as a rank over an arrowhead. There is no corner index to fall back on
   the way the full art has, which is the price of the flat art being legible
   at this size at all.

   The card SIZE follows the viewport height, because the constraint this
   pyramid is up against is vertical: at 1280x720 the hand has to clear the
   fold, and at 5.4vh the cards come down to 38px there and go back to 46px on
   a tall screen. */
.sevw-tableau {
    --cw: clamp(36px, 5.4vh, 46px);
    --sevw-lap: 0.15;
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    margin: 0 auto 14px;
    padding: 10px 8px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.16);
    width: fit-content;
    max-width: 100%;
}
.sevw-row { display: flex; gap: 10px; justify-content: center; }
/* Every row but the first is pulled up over the one above it. */
.sevw-row + .sevw-row {
    margin-top: calc(var(--cw) * 1.39 * var(--sevw-lap) * -1);
}
/* A row sits above the one before it, so the lower rows (the draftable ones)
   are the ones on top and nothing covers a card the player can take. */
.sevw-row { position: relative; }
.sevw-row:nth-child(2) { z-index: 1; }
.sevw-row:nth-child(3) { z-index: 2; }
.sevw-row:nth-child(4) { z-index: 3; }
.sevw-row:nth-child(5) { z-index: 4; }
.sevw-row:nth-child(6) { z-index: 5; }
.sevw-row:nth-child(7) { z-index: 6; }
.sevw-row:nth-child(8) { z-index: 7; }
.sevw-cell { display: flex; position: relative; }
/* A Joker in the pyramid, marked because the flat art cannot say so at this
   width (see the atlas note above). Top left, which is the corner the
   overlap never covers. */
.sevw-wild {
    position: absolute;
    top: 1px;
    left: 2px;
    z-index: 2;
    font-size: 10px;
    line-height: 1;
    color: #b8860b;
    text-shadow: 0 0 2px #fff, 0 0 2px #fff;
    pointer-events: none;
}
/* The help marker, in the pyramid's top corner and out of the rows' way. */
.sevw-info {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    border-radius: 9px;
    font-size: 13px;
    font-weight: 700;
    line-height: 1;
    opacity: 0.55;
    cursor: help;
}
.sevw-info:hover, .sevw-info:focus-visible { opacity: 1; color: var(--accent); }
.sevw-tableau > .sevw-info { position: absolute; top: 4px; right: 6px; z-index: 9; }
#sevw-hand-title .sevw-info { margin-left: 6px; vertical-align: baseline; }
/* Draftable cards read as active; covered ones recede; drafted slots leave a
   faint gap so the shrinking pyramid stays legible. */
.sevw-take { cursor: pointer; outline: 2px solid var(--accent); outline-offset: 1px; }
.sevw-take:hover { transform: translateY(-4px); transition: transform 120ms ease; }
.sevw-open { outline: 2px solid rgba(255, 255, 255, 0.35); outline-offset: 1px; }
.sevw-buried { opacity: 0.82; }
.sevw-cell.gone { opacity: 0.18; }
.sevw-cell.gone .card.empty { border-style: dotted; }
/* --- the two parks -------------------------------------------------------- */
.sevw-parks {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    /* Each park is as tall as its own contents. Stretched to match (the grid
       default) the shorter park carried a block of empty felt as big as the
       other park's hand row. */
    align-items: start;
    gap: 12px;
    margin-bottom: 14px;
}
.sevw-park {
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    padding: 8px 10px;
    min-width: 0;
}
.sevw-park.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
/* Everything in a park is CENTERED on the park's axis, the heading included:
   the score is a caption of the name, so the two center as one unit
   (ui-conventions.md, "Center, do not left-align"). */
.sevw-park-head {
    display: flex;
    justify-content: center;
    align-items: baseline;
    flex-wrap: wrap;
    column-gap: 10px;
    margin-bottom: 6px;
}
.sevw-park-name { font-weight: 700; white-space: nowrap; }
.sevw-park-score {
    opacity: 0.85;
    font-weight: 600;
    font-size: 13px;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
/* Rope lines, as compact chips: the sum a slide must pay, and the built
   state. The rope cards' FACES are deliberately not painted, because their
   ranks are already spent in the target, and their SUITS are counted
   park-wide in .sevw-suits rather than line by line. That is what makes four
   lines fit on one short row instead of a card-height block. */
.sevw-lines {
    display: flex;
    justify-content: center;
    gap: 6px;
    align-items: stretch;
    flex-wrap: wrap;
    margin-bottom: 8px;
}
.sevw-line {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    padding: 4px 8px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.14);
    min-width: 54px;
}
/* Built or open is carried by the chip itself rather than by a word: a solid
   green chip with a check and its points, against a dashed, dimmer chip with a
   hollow mark. Both keep the same two lines, so building one cannot change the
   chip's height and walk the row. */
.sevw-line.built {
    background: rgba(46, 125, 50, 0.3);
    border: 1px solid rgba(125, 220, 140, 0.8);
}
/* Open: a dashed edge and no fill, which reads as a slot waiting for cards
   next to a built slide's solid green. */
.sevw-line.open {
    background: rgba(0, 0, 0, 0.1);
    border: 1px dashed rgba(242, 239, 233, 0.45);
}
/* The number the slide must pay: the one thing on the chip read at a glance. */
.sevw-line-target { font-weight: 700; font-size: 17px; line-height: 1.15; }
.sevw-line-mark { font-size: 11px; font-weight: 700; line-height: 1.2; }
.sevw-line.built .sevw-line-mark { color: #9ce8a8; }
.sevw-line.open .sevw-line-mark { opacity: 0.6; font-weight: 500; }
/* The park's suit tally: one chip per suit, counting what a theme in that suit
   would score. Park-wide, so the four numbers can be compared at a glance,
   and the biggest is outlined. */
.sevw-suits {
    display: flex;
    justify-content: center;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0 0 8px;
}
.sevw-suits-label { font-size: 12px; font-weight: 600; opacity: 0.8; }
.sevw-tally {
    font-size: 13px;
    font-weight: 700;
    padding: 1px 5px;
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.16);
}
/* Each chip is INKED IN ITS SUIT, the same four colors the cards and the log
   use (platform.css's --red, --red-diamond, --blue-club). The global `.red`
   and `.club` rules are scoped to the log, so the colors are named here; the
   chips carry a `spade` class of this game's own for the fourth, since
   suitTextClass returns no modifier for the conventional ink. */
.sevw-tally.red { color: var(--red); }
.sevw-tally.red.diamond { color: var(--red-diamond); }
.sevw-tally.club { color: var(--blue-club); }
.sevw-tally.spade { color: var(--ink); }
.sevw-tally.none { opacity: 0.45; font-weight: 500; }
/* The suit a theme would pay most for. Outlined rather than recolored: the
   chip's ink is already carrying the suit. */
.sevw-tally.best { box-shadow: inset 0 0 0 1px var(--accent); }
/* The suit of YOUR face-down theme, on your own board: its number already
   counts the theme card, so it is the theme's score. Underlined, so the chip
   says which suit you bet on without a word. */
.sevw-tally.themed { text-decoration: underline 2px; text-underline-offset: 3px; }
/* River, pool, and theme zones. All three are the same height (a 46px card's,
   64px), so the row does not jump as a zone fills. */
.sevw-zones {
    --sevw-zh: calc(46px * 1.39);
    display: flex;
    justify-content: center;
    gap: 10px;
    flex-wrap: wrap;
    align-items: flex-start;
}
.sevw-zone {
    min-width: 54px;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.sevw-zone-title {
    font-size: 12px;
    font-weight: 600;
    opacity: 0.85;
    margin-bottom: 4px;
    white-space: nowrap;
    text-align: center;
}
.sevw-riverrow { display: flex; justify-content: center; }
/* The lazy river as its two END VALUES ("5–9"), the only part of a run you
   play onto. Card-sized, so it lines up with the pool and theme beside it. */
.sevw-river-ends {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 46px;
    height: var(--sevw-zh);
    padding: 0 6px;
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.92);
    color: #1c2733;
    font-size: 18px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.sevw-poolrow { display: flex; justify-content: center; }
.sevw-poolrow .card + .card { margin-left: -32px; }
.sevw-poolrow .card.empty, .sevw-riverrow .card.empty { --cw: 46px; opacity: 0.5; }
/* The other seat's hand: its public cards face up, a back for each secret
   one, SIDE BY SIDE. The simplified art puts the rank in the middle of the
   card, so an overlapped row hid the one thing a public card is there for.
   Small enough (32px, inside the always-simplified band) that seven still fit
   a park on one row at the wide layout's narrowest. */
/* Its cards are shorter than the zone cards beside it, so they sit at the
   bottom of the row and line up with the pool's and theme's bottom edge. */
.sevw-zone-opphand { align-self: stretch; }
.sevw-zone-opphand .sevw-opphand { margin-top: auto; }
.sevw-opphand {
    --cw: 32px;
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 4px;
}
.sevw-opphand .card.empty { opacity: 0.5; }
/* Any board box that explains itself when tapped (wireHelp). The cursor and
   the hover edge are the whole affordance: it arms nothing, so it must not
   read as a control that plays a card. */
.sevw-explains { cursor: help; border-radius: 8px; }
.sevw-explains:hover, .sevw-explains:focus-visible {
    outline: 1px solid var(--panel-border);
    outline-offset: 3px;
}
/* --- the help card -------------------------------------------------------- */
.sevw-help {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    background: rgba(0, 0, 0, 0.55);
}
.sevw-help-card {
    /* OPAQUE, the way the platform's own lifted panels do it: --panel is
       rgba(255,255,255,0.07), so laid straight over the board the card's text
       sat on top of the park showing through it. */
    background: linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    padding: 14px 16px;
    max-width: 420px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
}
.sevw-help-title { margin: 0 0 8px; text-align: center; }
.sevw-help-line { margin: 0 0 8px; font-size: 14px; line-height: 1.5; }
/* --- the private hand and action bar ------------------------------------- */
/* A TIGHT box: the shared .panel padding, the panel-head's 10px under its
   title, and .controls' 12px above and below an action row that is empty
   until cards are selected added up to about 60px of nothing around three
   cards. */
/* As wide as a FULL hand of seven, and no wider, so the box does not stretch
   across the whole board around three cards, and it is the same width at
   every hand size, so its edges do not move as the hand grows. 7 x 64px cards
   plus six 8px gaps (.cardrow) plus 24px of padding is 520px; the action
   buttons wrap inside it. */
.sevw-grid > .sevw-hand {
    padding: 8px 12px;
    width: min(100%, 520px);
    box-sizing: border-box;
}
.sevw-hand .panel-head h3 { margin: 0 0 6px; }
/* Centered, like every hand in the collection. The platform rule names
   #hand-cards and #opp-cards, which this game's own id is not. */
.sevw-hand .cardrow { --cw: 64px; min-height: 30px; justify-content: center; }
/* Only a hand the player can act on is pressable: during the other seat's
   turn the same cards are painted as plain divs, and must not look live. */
button.sevw-handcard { cursor: pointer; }
button.sevw-handcard:hover { transform: translateY(-3px); transition: transform 120ms ease; }
.sevw-handcard.sel {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    transform: translateY(-8px);
}
.sevw-actions { margin: 8px 0 0; }
.sevw-actions:empty { margin: 0; }
/* The Bot-level option steers the THINKING bot only; picking the flip-deck
   solo bot dims it (still clickable - the choice is remembered for the
   duel). Set by initSevenWatersSetup on the option group. */
.opt-group.sevw-inert { opacity: 0.45; }
/* --- the result panel ----------------------------------------------------- */
.sevw-result-table {
    display: grid;
    grid-template-columns: minmax(150px, auto) 1fr 1fr;
    gap: 4px 12px;
    margin: 12px auto;
    max-width: 460px;
}
.sevw-result-row { display: contents; }
.sevw-result-label { opacity: 0.85; font-size: 14px; }
.sevw-result-cell { text-align: center; font-weight: 600; font-size: 14px; }
.sevw-result-row.header .sevw-result-cell { font-weight: 700; font-size: 15px; }
.sevw-result-row.totals .sevw-result-label,
.sevw-result-row.totals .sevw-result-cell {
    border-top: 1px solid var(--panel-border);
    padding-top: 6px;
    font-weight: 700;
    font-size: 16px;
}
.sevw-result-themes {
    display: flex;
    justify-content: center;
    gap: 24px;
    margin: 10px 0;
}
.sevw-result-theme { display: flex; flex-direction: column; align-items: center; gap: 4px; }
@media (max-width: 460px) {
    /* A phone keeps the overlap and most of the horizontal air: seven cards
       plus six gaps have to fit the width, so the gap comes down with the
       card rather than away. */
    .sevw-tableau { --cw: 38px; --sevw-lap: 0.16; padding: 8px 5px; }
    .sevw-row { gap: 6px; }
    .sevw-hand .cardrow { --cw: 52px; }
    .sevw-lines { gap: 4px; }
    .sevw-line { min-width: 46px; padding: 3px 6px; }
    .sevw-line-target { font-size: 15px; }
    .sevw-suits { gap: 4px; }
    .sevw-suits-label { font-size: 11px; }
}
/* =========================================================================
 * Blackjack River Research (riverresearch)
 *
 * Prefix policy: every game-specific class is `brr-`. Shared classes are
 * reused as-is - cards are `.card`, a catchable School adds `.lit`, hand cards
 * add `.sel`, buttons are `.btn`, and the result panel reuses
 * `.winner-banner` / `.result-note` / `.result-actions`.
 *
 * The whole game is the question "is this column exactly 21?", so the layout
 * is built around four running totals that have to be legible at a glance, and
 * around a preview of what the card you picked up would make them.
 * ========================================================================= */

/* --- the river: four population columns ---------------------------------- */
.brr-river {
    --cw: 42px;
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin: 6px 0 14px;
}
.brr-col {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 6px 4px 8px;
    border-radius: 10px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
}
.brr-col.ready { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.brr-col.hinted { border-color: #ffd166; box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.55); }
.brr-head { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.brr-colname { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.7; }

/* The running total, the one number the game turns on. */
.brr-total {
    display: flex;
    align-items: baseline;
    gap: 6px;
    font-size: 26px;
    font-weight: 700;
    line-height: 1.05;
    font-variant-numeric: tabular-nums;
}
.brr-total.ready { color: var(--accent); }
.brr-total.over { color: #e8a33d; }
.brr-total.violation { color: #e05d5d; }
/* "what this click would make it" - never shown without a live target. */
.brr-preview {
    font-size: 14px;
    font-weight: 700;
    opacity: 0.9;
    padding: 0 4px;
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.08);
}
.brr-preview.ready { color: #1c2733; background: var(--accent); opacity: 1; }
.brr-preview.over { color: #e8a33d; }
.brr-preview.violation { color: #fff; background: #b03a3a; opacity: 1; }
.brr-flag {
    font-size: 11px;
    font-weight: 600;
    text-align: center;
    color: var(--accent);
    line-height: 1.2;
}
.brr-flag.over { color: #e8a33d; }

/* --- a School of Fish, with the Fisherman lying across it ---------------- */
/* The row is wider than a card because a Fisherman is laid SIDEWAYS: rotating
   a card about its center gives a footprint 1.39 times as wide, and it is shifted
   right so the School's own rank corner stays readable. 1.9x --cw covers both. */
.brr-stack {
    display: flex;
    flex-direction: column;
    gap: 3px;
    width: calc(var(--cw) * 1.9);
}
.brr-school {
    position: relative;
    width: 100%;
    height: calc(var(--cw) * 89 / 64);
}
.brr-school .card.brr-fish {
    position: absolute;
    left: 0;
    top: 0;
}
.brr-school .card.brr-fish.lit {
    cursor: pointer;
    outline: 3px solid var(--accent);
    outline-offset: 1px;
}
.brr-school .card.brr-fisherman {
    position: absolute;
    left: calc(var(--cw) * 0.645);
    top: 0;
    /* `rotate`, never `transform` (ui-conventions, "A sideways card turns
       with rotate"). */
    rotate: 90deg;
    box-shadow: -0.15em 0.05em 0.35em rgba(0, 0, 0, 0.5);
    pointer-events: none;
}
/* What a WORKED School has left. Only a worked School carries a number: an
   unworked one's rank already says how many fish it holds, so a badge there
   would only repeat the card. */
.brr-caught {
    position: absolute;
    left: 1px;
    bottom: 1px;
    min-width: 16px;
    text-align: center;
    font-size: 12px;
    font-weight: 800;
    border-radius: 5px;
    padding: 0 3px;
    background: #b03a3a;
    color: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.brr-school .brr-preview {
    position: absolute;
    right: 0;
    bottom: 2px;
}
.brr-gap {
    font-size: 12px;
    text-align: center;
    opacity: 0.6;
    padding: 14px 2px;
    border: 1px dashed var(--panel-border);
    border-radius: 8px;
}

/* --- the per-column action foot ------------------------------------------ */
.brr-foot { min-height: 26px; display: flex; justify-content: center; }
.brr-foot .btn.small { font-size: 12px; padding: 3px 7px; }
.brr-blocked {
    font-size: 11px;
    text-align: center;
    opacity: 0.65;
    line-height: 1.2;
    align-self: center;
}

/* --- hand, actions ------------------------------------------------------- */
.brr-hand .brr-handrow { --cw: 62px; min-height: 34px; align-items: center; gap: 8px; }
.brr-handcard { cursor: pointer; }
.brr-handcard:hover { transform: translateY(-3px); transition: transform 120ms ease; }
.brr-handcard.sel {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    transform: translateY(-8px);
}
/* Fishermen read differently from Schools even before you know the ranks. */
.brr-handcard.brr-rod { box-shadow: 0 0 0 2px #e8a33d, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
.brr-legend { font-size: 12px; opacity: 0.75; margin-top: 8px; }
.brr-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin: 10px 0 14px;
}
.brr-note { font-size: 12px; opacity: 0.8; max-width: 46ch; line-height: 1.35; }

/* --- completed Studies -------------------------------------------------- */
.brr-studies-row { display: flex; flex-wrap: wrap; gap: 10px; }
.brr-study {
    --cw: 26px;
    padding: 5px 7px 6px;
    border-radius: 8px;
    border: 1px solid var(--panel-border);
    background: rgba(255, 255, 255, 0.04);
}
.brr-studyhead { font-size: 11px; font-weight: 700; opacity: 0.8; margin-bottom: 3px; }
.brr-studyrow { display: flex; }
.brr-studyrow .card + .card { margin-left: -13px; }
.brr-studycard { box-shadow: 0.05em 0.1em 0.2em rgba(0, 0, 0, 0.45); }
.brr-studyrod { align-self: flex-end; }
.brr-studymarks { font-size: 10px; opacity: 0.8; margin-top: 3px; max-width: 22ch; line-height: 1.25; }

/* --- the Breakthrough checklist ------------------------------------------ */
.brr-marks {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 7px;
}
.brr-mark {
    padding: 5px 9px;
    border-radius: 8px;
    border: 1px solid var(--panel-border);
    background: rgba(255, 255, 255, 0.03);
    opacity: 0.6;
}
.brr-mark.got {
    opacity: 1;
    border-color: var(--accent);
    background: rgba(255, 255, 255, 0.07);
}
.brr-markname { display: block; font-size: 13px; font-weight: 700; }
.brr-mark.got .brr-markname::after { content: ' \2713'; color: var(--accent); }
.brr-markrule { display: block; font-size: 11px; opacity: 0.8; line-height: 1.3; }

/* Concluding ends the game and cannot be undone, so its armed state is red.
   (There is no shared .btn.danger; every game that needs one prefixes it.) */
.btn.brr-danger {
    background: #b03a3a;
    border-color: #b03a3a;
    color: #fff;
}
.btn.brr-danger:hover:not(:disabled) { background: #9a2a2a; }

/* --- the log ------------------------------------------------------------ */
.logline.brr-logstudy { font-weight: 600; color: var(--accent); }
.logline.brr-logviolation { font-weight: 600; color: #e05d5d; }

@media (max-width: 720px) {
    .brr-river { --cw: 34px; gap: 6px; }
    .brr-total { font-size: 22px; }
    .brr-hand .brr-handrow { --cw: 52px; }
}
@media (max-width: 460px) {
    .brr-river { --cw: 27px; gap: 4px; }
    .brr-col { padding: 5px 2px 6px; }
    .brr-colname { font-size: 9px; letter-spacing: 0.03em; }
    .brr-total { font-size: 19px; gap: 3px; }
    .brr-flag { font-size: 9px; }
    .brr-hand .brr-handrow { --cw: 46px; }
}
/* =========================================================================
 * Deck Express: Southern Railway (deckexpress)
 *
 * Prefix policy: every game-specific class is `dx-`. Shared classes are
 * reused as-is - cards are `.card` (`.mini` on the table), hand cards select
 * with `.card.sel` and arm with `.clickable`, buttons are `.btn`, and the
 * result panel reuses `.winner-banner` / `.result-note` / `.result-actions`.
 *
 * The table is four company columns (value card, share offering, track) over
 * the train yard line, then the seat panels, then the action row above the
 * hand. Money is a card COUNT, so the seat panels lead with it.
 * ========================================================================= */
.dx-board { display: flex; flex-direction: column; gap: 12px; }

/* --- the market: four company columns ------------------------------------ */
.dx-companies {
    display: grid;
    grid-template-columns: repeat(4, minmax(120px, 1fr));
    gap: 10px;
}
@media (max-width: 700px) {
    .dx-companies { grid-template-columns: repeat(2, minmax(120px, 1fr)); }
}
.dx-company {
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.03);
    padding: 8px 9px 9px;
    display: flex;
    flex-direction: column;
    gap: 7px;
}
.dx-company-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    min-height: 26px;
}
.dx-company-name { font-weight: 800; font-size: 14px; }
.dx-loco-chip {
    font-size: 11px;
    font-weight: 700;
    padding: 2px 7px;
    border-radius: 999px;
    border: 1px solid var(--accent);
    white-space: nowrap;
}
.dx-zone { display: flex; align-items: flex-start; gap: 7px; }
.dx-zone-label {
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.65;
    min-width: 52px;
    padding-top: 4px;
}
.dx-none { font-size: 11px; opacity: 0.55; padding-top: 4px; }
/* Offering and track fan tightly; every card stays identifiable. */
.dx-offer .card + .card, .dx-track .card + .card { margin-left: -14px; }
.dx-offer, .dx-track { display: flex; flex-wrap: wrap; }

/* --- the train yard line -------------------------------------------------- */
.dx-yardline {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 10px;
}

/* --- seat panels ----------------------------------------------------------- */
.dx-players {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: 10px;
}
.dx-seat { padding: 8px 10px 9px; }
.dx-seat.dx-active { border-color: var(--accent); }
.dx-seat-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 5px;
}
.dx-seat-name { font-weight: 800; }
.dx-seat-money { font-weight: 700; font-size: 13px; }
.dx-seat-body {
    display: flex;
    flex-direction: column;
    gap: 5px;
    align-items: flex-start;
}
.dx-shares .card + .card { margin-left: -14px; }
.dx-chip {
    font-size: 11px;
    padding: 2px 7px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: rgba(255, 255, 255, 0.04);
    opacity: 0.85;
}

/* --- actions, hand --------------------------------------------------------- */
/* Reserve a row so the action buttons appearing never shifts the hand. */
.dx-actions { min-height: 38px; align-items: center; }
.dx-note { font-size: 12px; opacity: 0.8; max-width: 52ch; line-height: 1.35; }
.dx-hint { border-left: 2px solid var(--accent); padding-left: 8px; }
/* Build mode fades the cards that cannot be track (filter, never opacity:
 * the hand row wraps, but a faded overlap must not bleed neighbors). */
#hand-cards .card.dx-dim { filter: grayscale(0.8) brightness(0.55); }

/* --- final standings -------------------------------------------------------- */
.dx-final { display: flex; flex-direction: column; gap: 4px; margin: 10px 0; }
.dx-final-row {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 4px 8px;
    border-radius: 7px;
    background: rgba(255, 255, 255, 0.03);
}
.dx-final-winner { background: rgba(255, 255, 255, 0.09); font-weight: 700; }

/* ==========================================================================
   Solatro
   (src/solatro/, pages/solatro.mjs)
   ==========================================================================
   Solo poker roguelike. Every shared class (.card, .cardrow, .mini, .panel,
   .pile-chip, .logline, .winner-banner, .btn, .sel, .clickable, .controls)
   is reused as-is; only what is genuinely this game's own lives here, all of
   it prefixed `sol-`. The layout reads top to bottom the way a round does:
   the round banner (target, meter, uses, tokens), the plays already scored,
   the hand with its preview and actions, then the shoppe between rounds. */

/* Centered as a block (the Cuttle treatment): the column keeps its fixed cap,
   so it cannot re-center mid-run; it just sits on the page's axis instead of
   hugging the left edge. */
.sol-board { display: flex; flex-direction: column; gap: 10px; max-width: 520px; margin-inline: auto; }
.sol-round-line { display: flex; align-items: baseline; gap: 12px; }
.sol-round-name { font-size: 17px; font-weight: 700; }
.sol-round-target { font-size: 13.5px; opacity: 0.85; }

/* The score meter: the round in one strip. The target tick is the right edge. */
.sol-meter {
    position: relative; height: 18px; border-radius: 9px; overflow: hidden;
    background: rgba(0, 0, 0, 0.35);
    border: 1px solid rgba(255, 255, 255, 0.15);
}
.sol-meter-fill {
    height: 100%; border-radius: 9px;
    background: linear-gradient(90deg, #7a5a2a, #b98a3c);
    transition: width 250ms ease;
}
.sol-meter-fill.beaten { background: linear-gradient(90deg, #2e7d46, #58b06e); }
.sol-meter-label {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    font-size: 12px; font-weight: 600; letter-spacing: 0.04em;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
}

.sol-uses { display: flex; flex-wrap: wrap; gap: 16px; align-items: baseline; }
.sol-pips { font-size: 13px; letter-spacing: 0.12em; }
.sol-uses-note { font-size: 12px; opacity: 0.7; }

/* The Joker's collision rule, warned about while it can still bite. */
.sol-warn {
    font-size: 12.5px; line-height: 1.4; padding: 6px 10px; border-radius: 10px;
    color: #f2d694; background: rgba(185, 138, 60, 0.18);
    border: 1px solid rgba(224, 178, 60, 0.45);
}
.sol-warn.lost {
    color: #f7c3c3; background: rgba(168, 50, 50, 0.22);
    border-color: rgba(212, 80, 80, 0.5);
}

/* Owned tokens: chips with their Use buttons. */
.sol-tokens { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.sol-token {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 3px 10px; border-radius: 999px; font-size: 12.5px;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.18);
}
.sol-token.armed {
    color: #f2d694; background: rgba(185, 138, 60, 0.22);
    border-color: rgba(224, 178, 60, 0.5); font-weight: 700;
}
.sol-token-note, .sol-token-none { font-size: 12px; opacity: 0.75; }

/* One scored play: number, mini cards, name and points. */
.sol-play { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 6px 0; }
.sol-play-n { font-size: 12.5px; opacity: 0.7; min-width: 1.2em; }
.sol-play-name { font-size: 13.5px; }

/* The banked Joker sits beside the hand with its little caption. */
.sol-joker-slot {
    display: inline-flex; flex-direction: column; align-items: center; gap: 2px;
    margin-left: 14px;
}
.sol-joker-note {
    font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
    opacity: 0.75;
}
/* The risk, on the card itself: the warning above can scroll out of view. */
.sol-joker-risk {
    font-size: 10.5px; max-width: 9em; text-align: center; line-height: 1.25;
    color: #f2d694;
}

/* What the selection would score, before it is committed. Reserve the line
   so picking cards never shifts the buttons below it. */
.sol-preview { font-size: 13.5px; margin-top: 10px; min-height: 1.4em; }

.sol-credit { font-size: 14px; font-weight: 600; margin-bottom: 8px; }
.sol-shop-item { display: flex; align-items: center; gap: 10px; margin: 6px 0; }
.sol-shop-item .btn { flex-shrink: 0; }
.sol-shop-name { font-weight: 600; font-size: 13.5px; min-width: 8.5em; }
.sol-shop-effect { font-size: 12.5px; opacity: 0.8; }
/* A purchase that can still be handed back reads as provisional. */
.sol-shop-effect.bought { color: #b7f7c3; opacity: 1; }

/* The milestone ladder on the result screen. */
.sol-ladder { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; }
.sol-rank {
    padding: 3px 10px; border-radius: 999px; font-size: 12px;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.14);
    opacity: 0.65;
}
.sol-rank.reached {
    opacity: 1; color: #b7f7c3;
    background: rgba(38, 120, 60, 0.25);
    border-color: rgba(120, 220, 140, 0.4);
}

.sol-result-line { font-size: 14px; margin: 8px 0; }
.sol-note { font-size: 13px; opacity: 0.85; }

/* The Cheat_Sheet's tables (pages/solatro.mjs): the shoppe's tokens and the
   result ranks. Scoped to their own class, so they style the Rules_Summary and
   the Rules_Page alike. The hand points are in the Hand rankings list, which
   the shared .reflist rules style. */
.sol-table { border-collapse: collapse; margin: 6px 0 10px; }
.sol-table th, .sol-table td {
    text-align: left; vertical-align: baseline; padding: 2px 14px 2px 0;
}
.sol-table th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.75; }
.sol-table .sol-num { text-align: right; font-variant-numeric: tabular-nums; }

@media (max-width: 560px) {
    .sol-shop-name { min-width: 0; }
    .sol-shop-item { flex-wrap: wrap; }
    .sol-hand .controls .btn { flex: 1 1 45%; }
}

/* =========================================================================
   Cuttle (cuttle)
   Layout intent: the opponent's AREA on top (hand backs, Royals, point
   stacks), a CENTER strip (deck, browsable scrap, the 7's reveal, the
   counter chain), the viewer's area beneath, then the action bar and the
   hand tray. Point stacks wear their Jacks fanned across the top-right
   corner (--jack-i sets the offset) with a ring + badge when stolen;
   Glasses Eights lie sideways. Interaction is selection + glow: the
   selected hand card (.ctl-sel), legal targets (.ctl-target), and
   everything else fades (.ctl-faded) while a pick is armed. The score
   chips carry the race: points made OUT OF the King-reduced goal.
   ========================================================================= */

.ctl-status.ctl-turn { color: var(--accent); font-weight: 600; }

.ctl-chip {
    display: inline-block; padding: 2px 8px; margin-right: 6px;
    border: 1px solid rgba(255,255,255,0.18); border-radius: 999px;
    font-size: 0.82rem; opacity: 0.9;
}
.ctl-chip.ctl-toact { border-color: var(--accent); color: var(--accent); font-weight: 700; }
.ctl-chip.ctl-passchip { border-color: #e8a03a; color: #e8a03a; }
.ctl-chip.ctl-seenchip { border-color: #ffd166; color: #ffd166; }

.ctl-board { display: flex; flex-direction: column; gap: 10px; margin: 10px 0; }

.ctl-area {
    border: 1px solid rgba(255,255,255,0.12); border-radius: 10px;
    padding: 8px 10px; display: flex; flex-direction: column; gap: 8px;
}
.ctl-area.ctl-acting { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
/* CENTERED BOARD. Every row sits on one axis - each player's heading, the
   opponent's hand, both fields, the deck and scrap piles, the action bar
   and the hand tray - so the table reads as a table rather than as a
   left-aligned list.
   The known cost: a row RE-CENTERS when its card count changes, so cards
   already down shift by half a card width when one is added. The
   CONTAINERS still never move, which is what the reserved-space rules are
   protecting - .ctl-actions keeps its min-height, .ctl-center keeps its
   perch headroom, and the hand tray keeps its eight-card cap and is
   centered as a block (margin-inline: auto) rather than sized to content. */
.ctl-areahead {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    justify-content: center;
}
.ctl-areaname { font-weight: 700; }
.ctl-score {
    font-weight: 800; font-size: 1.05rem; padding: 1px 10px;
    border: 1px solid rgba(255,255,255,0.25); border-radius: 8px;
}
.ctl-area.ctl-acting .ctl-score { border-color: var(--accent); }

/* RESERVED CARD ROWS. Both of these rows are empty at the start of a game
   and hold cards later, and a card is tall: measured over a real game, the
   opponent's field grew from 19px (just the "nothing in play" note) to
   84px, and that pushed the center row, BOTH play areas, the action bar
   and the hand tray down by 65px. So each row reserves ONE CARD ROW at its
   own card size, permanently, and the note sits centered in the space it
   will eventually occupy. Card height is width x 89/64 (see .card), which
   is why the reservation is expressed as a calc rather than a magic px. */
.ctl-handrow {
    display: flex; gap: 4px; flex-wrap: wrap; justify-content: center;
    --ctl-hand-cw: 34px;
    min-height: calc(var(--ctl-hand-cw) * 89 / 64);
}
.ctl-handrow .card { --cw: var(--ctl-hand-cw); }

.ctl-field {
    display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-start;
    justify-content: center;
    --ctl-field-cw: 56px;
    /* A Jack fans up out of its stack (.ctl-stack .ctl-jack is absolutely
       positioned, so it adds no height) and the "stolen" badge sits above
       the card, so the reservation carries a little headroom over the bare
       card height. */
    min-height: calc(var(--ctl-field-cw) * 89 / 64 + 6px);
}
.ctl-stacks, .ctl-royals {
    display: flex; gap: 12px; flex-wrap: wrap; justify-content: center;
}
.ctl-field .card { --cw: var(--ctl-field-cw); }
.ctl-note { opacity: 0.6; font-size: 0.85rem; align-self: center; }

/* A point stack: the base card with Jacks fanned over its corner. */
.ctl-stack { position: relative; }
.ctl-stack .ctl-jack {
    --cw: 34px;
    position: absolute;
    top: calc(-8px * var(--jack-i, 1));
    right: calc(-14px + -8px * var(--jack-i, 1));
    /* `rotate`, never `transform`: see .ctl-glasses below. */
    rotate: 14deg;
    box-shadow: -2px 2px 4px rgba(0,0,0,0.4);
}
.ctl-stack.ctl-stolen > .card:first-child { outline: 2px solid #e8703a; outline-offset: 1px; }
.ctl-badge {
    position: absolute; left: -6px; top: -6px; z-index: 2;
    background: #e8703a; color: #1a1a1a; font-size: 0.62rem; font-weight: 800;
    letter-spacing: 0.03em; text-transform: uppercase;
    border-radius: 999px; padding: 1px 6px; pointer-events: none;
}

/* Glasses Eights lie sideways, as printed. */
/* Glasses Eights lie sideways, as printed - via the INDEPENDENT `rotate`
   property, not `transform`. A flying ghost gets an inline
   `transform: translate(...)` from ui/animate.ts, which would replace a
   `transform: rotate(90deg)` outright: the ghost then animated from 90deg
   to 0deg, so the Glasses appeared to spin every time any card moved.
   `rotate` composes with `transform` instead of being overwritten. */
.card.ctl-glasses { rotate: 90deg; margin: 0 12px; }

/* The top padding is RESERVED for the known-deck-top perch, which
   overhangs the deck pile. Reserving it always (rather than adding a
   margin when a perch appears) keeps the piles from lurching down the
   moment a 7 puts a card back. It was 26px while the perch floated above
   the pile; the perch now sits ON the pile and overhangs by about 8px
   (see .ctl-deckperch), so 10px covers it. */
.ctl-center {
    display: flex; gap: 16px; align-items: center; flex-wrap: wrap;
    justify-content: center; padding: 10px 2px 2px;
    /* The anchor for the opened scrap, which lies over this strip. */
    position: relative;
    /* ...and the row's HEIGHT is reserved for its tallest occupant, the
       Seven's staged pair (the deck and scrap piles are 40px). A 7 or a
       counter chain arriving used to grow this row and shove both play
       areas, the action bar and the tray down with it. The staged pair is
       48px now, the counter chain's size, so the reservation fits both. */
    --ctl-staged-cw: 48px;
    min-height: calc(var(--ctl-staged-cw) * 89 / 64 + 12px);
}
.ctl-pile { display: flex; align-items: center; gap: 8px; }
.ctl-pile .card { --cw: 40px; }
.ctl-pile .card.ctl-spent { opacity: 0.35; }
.ctl-pilelabel { font-size: 0.85rem; opacity: 0.8; }
.ctl-staged { display: flex; align-items: center; gap: 8px; }
.ctl-staged .card { --cw: var(--ctl-staged-cw); }
.ctl-chain { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ctl-chain .card { --cw: 48px; }
.ctl-chainarrow { font-size: 1.2rem; opacity: 0.8; }

/* THE OPENED SCRAP LIES OVER THE DECK AND SCRAP STRIP, not below it.
   Below it, opening the pile (or picking for a 3) pushed your own field, the
   action bar and the hand down by several card rows, often out of the
   viewport, and pulled them back up when it closed. Laid over the strip it
   moves nothing.

   IT GROWS UPWARD, over the opponent's field, from the strip's bottom edge.
   It used to hang down from the top edge, and a long pile reached past your
   own field onto the action bar: late in a game, selecting a 3 opened the
   picker over the "Play for 3 points" button and your field, so the 3 could
   only be played as a one-off (reported; measured at 1280px and 390px). The
   cards below the strip are the ones a selected card is played with, so
   those stay uncovered. It scrolls rather than growing past a sensible
   height. */
.ctl-scrapbrowser {
    position: absolute; left: 0; right: 0; bottom: 0; z-index: 5;
    border: 1px solid rgba(255,255,255,0.28); border-radius: 8px;
    padding: 8px 10px;
    background: var(--felt-dark);
    box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.55);
    max-height: min(45vh, 340px); overflow-y: auto;
}
.ctl-scraphead {
    display: flex; align-items: center; justify-content: center;
    gap: 8px; flex-wrap: wrap;
}
/* "Just discarded": cards shed since the turn began, shown face-up beside
   the piles. Only the TOP of the scrap is visible otherwise, so a two-card
   strip used to hide half of itself. */
.ctl-justshed {
    border: 1px dashed rgba(240, 192, 90, 0.45);
    border-radius: 8px; padding: 4px 8px;
}
.ctl-justshed .ctl-pilelabel { color: #f0c05a; }
/* JUST PLAYED sits beside the scrap and reads as the news of the turn, so
   it takes the accent rather than the discard row's amber. Both of these
   share the center row's already-reserved HEIGHT (their cards are the
   piles' 40px, about 66px with the box's padding and border, and the
   staged pair the row reserves for is 67px), so they cost no vertical
   movement - they do re-center the piles horizontally while they show,
   which is the one place that is worth it. */
.ctl-justplayed {
    border: 1px solid rgba(122, 184, 217, 0.5);
    border-radius: 8px; padding: 4px 8px;
}
.ctl-justplayed .ctl-pilelabel { color: #7ab8d9; }
/* A finished counter chain (def.ts recentChain) is the same row with each
   2 after an arrow and the name of the seat that threw it. Same 40px cards,
   so the row's reserved height holds; nowrap so a label never stacks. */
.ctl-justchain { flex-wrap: wrap; justify-content: center; }
.ctl-justchain .ctl-pilelabel { white-space: nowrap; }
/* JUST SCUTTLED (def.ts recentScuttle): the same box as JUST PLAYED, in the
   coral of a card that went, so a scuttle reads as a loss rather than as a
   play. Same 40px cards, so it shares the row's reserved height too. */
.ctl-justscuttled {
    border: 1px solid rgba(239, 111, 108, 0.55);
    border-radius: 8px; padding: 4px 8px;
}
.ctl-justscuttled .ctl-pilelabel { color: #ef6f6c; white-space: nowrap; }
.ctl-scrapcards {
    display: flex; gap: 5px; flex-wrap: wrap; justify-content: center;
    margin-top: 6px;
}
/* The scrap is public information and often decides a 3: read it at a
   comfortable size rather than as a row of thumbnails. */
.ctl-scrapcards .card { --cw: 52px; }

/* SPACE IS RESERVED, not allocated on demand: this bar changes content on
   every turn (nothing / a draw button / buttons plus a wrapped power line),
   and sizing to its content made the whole board jump each time. The
   min-height fits a row of buttons plus one wrapped note line, and applies
   in the empty state too, so the layout below never moves. */
.ctl-actions {
    /* 92px, arrived at by measurement rather than taste. The bar holds a
       row of buttons plus the selected card's POWER TEXT, and the longest
       of those (the 2's "scrap or counter", the Queen's shield) wraps to
       TWO lines while most wrap to one - so a single-line reservation let
       the bar swing 72px -> 91px and shove the hand tray down 19px every
       time you selected one of those cards. 92px fits the two-line case,
       and .ctl-actionnote below reserves the second line so the one-line
       case fills the same box instead of leaving a gap. */
    min-height: 92px; display: flex; align-items: center; gap: 8px;
    flex-wrap: wrap; justify-content: center; align-content: center;
}
/* Buttons come BEFORE any explanatory text, so they stay the nearest
   thing to the hand you just tapped - which is why they are centered with
   everything else rather than pinned left: the hand is centered now, so
   the center is what "near your cards" means. Done with flex `order`
   rather than DOM order so the reading order stays context-then-action
   for screen readers. */
.ctl-actions .btn { order: -1; }
.ctl-actions.filled {
    border: 1px solid rgba(255,255,255,0.15); border-radius: 8px;
    padding: 8px 10px;
}
/* TWO LINES, always: the power texts are one or two lines depending on the
   card, and letting the taller ones grow the bar moved everything below.
   Reserved here as well as on .ctl-actions so a one-line note still
   occupies the two-line box rather than sitting the buttons higher. */
.ctl-actionnote {
    opacity: 0.85; font-size: 0.9rem;
    min-height: 2.6em; display: flex; align-items: center;
}
.ctl-selinfo { font-size: 0.9rem; }

/* The hand tray. SCOPED to .ctl-handpanel (def.ts puts that class on the
   shared #hand-panel): an unscoped `#hand-cards .card` rule here would
   resize the hand of every one of the ~60 games that share this id.
   The row is capped at exactly EIGHT card widths, so a full hand fills
   it edge to edge - the limit is visible in the furniture, not just in
   the "(n/8)" title. */
.panel.ctl-handpanel { --ctl-hand-cw: 72px; }
.panel.ctl-handpanel #hand-cards {
    gap: 4px;
    max-width: calc(8 * var(--ctl-hand-cw) + 7 * 4px);
    /* Centered as a BLOCK, then centered within: the tray's own box stays
       put at its eight-card cap while the cards inside re-center. */
    margin-inline: auto;
    justify-content: center;
}
.panel.ctl-handpanel #hand-cards .card { --cw: var(--ctl-hand-cw); }
/* A ninth card (over the limit, until one is shed) shares the eight-card row
   rather than wrapping to a second: the second row put 104px under the hand
   for one turn, and the app layout's fit then drew the board smaller for the
   rest of the game. Narrower cards here also read as "one too many". */
.panel.ctl-handpanel #hand-cards:has(> .card:nth-child(9)) .card {
    --cw: round(down, calc((8 * var(--ctl-hand-cw) - 4px) / 9), 1px);
}

/* Selection and targeting glows; fades while a pick is armed. */
.card.ctl-sel { outline: 3px solid var(--accent); outline-offset: 2px; }
.card.ctl-target {
    outline: 3px solid #ffd166; outline-offset: 2px;
    animation: ctl-pulse 1.1s ease-in-out infinite alternate;
}
@keyframes ctl-pulse {
    from { outline-color: rgba(255, 209, 102, 0.55); }
    to { outline-color: #ffd166; }
}
.card.ctl-faded { filter: grayscale(0.5) brightness(0.65); }
/* The card a waiting one-off or Jack is aimed at (markAimed in def.ts).
   Dashed and steady, so it never reads as the pulsing "tap me" ring above:
   nobody taps it, it is the card about to go. Scoped under the board so it
   out-specifies the stolen-stack ring on a base card. */
#ctl-board .card.ctl-aimed { outline: 3px dashed #ef6f6c; outline-offset: 2px; }
/* A hand card picked for the 4's discard (def.ts renderHand). The rest of
   the hand pulses gold (.ctl-target) as "tap me", and --accent is that same
   gold, so a gold selection ring was invisible here. The pick is ringed,
   steady, in the coral of a card about to go, and dimmed a little. NO LIFT:
   a 10px lift put the ring over the hand title in a browser at both 1280px
   and 390px. Scoped under the hand panel so a flight ghost (a clone in
   <body>) never carries it. */
.panel.ctl-handpanel #hand-cards .card.ctl-shedpick {
    outline: 3px dashed #ef6f6c; outline-offset: 2px;
    filter: brightness(0.82);
}

/* Public knowledge: a hidden-zone card everyone has seen (a 7's put-back
   on the deck, a 3's take, a 9's bounce) renders face-up with an amber
   inner ring; your own exposed cards wear an eye badge. */
.card.ctl-known { box-shadow: 0 0 0 2px #ffd166 inset; }
/* PUBLIC KNOWLEDGE gets the site's existing eye, not a new invention:
   these declarations mirror .evo-eye (evolutionpoker), which marks its
   publicly-acquired cards the same way. def.ts builds the same SVG.
   NOTE: `position: relative` here is a TWO-class rule and therefore
   out-specifies `.card-fly { position: fixed }` on an animation ghost,
   which made flying clones land in document flow and flicker. def.ts
   names ctl-badged/ctl-known in ghostStripClasses so no ghost ever
   carries them; do not remove that without removing this. */
.card.ctl-badged { position: relative; }
/* The badge itself is the SHARED `.eye-badge` (webplay.css), including its
   .card.mini sizes: these declarations used to be copied here verbatim, which
   meant "publicly known" could drift between games. `.ctl-eye` survives as
   this game's own hook - def.ts passes it to eyeBadge() and names it in
   ghostStripClasses - and the one thing it changes is WHICH CORNER: the
   shared badge sits bottom-right, and on Cuttle's cards that corner belongs
   to the power glyph (.ctl-fx below), so here the eye moves to the top.
   Two classes, so it beats the shared single-class rule without touching
   the other games that use the eye. */
.eye-badge.ctl-eye { top: 4%; bottom: auto; }

/* A known top card perches ON the pile, overlapping it and tipped a few
   degrees, so it reads as "the next card off the deck" rather than as a
   second pile. It is anchored to the deck card (left: 0, nudged right)
   and lifted by a fraction of ITS OWN height, so roughly 60% of it stays
   over the pile at any card size. An earlier `bottom: calc(100% - 6px)`
   cleared the pile entirely and floated up into the opponent's field. */
/* ON the pile, not above it: nudged right and up just enough (about 8px)
   that the back beneath still shows as a deck. Lifting it 38% of its height
   meant reserving 26px of empty headroom over the piles for the whole game. */
.ctl-deckperch {
    position: absolute; left: 0; top: 0;
    transform: translate(9px, -13%);
    pointer-events: none;
    z-index: 2;
}
.ctl-deckpile { position: relative; }
/* Drawing AT the hand limit is legal here (house rule) but costs a card:
   the warning color is the whole point, so it is not a silent trade. */
.ctl-warn { color: #f0c05a; }
.btn.ctl-warnbtn { border-color: #f0c05a; color: #f0c05a; }
.card.ctl-warntap { outline: 2px dashed rgba(240, 192, 90, 0.75); outline-offset: 2px; }
.card.ctl-knowntop {
    --cw: 44px;
    box-shadow: 0 3px 8px rgba(0, 0, 0, 0.5);
    pointer-events: auto;
}
/* The tip: `rotate`, never `transform`, scoped under the perch so a flight
   ghost (a clone in <body>) is not tipped (see .ctl-glasses below). */
.ctl-deckperch .card.ctl-knowntop { rotate: -7deg; }

/* POWER GLYPHS (icons.ts, withPower in def.ts): a dark chip in each card's
   bottom-right corner, over the card's second index. The first index, top-
   left, still names the card, and the public eye sits top-right (see
   .eye-badge.ctl-eye above), so a card that wears both keeps them apart.
   Sized off the card's own --cw, so it follows every card size on the
   board: about 19px on a 72px hand card, 15px in the field, 11px in the
   opponent's 34px row, where it is still one solid shape.

   The positioning context is given per CONTAINER, not by a class on the
   card. A flying or dragged card is a clone appended to <body>, outside all
   of these, so none of them can out-specify its `position: fixed` (the trap
   .card.ctl-badged documents above). */
.panel.ctl-handpanel #hand-cards .card,
#ctl-board .ctl-royals .card,
#ctl-board .ctl-handrow .card,
#ctl-board .ctl-staged .card,
#ctl-board .ctl-chain .card,
#ctl-board .ctl-scrapcards .card,
#ctl-board .ctl-deckperch .card { position: relative; }
.ctl-fx {
    position: absolute; bottom: 3%; right: 4%; z-index: 1;
    line-height: 0; padding: 1px; border-radius: 5px;
    background: rgba(16, 22, 30, 0.86); color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.35);
    pointer-events: none;
}
.ctl-fx .ctl-fxicon {
    display: block;
    width: clamp(10px, calc(var(--cw) * 0.27), 20px);
    height: clamp(10px, calc(var(--cw) * 0.27), 20px);
}
/* The opponent's row is 34px: tuck the chip into the very corner and drop
   its padding, or it crowds the big rank of the simplified art. */
.ctl-handrow .ctl-fx { bottom: 1px; right: 1px; padding: 0; border-radius: 4px; }
/* Glasses lie sideways; the glyph stays upright on them. */
.card.ctl-glasses > .ctl-fx { rotate: -90deg; }
/* Beside the power's name in the action bar, which is where the picture is
   learned, and in the Card abilities list (on the board and on the rules
   page). In em there, so it rides the text size. */
.ctl-selinfo .ctl-fxicon,
.ctl-actionnote .ctl-fxicon,
.ctl-ref .ctl-fxicon,
.rules.cheatsheet .ctl-fxicon {
    display: inline-block;
    width: 1.15em; height: 1.15em;
    vertical-align: -0.2em;
    margin-right: 0.3em;
    color: var(--accent);
}
.ctl-ref .ctl-fxblank { display: inline-block; }

/* The twin-one-off button reads as the variant's special move. */
.btn.ctl-twinbtn { border-color: #9ad17a; color: #9ad17a; }

/* Four fixed columns (name / Points / Goal / Kings): the shared
   .result-row auto-fit minmax(180px) is sized for poker hand cells and
   wraps these narrow cells onto a second line. */
.ctl-result { max-width: 420px; }
.ctl-result .result-row {
    grid-template-columns: minmax(90px, 1.4fr) repeat(3, 1fr);
    gap: 8px;
}

/* Tapping the deck draws (or digs, with a 7 selected). */
.ctl-deckpile .card.clickable { cursor: pointer; }
.ctl-deckpile .card.clickable:hover { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The selected card's play zone: tap the open space or drag the card in.
   Turns GREEN when the play would win the game outright. */
.ctl-field.ctl-playarea {
    outline: 2px dashed var(--accent); outline-offset: 4px;
    border-radius: 8px; cursor: pointer;
    /* NO min-height here. This is a TWO-class rule, so a min-height set
       here out-specifies the reservation on .ctl-field and wins - which is
       what it used to do with `min-height: 40px`, collapsing your own
       empty field from 84px to 40px the instant you selected a card, and
       moving the action bar and hand tray 44px up and back down again on
       every select and deselect. The base reservation is already larger
       than this zone needs; it just needs to be left alone. */
}
.ctl-field.ctl-playarea.ctl-winplay {
    outline: 3px solid #6fdc8c;
    background: rgba(111, 220, 140, 0.12);
    animation: ctl-winpulse 1s ease-in-out infinite alternate;
}
@keyframes ctl-winpulse {
    from { box-shadow: 0 0 0 0 rgba(111, 220, 140, 0.0); }
    to { box-shadow: 0 0 14px 2px rgba(111, 220, 140, 0.45); }
}
.ctl-winhint {
    align-self: center; margin-left: 8px;
    color: #6fdc8c; font-weight: 800; font-size: 0.82rem;
    text-transform: uppercase; letter-spacing: 0.04em;
}
/* Drag feedback: the shared drag module toggles .dropzone-active. */
.ctl-field.dropzone-active,
.ctl-theirs [data-target-id].dropzone-active {
    outline: 3px solid var(--accent); outline-offset: 3px;
}

/* The opponent's draw: a back sliding deck -> their hand row. */
.card.ctl-drawfly {
    position: fixed; z-index: 900; margin: 0; pointer-events: none;
    transition: transform 0.42s cubic-bezier(0.2, 0.7, 0.3, 1),
        opacity 0.42s ease;
}

/* The 3's picker: full-size, value-sorted, un-takeable 3s faded. */
.ctl-scrappicking .ctl-scrapcards .card { --cw: 56px; }

/* Cheatsheet list: give each rank's line room to breathe. */
.ctl-ref li { margin: 4px 0; }
.ctl-refnote { opacity: 0.85; font-size: 0.9rem; }

@media (max-width: 560px) {
    /* Set the VARIABLE, not the card: .ctl-field's reserved min-height is
       derived from it, so overriding --cw directly would leave the phone
       reserving desktop-sized headroom. */
    .ctl-field { --ctl-field-cw: 46px; }
    /* Eight of these no longer fit a phone, so the row wraps to two
       lines - the 8-wide cap stays meaningful on anything larger. */
    .panel.ctl-handpanel { --ctl-hand-cw: 52px; }
    .ctl-stack .ctl-jack { --cw: 28px; }
    .ctl-center { gap: 10px; }
    /* A finished re-counter chain is three cards and four labels: at 40px
       cards and 8px gaps it wrapped its outcome onto a second line at 390px
       and pushed everything below down 27px. Tighter, it keeps one line. */
    .ctl-justchain { gap: 4px; padding: 4px 6px; }
    .ctl-justchain .card { --cw: 36px; }
    /* The upward scrap reaches only as far as the opponent's field, so the
       status line above the board stays readable (at 340px it covered it). */
    .ctl-scrapbrowser { max-height: min(32vh, 340px); }
}


/* The finished-game summary is lifted into a modal by the shell, for every game
   (GameShell.syncRoundOverlay, `#result-panel.result-overlay` in platform.css).
   Cuttle used to carry a duplicate of that geometry under `ctl-resultmodal`, from
   when the shell lifted a between-rounds summary only. */

/* ---------- the setup card reads down the middle ----------------------------
   `.setup-card` is centered on the page as a block (max-width + margin auto)
   but its CONTENTS were left-aligned, so the heading and the option rows sat
   against the left edge above a board that is centered throughout.

   Scoped with :has() on #opt-micro, which only Cuttle emits, because the card
   itself and `.opt-group` are shared template markup: a bare `.opt-group` rule
   would re-align option rows on all ninety-odd setup screens and in any other
   panel that reuses the class. A browser without :has() simply drops these two
   rules and gets the left-aligned card exactly as it is today, which is the
   same bet the shared sheet already makes for its own :has() rules. */
.setup-card:has(#opt-micro) h2 { text-align: center; }
.setup-card:has(#opt-micro) .opt-group { justify-content: center; }
/* =========================================================================
   German Whist (germanwhist, gw-)
   Layout intent: the board is three quiet panels in a row - the stock (the
   face-up prize sitting ON the face-down pack, with the trump and the count
   beside it), the trick, and the standing - and nothing above the hand may
   change height mid-deal. A settled trick stays on display until the next
   lead, so the caption line reserves its height. The standing carries the
   game's signature feature: every card drawn face up is PUBLIC, so the
   opponent's "Seen" row keeps those faces visible until they are played,
   and the viewer's own seen cards wear the shared eye badge. From stage 2
   that row becomes the opponent's whole OPEN hand, because by then every
   card is accounted for and both hands are deducible.
   ========================================================================= */

/* Two lines' worth, always: the wording grows when it is explaining the
   stage-1 auction ("Winning takes the K♠; lose it and you draw hidden"). */
.status.gw-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.gw-status { min-height: 4.4em; }
}

.gw-scorename { margin-right: 6px; }
.gw-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }

/* --- the stage banner: does this trick count? ---------------------------- */

/* The one loud line on the board. German Whist's whole shape is that the
   first 13 tricks buy cards and the last 13 ARE the game, so the switch gets
   a banner rather than a note in a corner: muted while building, accented
   (and carrying the running tally) once the tricks score. */
.gw-stagebanner {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    padding: 7px 12px;
    margin-bottom: 10px;
    border-radius: 12px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.16);
}
.gw-stagetag {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 2px 8px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.10);
    white-space: nowrap;
}
.gw-stageword { font-size: 13px; opacity: 0.85; }
.gw-stagebanner.gw-building { opacity: 0.85; }
.gw-stagebanner.gw-counting {
    border-color: rgba(255, 209, 102, 0.55);
    background: rgba(255, 209, 102, 0.12);
}
.gw-stagebanner.gw-counting .gw-stagetag {
    background: var(--accent);
    color: #1a1a1a;
}
.gw-stagebanner.gw-counting .gw-stageword { opacity: 1; font-weight: 600; }
.gw-stagetally { display: flex; gap: 8px; margin-left: auto; }
.gw-stagecount {
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    padding: 2px 9px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.28);
    white-space: nowrap;
}
.gw-stagecount.mine { color: var(--accent); }

/* --- the board: (stock + trick) · standing ------------------------------- */

.gw-board {
    display: flex;
    gap: 12px;
    align-items: flex-start;
    flex-wrap: wrap;
    margin-bottom: 12px;
}
/* The banner spans the board above the boxes. */
.gw-board > .gw-stagebanner { flex: 1 1 100%; margin-bottom: 0; }
/* The stock and the trick are two halves of one question, so they sit
   side by side and neither grows: the trick is two cards wide and the stock
   is one. The standing takes all the remaining width, because the open hand
   it prints in stage 2 is 13 cards long. */
.gw-playrow {
    display: flex;
    gap: 12px;
    align-items: stretch;
    flex: 0 1 auto;
}
.gw-stockbox,
.gw-trickbox,
.gw-standingbox {
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
.gw-trickbox { flex: 0 0 auto; }
.gw-standingbox { flex: 1 1 340px; min-width: 260px; }
/* Tricks that score: the box holding them says so. */
.gw-trickbox.gw-counts {
    border-color: rgba(255, 209, 102, 0.5);
    background: rgba(255, 209, 102, 0.07);
}
.gw-countchip {
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.04em;
    background: var(--accent);
    color: #1a1a1a;
}
.gw-boxtitle {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    margin-bottom: 6px;
}
.gw-stock-row { display: flex; gap: 12px; align-items: center; }
.gw-stock-meta {
    display: flex;
    flex-direction: column;
    gap: 6px;
    max-width: 190px;
}
.gw-trump { font-size: 14px; font-weight: 700; white-space: nowrap; }
.gw-stock-note { font-size: 12px; opacity: 0.75; }
.gw-stock-note.gw-prize { opacity: 0.9; }
.gw-stock-note.gw-stage2note {
    color: #ffd166;
    font-weight: 700;
    opacity: 1;
}
/* The open-hand fact, stated once here rather than as an eye on all 26 cards
   (see publicCardEl). Quieter than the line above it: that one is the news,
   this one is the explanation. */
.gw-stock-note.gw-opennote { opacity: 0.85; }

/* The stock: the face-up prize sits ON TOP of the face-down pack, offset so
   the pack's edge shows underneath - the card both players are fighting
   over is the biggest thing in the box. */
.gw-stock-stack {
    --gw-cw: 52px;
    position: relative;
    width: calc(var(--gw-cw) * 1.2);
    height: calc(var(--gw-cw) * 89 / 64 + 10px);
    flex: none;
}
.gw-stock-stack .card { --cw: var(--gw-cw); position: absolute; }
.gw-stock-back {
    top: 0;
    left: 0;
    box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.45);
}
.gw-upcard {
    top: 10px;
    left: calc(var(--gw-cw) * 0.2);
    box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.45);
}
/* Stage 2: the empty slot squares up where the pack used to be. */
.gw-stock-done {
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

/* The trick: a gutter of seat names, one card cell per seat, and a "led"
   flag column - fixed grid, so nothing shifts as cards land. */
.gw-trick-grid {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 10px;
    row-gap: 6px;
}
.gw-seat {
    justify-self: end;
    font-size: 13px;
    font-weight: 600;
    opacity: 0.7;
    white-space: nowrap;
}
/* Two cards, played one at a time: the trick needs no more room than they
   take, so its cards are sized here rather than inheriting the hand's. */
.gw-cell { display: flex; justify-content: center; }
.gw-cell .card { --cw: 50px; }
/* Stagger the leader's card left, so who led reads at a glance. */
.gw-cell.gw-led { transform: translateX(-8px); }
.gw-flag {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.5;
    min-width: 26px;
}
.card.gw-winning {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    border-radius: 6px;
}
/* A settled trick stays on display until the next lead, but visibly OVER. */
.gw-cell .card.gw-settled { opacity: 0.55; }
/* One line, reserved: "Trick 7: AI takes it". */
.gw-caption {
    margin-top: 8px;
    font-size: 13px;
    opacity: 0.8;
    min-height: 1.4em;
}

/* --- the standing: trick counts and the PUBLIC known cards --------------- */

.gw-track { margin-bottom: 8px; }
.gw-track:last-child { margin-bottom: 0; }
.gw-track-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 3px;
}
.gw-track-name { font-size: 13px; font-weight: 700; }
.gw-track-worth {
    font-size: 12px;
    opacity: 0.75;
    font-variant-numeric: tabular-nums;
}
/* The feature row: every face of the opponent's hand that is public (their
   face-up draws), kept visible until played. */
.gw-knownrow {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    margin-top: 2px;
    padding: 4px 7px;
    border-radius: 8px;
    background: rgba(255, 209, 102, 0.10);
    border: 1px solid rgba(255, 209, 102, 0.35);
}
.gw-known-label {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.8;
}
.gw-knownpile { display: inline-flex; gap: 4px; flex-wrap: wrap; }
/* Big enough to READ at a glance, and drawn from the simplified atlas, which
   is the art that stays legible at this size. These cards are the whole point
   of the row: 30px thumbnails made the player squint at information the game
   is trying to hand them. */
.gw-knownpile .card { --cw: 44px; position: relative; }
.gw-known-none { font-size: 12px; opacity: 0.55; margin-top: 2px; }

/* Stage 2 opens both hands (every card is accounted for by then), so the row
   becomes a 13-card hand and gets room to wrap. */
.gw-knownrow.gw-openhand {
    background: rgba(255, 209, 102, 0.14);
    border-color: rgba(255, 209, 102, 0.5);
    align-items: flex-start;
}
.gw-knownrow.gw-openhand .gw-knownpile { gap: 3px; }

/* --- the hand ------------------------------------------------------------- */

/* A card the follow rules exclude right now: visible, inert, dimmed. */
.card.gw-mute { opacity: 0.45; }
/* Trumps in the viewer's hand wear a quiet gold keel. */
.card.gw-trumpcard { box-shadow: 0 3px 0 -1px rgba(255, 209, 102, 0.55); }
/* A hand card the OPPONENT knows: it wears the site's shared eye badge (see
   .eye-badge in webplay.css, and eyeBadge in shared/ui/panels.ts), so public
   knowledge is marked the same way here as in Cuttle, Cupid, Evolution Poker,
   Bubble Gum and The Crew. In stage 1 that is the face-up draws; from stage 2
   it is the whole hand.

   `position: relative` is a TWO-class rule so it out-specifies `.card-fly`'s
   `position: fixed`, and `gw-seen` is listed in this game's
   ghostStripClasses - a ghost that kept the class landed in document flow. */
.card.gw-seen { position: relative; }

/* --- the actions bar: "Take the game" ------------------------------------- */

/* Appears only when the viewer holds 7 of the 13 tricks that count, so it is
   never a decision - only a shortcut past tricks that cannot matter. */
.gw-actions {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    padding: 9px 14px;
    margin-bottom: 12px;
    border-radius: 12px;
    border: 1px solid rgba(255, 209, 102, 0.55);
    background: rgba(255, 209, 102, 0.12);
}
.gw-actions-note { font-size: 12px; opacity: 0.85; }

/* --- the deal summary ------------------------------------------------------ */

.gw-summary {
    max-width: 460px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
.gw-summary .result-table { margin-top: 10px; }
.result-cell.gw-won { color: var(--accent); font-weight: 800; }
/* The shared .result-row auto-fits 180px seat columns, which WRAPS the
   second seat onto its own row inside this narrow two-seat summary (and
   inside the log dropdowns): pin the shape to label + two seats. */
.gw-result .result-row {
    grid-template-columns: minmax(120px, auto) 1fr 1fr;
}

/* --- the log --------------------------------------------------------------- */

/* A scored deal's collapsed dropdown, with the overview table the between-deals
   summary shows, is the platform's now: `.loggroup` and `.loggroup-body` in
   src/shared/styles/platform.css style every game's grouped log the same way. */

@media (max-width: 760px) {
    /* Narrow: the stock and trick stop sharing a row with the standing, but
       stay side by side with each other. */
    .gw-playrow { flex: 1 1 100%; }
}
@media (max-width: 560px) {
    .gw-stock-stack { --gw-cw: 46px; }
    .gw-cell .card { --cw: 44px; }
    .gw-knownpile .card { --cw: 38px; }
}
/* =========================================================================
   Boast or Nothing (boastornothing, bon-)
   Layout intent: the board is two columns - the seats and the trick on the
   left, the RANKING STACK and the out-of-play pair on the right - and the
   ranking stack is the one panel that must never scroll off or move, because
   it decides every trick and it changes after each one. A settled trick stays
   on display until the next lead, so the caption line under it reserves its
   height; nothing above the hand changes height mid-hand.
   Pass Cards are NOT on the sprite atlas (they are not one of the 54), so
   .bon-pass paints its own face and has to out-specify every atlas rule the
   shared resize sweep can add - see the background-image kills below.
   ========================================================================= */
/* Two lines' worth, always: the wording grows when it is explaining that only
   Pass Cards are down and no suit has been named yet. */
.status.bon-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.bon-status { min-height: 4.4em; }
}
.bon-progress-tricks,
.bon-progress-bullseye { font-size: 13px; opacity: 0.75; }
.bon-progress-bullseye {
    margin-left: auto;
    padding: 3px 10px;
    border-radius: 999px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    opacity: 0.9;
}
/* --- the board ------------------------------------------------------------ */
.bon-board {
    display: flex;
    gap: 12px;
    align-items: flex-start;
    flex-wrap: wrap;
    margin-bottom: 12px;
}
.bon-board .bon-main { flex: 1 1 420px; min-width: 300px; }
.bon-board .bon-side {
    flex: 0 1 250px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.bon-panelhead {
    font-size: 12px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
    margin-bottom: 8px;
}
/* --- the seats: tricks against the bullseye, and the match score --------- */
.bon-seats {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.bon-seat {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 108px;
    padding: 6px 10px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.bon-seat.bon-seat-mine { border-color: rgba(255, 255, 255, 0.35); }
/* Whose card the table is waiting for. */
.bon-seat.bon-seat-turn {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}
.bon-seatname { font-size: 13px; font-weight: 700; }
/* The number of the hand. Green while the seat is on a scoring count, muted
   while it is off one - the whole read of the game in one chip. */
.bon-seattricks { font-size: 13px; font-weight: 700; }
.bon-seattricks.bon-zero { color: #9fe0b4; }
.bon-seattricks.bon-bullseye { color: var(--accent); }
.bon-seattricks.bon-miss { opacity: 0.55; font-weight: 400; }
.bon-seatpass { font-size: 12px; opacity: 0.8; min-height: 1.1em; }
.bon-seathand { font-size: 12px; opacity: 0.6; }
/* --- the trick ----------------------------------------------------------- */
.bon-trickpanel { padding: 10px 12px; }
.bon-trickrow {
    display: flex;
    gap: 8px;
    align-items: flex-start;
    flex-wrap: wrap;
}
/* Ownership reads two ways: the caption (needed with up to four opponents -
   the two-player siblings can do without one) and the vertical stagger, the
   viewer's slot a step lower. */
.bon-trickcell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.bon-trickcell.bon-mine { padding-top: 14px; }
.bon-trickcell.bon-theirs { padding-bottom: 14px; }
.bon-trickcap { font-size: 11px; opacity: 0.65; white-space: nowrap; }
.bon-trickcell.bon-mine .bon-trickcap { opacity: 0.9; }
/* The slot the table is waiting on. */
.bon-trickcell.bon-next .card.empty { border-color: var(--accent); }
/* The card TAKING the trick, marked as the trick fills and not only once it is
   settled: with the suits' ranking moving under the players, working out who is
   ahead from the cards is not something anyone should have to do in their head. */
.bon-trickpanel .card.bon-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}
.bon-trickcell:has(.card.bon-winning) .bon-trickcap {
    color: var(--accent);
    font-weight: 700;
    opacity: 1;
}
.bon-lasttrick { margin-top: 8px; font-size: 13px; opacity: 0.85; min-height: 2.6em; }
/* --- the ranking stack: the panel the game turns on ---------------------- */
.bon-ranking { padding: 10px 12px; }
.bon-rankrow {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 4px 8px;
    border-radius: 8px;
    border: 1px solid transparent;
}
.bon-rankrow.bon-rank-top {
    background: rgba(255, 209, 102, 0.14);
    border-color: var(--accent);
}
/* The suit the last trick just demoted, so the move is legible rather than
   something the player has to have watched. */
.bon-rankrow.bon-rank-moved { border-color: rgba(255, 255, 255, 0.4); }
.bon-rankplace {
    font-size: 12px;
    font-weight: 800;
    opacity: 0.5;
    min-width: 12px;
    font-variant-numeric: tabular-nums;
}
.bon-ranksuit { font-size: 15px; font-weight: 700; flex: 1 1 auto; }
.bon-ranksuit.red { color: var(--red); }
.bon-ranksuit.red.diamond { color: var(--red-diamond); }
.bon-ranksuit.club { color: var(--blue-club); }
.bon-rankheld { font-size: 12px; opacity: 0.65; white-space: nowrap; }
.bon-ranknote { margin-top: 8px; font-size: 12px; opacity: 0.62; }
/* --- the two out-of-play cards ------------------------------------------- */
.bon-tablepanel { padding: 10px 12px; }
.bon-tablerow { gap: 6px; }
.bon-tablerow .card.bon-extra { --cw: 46px; }
.bon-tablenote { margin-top: 8px; font-size: 12px; opacity: 0.62; }
/* --- the Pass Card face -------------------------------------------------- */
/* Not on the atlas, so every atlas background has to be killed - including the
   one the shared resize sweep (src/shared/ui/sprites.ts refreshCardArt) can add
   as `art-mini`, which wins on specificity over a plain .card rule. The three
   selectors below mirror .card.empty's kills for exactly that reason. */
.card.bon-pass,
.card.bon-pass.art-mini,
.card.bon-pass.art-mini:not(.simple) {
    background-image: none;
    background-color: #2f4f3f;
    border: 1px solid rgba(0, 0, 0, 0.4);
    box-shadow: inset 0 0 0 2px rgba(242, 239, 233, 0.22);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    color: var(--ink);
}
.bon-passmark {
    font-size: calc(var(--cw) * 0.19);
    font-weight: 800;
    letter-spacing: 0.06em;
    transform: rotate(-90deg);
    white-space: nowrap;
}
/* --- the hand ------------------------------------------------------------ */
/* While the viewer is choosing, everything unplayable fades: wrong suit and
   (never) a Pass Card, so the legal moves are the bright ones. */
#hand-cards .card.bon-faded { filter: brightness(0.55) saturate(0.7); }
.bon-cardlabel.bon-passlabel { opacity: 0.85; font-style: italic; }
/* --- the hand summary and the final standings ---------------------------- */
.bon-summary .winner-banner { font-size: 21px; }
/* The shared row grid reserves 180px a seat, which five seats cannot have.
   --bon-cols is set by the renderer from the table size. */
.bon-result .result-row {
    grid-template-columns: minmax(88px, auto) repeat(var(--bon-cols, 4), minmax(56px, 1fr));
}
.bon-result .result-cell.bon-won { color: var(--accent); }
.bon-reasons { display: flex; flex-direction: column; gap: 2px; }
.bon-reason { font-size: 13px; }
/* --- the rules and the scoring reference --------------------------------- */
.bon-suit { font-weight: 700; }
.bon-suit.red { color: var(--red); }
.bon-suit.red.diamond { color: var(--red-diamond); }
.bon-suit.club { color: var(--blue-club); }
.bon-scoretable { border-collapse: collapse; margin: 6px 0 4px; font-size: 13.5px; }
.bon-scoretable th,
.bon-scoretable td { padding: 3px 12px 3px 0; text-align: left; vertical-align: top; }
.bon-scoretable thead th { font-size: 12px; opacity: 0.6; font-weight: 700; }
.bon-score-value { font-weight: 800; font-variant-numeric: tabular-nums; }
@media (max-width: 620px) {
    .bon-board .bon-side { flex: 1 1 100%; }
    .bon-trickcell .card { --cw: 50px; }
    .bon-seat { min-width: 96px; }
}

/* =========================================================================
   Let Me Off (letmeoff)

   The layout follows from the one fact that makes this game itself: A PLAYER
   HOLDS THREE HANDS, and two of them belong to somebody else as well. So the
   screen is built in bands from the table inward to the player:

     the other seats, arranged AROUND the viewer, with the stand each PAIR of
     them shares drawn in the gap between them    (.lmo-table)
     the trick, as a cross                         (.lmo-trick)
     this player's own counters                    (.lmo-mine)
     the two stands this player shares, face up    (.lmo-stands)
     this player's own hand                        (#hand-panel)

   Reading down that column is reading outward from the public table to the
   private cards, and the two shared stands sit exactly where they belong: below
   the table, above the hand, because they are half public and half yours.

   Seats sit around the viewer rather than in a row because both of a player's
   stands are shared with their IMMEDIATE neighbors, so "left" and "right" on
   screen are the same left and right the stands are named for. The trick slots
   use the SAME position classes, so a played card lands under the seat that
   played it.
   ========================================================================= */

/* --- the scoreboard ------------------------------------------------------ */
/* HIGH score wins here, so `.progress-score.leading` marks the MAXIMUM - the
   opposite of the Hearts family sharing this widget. */
.lmo-scorename { font-size: 12px; opacity: 0.8; margin-right: 5px; }
.lmo-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }
.lmo-progress-tricks { opacity: 0.7; }

/* --- the status line ----------------------------------------------------- */
/* Reserved height, so naming the suit to follow, or pressing Hint, cannot make
   the board jump. Two lines where a status plus its hint can wrap; ONE where the
   play area is wide enough for the longest of them, "Follow ♦ Diamonds from any
   of your three hands. Hint: answer with 10♦, off the stand on your right.",
   which measures 822px at 18px and a little more in the bold turn weight. The
   play area is the viewport less 348px (the log column and the gaps). */
.status.lmo-status { min-height: 2.9em; }
@media (min-width: 1200px) {
    .status.lmo-status { min-height: 1.45em; }
}
/* LIT whenever this device may act. A sentence on its own is not enough of a
   signal about whose turn it is, which is what the seat chip, the panel border
   and this between them are for. */
.status.lmo-status.lmo-turn {
    color: var(--accent);
    font-weight: 700;
}
.lmo-hintnote { opacity: 0.85; font-weight: 400; }

/* --- the table: every seat but the one on screen, and the stands between them */
/* FIVE COLUMNS: a panel, the stand those two panels share, a panel, the other
   stand, a panel. A stand is not a pile belonging to the board, it is a place
   BETWEEN TWO PLAYERS - which is exactly how the viewer's own two are drawn,
   between them and each neighbor - so the ones they only watch are drawn between
   the two seats that share them and nowhere else. */
.lmo-table {
    display: grid;
    grid-template-columns: 1fr auto 1fr auto 1fr;
    grid-template-areas: 'left lstand up rstand right';
    align-items: end;
    gap: 10px;
    margin-bottom: 12px;
    /* The seat panels size themselves off THIS width, not the viewport's: at a
       1024px window the log column takes 300px and the panels are as narrow as
       a phone's, while at 768px (one column) they are wider. See the @container
       block below. Nothing in here is position: fixed, so containment costs
       nothing (the ghost cards fly in document.body). */
    container: lmotable / inline-size;
}
/* Three seats have no far chair; the two that remain take the outer columns,
   which keeps the neighbors symmetrical instead of leaving a lopsided gap. The
   one stand the viewer does not share is the one those two share with EACH
   OTHER, so it sits in the middle: still between its own two sharers. */
.lmo-table.seats-3 { grid-template-areas: 'left . lstand . right'; }
.lmo-seat.pos-left { grid-area: left; }
.lmo-seat.pos-right { grid-area: right; }
.lmo-seat.pos-up {
    grid-area: up;
    /* Raised clear of its neighbors so the row reads as an arc around a table.
       Bottom margin rather than a transform, so the space stays reserved and
       nothing underneath is overlapped. */
    margin-bottom: 22px;
}
.lmo-seat,
.lmo-mine {
    min-width: 0;
    padding: 8px 10px 10px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default, so the accent ring never shifts the layout. */
    outline: 2px solid transparent;
}
/* The seat on turn. Gold is the platform's "this is where the action is". */
.lmo-seat.lmo-active,
.lmo-mine.lmo-active { outline-color: var(--accent); }
.lmo-seat-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 6px;
}
.lmo-seat-name { font-weight: 700; font-size: 14px; }
/* WHOSE TURN IT IS, on the name itself: a gold chip with a "▶" in front, plus
   the gold ring on the panel. It used to be a separate "to play" chip, which
   arrived and left every turn and took a line with it wherever the chips
   wrapped. Accented and never abbreviated: the whole board is read off it. */
.lmo-seat-name.lmo-toact {
    background: var(--accent);
    color: var(--accent-ink);
    border-radius: 999px;
    padding: 0 6px;
}
.lmo-seat-name.lmo-toact::before { content: '\25b6\00a0'; font-size: 0.8em; }

/* A SEAT PANEL IS FOUR FIXED ROWS, at every width: the name and match total,
   cards in reach, the concealed hand with the tricks won beside it, and a foot
   row holding the suits it has shown it cannot follow and the Aces it has won
   with. Nothing that turns up mid-deal (a void, an Ace, the turn) adds a row,
   so the strip is one height all deal and nothing below it moves. Sizes for a
   narrow panel are in the @container block at the end of this section. */
.lmo-seat {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto 20px 30px 28px;
    grid-template-areas:
        'head  head'
        'reach reach'
        'backs tricks'
        'foot  foot';
    gap: 3px 6px;
    align-items: center;
    padding: 6px 10px 7px;
}
/* ONE LINE, whatever the name: a long one is cut short rather than wrapped. */
.lmo-seat .lmo-seat-head {
    grid-area: head;
    margin-bottom: 0;
    gap: 6px;
    flex-wrap: nowrap;
}
.lmo-seat .lmo-seat-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.lmo-seat .lmo-seat-cards { grid-area: backs; margin-bottom: 0; min-height: 0; }
/* The chips line is not a box here: each chip takes a cell of the panel. */
.lmo-seat .lmo-seat-line { display: contents; }
.lmo-seat .lmo-chip.lmo-reach { grid-area: reach; justify-self: start; }
.lmo-seat .lmo-chip.lmo-tricks { grid-area: tricks; justify-self: end; }
/* The voids on the left and the Aces on the right of ONE row, spanning the
   panel, so three voids cannot widen the column the tricks chip sits in. The
   viewer's own panel has a foot row too (see .lmo-mine). */
.lmo-seat-foot {
    grid-area: foot;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    min-width: 0;
}
.lmo-seat-foot .lmo-suits,
.lmo-seat-foot .lmo-aces { margin-top: 0; min-height: 0; }
/* In a seat panel the cards and their red cost say what the row is. */
.lmo-seat .lmo-aces-label { display: none; }
.lmo-seat .lmo-aces.cardrow { flex-wrap: nowrap; }
/* The match total, the number that decides the game. */
.lmo-seat-total {
    font-size: 20px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
/* The highest total at the table. The scoreboard marks it too, but a phone does
   not show the scoreboard's per-seat chips, so this is where "who is ahead"
   lives there. Nothing lights while every total is equal. */
.lmo-seat-total.lmo-leading { color: var(--accent); }
/* The concealed hand: one back per card held. The COUNT is public at a real
   table; the identities are not, and are never rendered. Overlapped hard, so a
   six-card hand still fits a narrow panel. */
.lmo-seat-cards {
    display: flex;
    gap: 0;
    margin-bottom: 6px;
    min-height: 30px;
    align-items: center;
}
.lmo-seat-cards .card { --cw: 21px; margin-right: -11px; }
.lmo-seat-empty { font-size: 11px; opacity: 0.6; }

/* --- the chips every seat carries ---------------------------------------- */
/* A row of its own only in the viewer's panel (.lmo-mine), and never wrapped
   there; a seat panel places each chip in a cell (see .lmo-seat). */
.lmo-seat-line {
    display: flex;
    gap: 6px;
    align-items: baseline;
    flex-wrap: nowrap;
    font-size: 12px;
}
.lmo-chip {
    padding: 1px 6px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.07);
    white-space: nowrap;
    opacity: 0.85;
}
/* CARDS IN REACH: this seat's own hand plus both of its stands, and the number
   the race is actually about. Tabular so the four of them can be compared down
   the column without reading the digits. */
.lmo-chip.lmo-reach {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    opacity: 1;
}
/* Nothing left in any of the three hands: this seat has ended the deal. */
.lmo-chip.lmo-reach.lmo-cleared {
    background: rgba(60, 160, 90, 0.35);
    color: #d8f5de;
}
/* The chip's short spelling, for a phone's seat panel (see seatChips). */
.lmo-cleared .lmo-short { display: none; }

/* --- the suit strip, on every board -------------------------------------- */
/* ON THE VIEWER'S OWN BOARD: all four suits, always, in the display order
   (clubs, diamonds, spades, hearts), each carrying how many of that suit are
   within reach across all three of their hands. Read by POSITION, so a suit that
   has run out is a mark in a place the eye already knows.

   ON EVERY OTHER BOARD: only the suits that seat has PROVED it holds nowhere,
   crossed out, and nothing at all until there is one. All four glyphs were
   drawn there first and that was wrong - three of them stood for "not known",
   which is to say for nothing, and they read as furniture rather than as
   information. They sit in the panel's fixed foot row, so the first void does
   not shove the board down when it appears. */
.lmo-suits {
    display: flex;
    gap: 9px;
    align-items: center;
    margin-top: 5px;
    /* Reserved, so a panel does not grow the first time a suit runs out. */
    min-height: 17px;
    font-size: 12px;
}
.lmo-suit {
    display: inline-flex;
    align-items: baseline;
    gap: 2px;
}
.lmo-suit-glyph { position: relative; font-size: 13px; line-height: 1; }
.lmo-suit-count { font-weight: 700; font-variant-numeric: tabular-nums; }
/* HOLDS NONE OF THIS SUIT ANYWHERE, which in this game is public the moment they
   play nothing on it. Crossed out with two bars over the glyph: a shape, not a
   color, so it survives a grayscale screenshot and a color-blind player reads it
   the same way. Pseudo-elements rather than an x character, so a text harvest
   cannot pick the mark up and read it as part of the hand. */
.lmo-suit-void { opacity: 0.9; }
.lmo-suit-void .lmo-suit-glyph::before,
.lmo-suit-void .lmo-suit-glyph::after {
    content: '';
    position: absolute;
    left: -3px;
    right: -3px;
    top: 50%;
    height: 2px;
    border-radius: 2px;
    background: #ff8a80;
}
.lmo-suit-void .lmo-suit-glyph::before { transform: rotate(45deg); }
.lmo-suit-void .lmo-suit-glyph::after { transform: rotate(-45deg); }
.lmo-suit-void .lmo-suit-count { color: #ff8a80; }

/* --- Aces set aside ------------------------------------------------------ */
/* Face up in front of their winner, as the rules require, with the running cost
   beside them. Public information: everybody watched them win. Drawn from the
   SIMPLIFIED atlas, because at this size the full art is a smudge and the rank
   is the entire reason they are on screen. */
.lmo-aces {
    display: flex;
    align-items: center;
    gap: 3px;
    flex-wrap: wrap;
    margin-top: 5px;
}
.lmo-aces .card { --cw: 20px; }
.lmo-aces-label {
    font-size: 10px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
}
.lmo-aces-cost {
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: #ff8a80;
}

/* --- the board: the trick -------------------------------------------------- */
/* Nothing but the trick, CENTERED. The stands that used to flank it now sit in
   the seat strip above, each between its own two players. The trick sizes to
   its slots and the board centers it. */
.lmo-board {
    display: grid;
    grid-template-columns: auto;
    justify-content: center;
    padding: 12px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.16);
    margin-bottom: 12px;
}
/* A DIAMOND: one fixed slot per seat, on the side that seat sits, so a card
   appears under the player who played it and never moves once it lands. North
   and South take the middle column and West and East sit between them, half a
   slot down, so the four slots take two slot heights rather than the three a
   plain cross needs. That was 125px of a 900px screen.

   EVERY SLOT IS RESERVED AT ITS FULL HEIGHT (card, caption and flag line, off
   the one card width the trick is drawn at), so a slot is the same height
   whether or not it carries a "winning", "won" or "no <suit>" flag, and the
   board never steps up or down as the trick fills. */
.lmo-trick {
    --lmo-playcw: 54px;
    --lmo-slot: calc(var(--lmo-playcw) * 1.39 + 38px);
    display: grid;
    grid-template-columns: auto auto auto;
    grid-template-rows: repeat(4, calc(var(--lmo-slot) / 2));
    grid-template-areas: '. up .' 'left up right' 'left down right' '. down .';
    align-items: center;
    justify-items: center;
    gap: 0 48px;
}
/* Three seats have no North: West and East sit beside South's upper half. */
.lmo-trick.seats-3 {
    grid-template-rows: repeat(3, calc(var(--lmo-slot) / 2));
    grid-template-areas: 'left . right' 'left down right' '. down .';
}
.lmo-play.pos-up { grid-area: up; }
.lmo-play.pos-left { grid-area: left; }
.lmo-play.pos-right { grid-area: right; }
.lmo-play.pos-down { grid-area: down; }
.lmo-play {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    min-height: var(--lmo-slot);
}
.lmo-play .card { --cw: var(--lmo-playcw); }
/* Wide enough for the longest caption this builds, "AI North . leads": at 68px
   it clipped to "AI West . le...", which mattered because the caption is what
   tells the led card apart from the winning one. The ellipsis stays as a
   backstop for a long name relayed by an online peer. */
.lmo-play-name {
    font-size: 11px;
    line-height: 14px;
    opacity: 0.7;
    max-width: 108px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* A slot still owed a card: outlined, not blank, so it reads as "waiting". */
.lmo-play.empty .card.empty { border-style: dashed; }
/* The seat on turn, so the eye knows where the next card is coming from. */
.lmo-play.onturn .card.empty { border-color: var(--accent); }
/* PLAYED NOTHING, which is a real move here: this seat held none of the led
   suit in any of its three hands. It has to look different from a slot that is
   still owed a card, because there is no discard in this game and an empty slot
   would otherwise be ambiguous. */
.lmo-play .card.empty.lmo-void {
    border-style: solid;
    border-color: rgba(255, 138, 128, 0.5);
    background: rgba(255, 138, 128, 0.09);
}
/* WHO IS CURRENTLY WINNING THE TRICK, called out on the card AND captioned
   underneath: color alone would leave it invisible to a color-blind player, and
   the caption also survives a grayscale screenshot. */
.lmo-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
.lmo-play-flag {
    font-size: 10px;
    line-height: 13px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--accent);
}
.lmo-play .card.empty.lmo-void ~ .lmo-play-flag { color: #ff8a80; }

/* --- the stands ---------------------------------------------------------- */
/* The two the viewer only watches: each in the gap between its own two panels,
   centered in that gap rather than stretched across it. */
.lmo-stand-hidden.lmo-side-left { grid-area: lstand; justify-self: center; }
.lmo-stand-hidden.lmo-side-right { grid-area: rstand; justify-self: center; }
/* The two the viewer SHARES, side by side and in that order: the one on their
   left on the left. NEVER WRAPPED - wrapping put "on your left" ABOVE "on your
   right" on a phone, which is the one thing this row exists to say. They fit
   instead, off --lmo-standcw. */
.lmo-stands {
    --lmo-standcw: 46px;
    display: flex;
    gap: 10px;
    flex-wrap: nowrap;
    align-items: flex-start;
    margin-bottom: 12px;
}
.lmo-stand {
    min-width: 0;
    padding: 7px 9px 9px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
/* The two stands THIS player shares: face up, playable, and given the room to
   be read, because they are as much this player's cards as the ones in hand.
   The green wash is the same "this side is yours" cue the sibling games use. */
.lmo-stand.lmo-stand-mine {
    flex: 1 1 0;
    min-width: 0;
    background: rgba(38, 120, 60, 0.12);
}
/* A stand this player does not share: a counted fan of backs, which is exactly
   what the table can see of it. Muted, because it is reference rather than
   something to aim at. */
.lmo-stand.lmo-stand-hidden { min-width: 132px; opacity: 0.8; }
.lmo-stand-title { font-size: 11px; opacity: 0.75; margin-bottom: 5px; }
.lmo-stand-cards {
    display: flex;
    align-items: center;
    gap: 5px;
    min-height: 34px;
}
/* The viewer's two stands are hand-like rows, and a hand is always CENTERED
   (ui-conventions.md). The shared rule only reaches #hand-cards, so a stand has
   to say it itself; its title and its empty line center with it, so the box
   reads as one column rather than a left-hung label over a centered row. */
.lmo-stand-mine .lmo-stand-cards { justify-content: center; }
.lmo-stand-mine .lmo-stand-title { text-align: center; }
/* The hidden ones overlap into a fan; the face-up ones must not, because the
   rank is the whole point of showing them. */
.lmo-stand-hidden .lmo-stand-cards { gap: 0; }
.lmo-stand-hidden .lmo-stand-cards .card { --cw: 21px; margin-right: -11px; }
/* Sized off the row's own property, so the reserved height follows the cards. */
.lmo-stand-mine .lmo-stand-cards .card { --cw: var(--lmo-standcw); }
/* ONE ROW RESERVED (card aspect 89/64 = 1.39), so emptying a wrapped row cannot
   walk the hand up the screen underneath it. */
.lmo-stand-mine .lmo-stand-cards {
    min-height: calc(var(--lmo-standcw) * 1.39);
}
.lmo-stand-count { font-size: 11px; opacity: 0.6; margin-top: 4px; }
.lmo-stand-empty { font-size: 11px; opacity: 0.6; }

/* --- this player's own counters ------------------------------------------ */
/* Every other seat shows these in its table panel; the viewer has no panel, so
   theirs needs a home of its own - and it is also where a trick this player
   took goes. TWO FIXED ROWS: the name, chips and total, then a foot row with
   the suit counts on the left and the Aces won on the right. */
.lmo-mine {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-rows: auto 28px;
    grid-template-areas:
        'name chips total'
        'foot foot  foot';
    gap: 3px 10px;
    align-items: center;
    padding: 6px 10px 7px;
    margin-bottom: 12px;
}
.lmo-mine .lmo-seat-head { display: contents; }
.lmo-mine .lmo-seat-name { grid-area: name; }
.lmo-mine .lmo-seat-total { grid-area: total; }
.lmo-mine .lmo-seat-line { grid-area: chips; }

/* --- the hand ------------------------------------------------------------ */
/* A little less padding than the shared panel: on a 900px-tall screen the hand
   ended 6px below the fold. */
.lmo-handpanel { padding: 10px 14px 12px; }
.lmo-handpanel .panel-head h3 { margin-bottom: 7px; }
/* A card the follow-suit rule forbids right now. Dimmed rather than removed:
   knowing what you CANNOT play is half of knowing what the position is. Only
   ever applied on this seat's own turn, where there is a choice being denied. */
.lmo-mute { opacity: 0.32; }

/* --- contextual actions -------------------------------------------------- */
.lmo-actions { align-items: center; }
.lmo-actions:not(.filled) { display: none; }

/* --- the deal summary and the final result ------------------------------- */
/* ONE EXPLICIT TRACK PER SEAT, as a class rather than an auto-fit guess. The
   shared .result-row auto-fits 180px seat columns, which needs 830px to seat four
   of them: at the DEFAULT four-player table the fourth seat wrapped onto the next
   line and collided with the following row's label. The seat count never changes
   mid-match, and auto-fit would have fixed the desktop case and still wrapped on a
   phone. Columns size to their contents, and the table keeps its natural width
   inside #result-panel's own overflow-x rather than being crushed. */
.lmo-summary .result-table.lmo-result { margin-top: 8px; }
/* ONE GRID FOR THE WHOLE TABLE, with every row a SUBGRID of it. The shared
   .result-row is a grid of its own, which sizes its label track from its OWN
   label: "Tricks scored" then made a wider first column than "Deal" did, and
   every row's seat columns started at a different x, so a column of figures was
   never actually a column - the widest label pushed its own row 28px out of step
   with the row above it. Subgridding makes the tracks the TABLE's, sized by every
   row at once, and keeps each row a real box so the Deal rule stays one
   unbroken line. */
.lmo-result {
    width: fit-content;
    grid-template-columns: auto repeat(3, minmax(4.6em, auto));
    row-gap: 2px;
    column-gap: 20px;
    /* Centered, like the other games' fit-content result tables. */
    margin-inline: auto;
}
.lmo-result.lmo-cols-4 {
    grid-template-columns: auto repeat(4, minmax(4.6em, auto));
}
.lmo-result .result-row {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: subgrid;
    align-items: baseline;
}
.lmo-result .result-label { text-align: left; white-space: nowrap; }
/* Right-aligned and tabular, so a column of figures lines up under the name it
   belongs to and the digits do not shuffle as they change. */
.lmo-result .result-cell {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
/* A seat that got all three hands away: the one row of the table that pays. */
.lmo-result .result-cell.lmo-won { color: var(--accent); font-weight: 800; }
/* The deal's own line, above the running match total. */
.lmo-result .result-row.lmo-dealrow {
    border-top: 1px solid var(--panel-border);
    margin-top: 4px;
    padding-top: 4px;
    font-weight: 700;
}
.lmo-log-out { color: var(--accent); }
.lmo-log-win { font-weight: 700; }

/* --- the scoring table in the Cheat_Sheet -------------------------------- */
/* Keyed on the table's own class, not the rules wrapper, so it styles the
   Rules_Summary and the Rules_Page alike. */
.lmo-scoretable { width: 100%; border-collapse: collapse; font-size: 13px; }
.lmo-scoretable th,
.lmo-scoretable td { padding: 3px 6px; text-align: left; vertical-align: top; }
.lmo-scoretable .lmo-score-value {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    font-weight: 700;
}
.lmo-scoretable .lmo-score-note { opacity: 0.7; }

/* --- narrow seat panels ---------------------------------------------------- */
/* Off the TABLE's width, not the viewport's: a panel is a fifth-or-so of the
   strip whatever the screen is. At 780px of strip a panel is about 150px wide,
   which is where the desktop sizes stop fitting the concealed hand and the
   tricks chip side by side on one row. That is a phone, but it is also a
   1024px window, where the log column leaves the strip 676px. */
@container lmotable (max-width: 780px) {
    .lmo-seat {
        grid-template-rows: auto 18px 21px 21px;
        gap: 2px 5px;
        padding: 4px 6px 5px;
    }
    .lmo-seat .lmo-seat-head { gap: 4px; }
    .lmo-seat .lmo-seat-name { font-size: 12.5px; }
    .lmo-seat .lmo-seat-name.lmo-toact { padding: 0 5px; }
    .lmo-seat .lmo-seat-total { font-size: 15px; }
    .lmo-seat .lmo-chip { padding: 1px 5px; }
    .lmo-seat .lmo-seat-cards .card { --cw: 14px; margin-right: -8px; }
    /* No fan of backs already says the hand is empty, and the words were wider
       than the cell they sat in. */
    .lmo-seat .lmo-seat-empty { display: none; }
    /* "out" is the word the rules use, and "all three hands away" does not fit. */
    .lmo-seat .lmo-cleared .lmo-long { display: none; }
    .lmo-seat .lmo-cleared .lmo-short { display: inline; }
    .lmo-seat .lmo-suits { gap: 4px; font-size: 11px; }
    .lmo-seat .lmo-aces.cardrow { gap: 2px; }
    .lmo-seat .lmo-aces .card { --cw: 15px; }
}

/* --- short screens -------------------------------------------------------- */
/* A LAPTOP IS SHORT. A 1440x900 screen gives the page about 790px once the
   browser's own bars are counted, and 1366x768 and 1280x800 less, and at full
   desktop sizes the hand ended 90px below the fold there. Every band gives a
   little back: the arc, the gaps between bands, the trick cards, the stand
   cards and the hand cards. All of them are sizes, so nothing here changes
   what any band shows, and each band is still one fixed height all deal. */
@media (min-width: 621px) and (max-height: 940px) {
    .lmo-seat.pos-up { margin-bottom: 0; }
    .lmo-table,
    .lmo-board,
    .lmo-mine,
    .lmo-stands { margin-bottom: 8px; }
    .lmo-board { padding: 8px 12px; }
    /* The names under the trick cards go, as on a phone: each card is already
       under the panel of the seat that played it. The flag line stays. */
    .lmo-trick {
        --lmo-playcw: 44px;
        --lmo-slot: calc(var(--lmo-playcw) * 1.39 + 20px);
        gap: 0 40px;
    }
    .lmo-play-name { display: none; }
    .lmo-stands { --lmo-standcw: 40px; }
    .lmo-stand { padding: 5px 9px 6px; }
    .lmo-handpanel .panel-head h3 { margin-bottom: 4px; }
    .lmo-handpanel #hand-cards .card { --cw: 64px; }
}

/* --- narrow screens ------------------------------------------------------ */
/* THE WHOLE TABLE ON ONE PHONE SCREEN. At 390x844 the bottom of the hand sat
   at 1272px, so a player scrolled between reading the table and playing a card
   on every turn. Each band below gives height back without dropping anything it
   said, and every band keeps a FIXED height through the deal (ui-conventions.md,
   "Containers must not move"): nothing that arrives mid-deal (a void, an Ace,
   the turn passing) changes a row's size. */
@media (max-width: 620px) {
    /* At a size where every status this game writes fits on one line (the
       longest is "Follow ♥ Hearts from any of your three hands."), with TWO
       reserved, because a hint takes the second. Reserving one let the hand step
       20px down the moment Hint was pressed, which is the moment the player
       reaches for the card it names. */
    .status.lmo-status { font-size: 15px; line-height: 1.3; min-height: 2.6em; }
    /* The scoreboard keeps the deal and the trick number. Its per-seat chips go:
       every seat's panel carries its own total, and the leader's is lit there. */
    .lmo-scoreboard .progress-score { display: none; }
    /* The chips were what spaced the deal from the trick number. */
    .lmo-scoreboard .lmo-progress-tricks::before { content: '\00b7\00a0'; }
    .lmo-scoreboard .lmo-progress-tricks { margin-left: 6px; }

    /* --- the seat strip ---------------------------------------------------- */
    /* Five columns will not fit: the three panels keep a row of their own and
       the two stands take the one under it, each still between its sharers. */
    .lmo-table {
        grid-template-columns: repeat(6, minmax(0, 1fr));
        grid-template-areas:
            'left   left   up     up     right right'
            '.      lstand lstand rstand rstand .';
        gap: 6px;
        margin-bottom: 8px;
    }
    .lmo-table.seats-3 {
        grid-template-areas:
            'left left left   right  right right'
            '.    .    lstand lstand .     .';
    }
    /* No arc: the raise was 22px of nothing on a screen this short. */
    .lmo-seat.pos-up { margin-bottom: 0; }
    /* The viewer's own panel, at the seat panels' phone sizes (the seat panels
       get theirs from the @container block, off the table's width). */
    .lmo-seat-name { font-size: 12.5px; }
    .lmo-seat-total { font-size: 15px; }
    .lmo-chip { padding: 1px 5px; }
    .lmo-seat-name.lmo-toact { padding: 0 5px; }
    .lmo-aces.cardrow { flex-wrap: nowrap; gap: 2px; }
    .lmo-aces .card { --cw: 15px; }
    .lmo-aces-label { display: none; }
    .lmo-suits { font-size: 11px; }

    /* The two stands nobody here shares: a one-line title, then the fan with
       its count beside it rather than under it. */
    /* The full width of its gap, so "Bot West and Bot North" fits one line. */
    .lmo-stand-hidden.lmo-side-left,
    .lmo-stand-hidden.lmo-side-right { justify-self: stretch; }
    .lmo-stand.lmo-stand-hidden {
        min-width: 0;
        padding: 3px 4px 4px;
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas: 'title title' 'cards count';
        column-gap: 6px;
        align-items: center;
    }
    .lmo-stand-hidden .lmo-stand-title {
        grid-area: title;
        font-size: 10px;
        margin-bottom: 2px;
        text-align: center;
    }
    .lmo-stand-hidden .lmo-stand-cards { grid-area: cards; min-height: 20px; }
    .lmo-stand-hidden .lmo-stand-cards .card { --cw: 14px; margin-right: -8px; }
    .lmo-stand-hidden .lmo-stand-count { grid-area: count; margin-top: 0; font-size: 10px; }

    /* --- the trick --------------------------------------------------------- */
    /* The diamond, smaller. The names under the cards go, because each card is
       already on the side of the panel that played it; the "won", "winning" and
       "no <suit>" line stays, and its slot is reserved without them. */
    .lmo-board { padding: 6px 8px; margin-bottom: 8px; }
    .lmo-trick {
        --lmo-playcw: 38px;
        --lmo-slot: calc(var(--lmo-playcw) * 1.39 + 18px);
        gap: 0 22px;
    }
    .lmo-play { gap: 3px; }
    .lmo-play-name { display: none; }
    .lmo-play-flag { line-height: 12px; }

    /* --- this player's own counters ------------------------------------------ */
    .lmo-mine {
        grid-template-rows: auto 21px;
        gap: 3px 8px;
        padding: 5px 8px 6px;
        margin-bottom: 8px;
    }

    /* --- this player's two stands -------------------------------------------- */
    /* STILL SIDE BY SIDE, so the one on the left is on the left, and each in ONE
       ROW: a stand never holds more than it was dealt (five at four seats, six at
       three), so the card width is whatever puts a full stand on one line. That
       is about 31px and 26px on a 390px phone, which is why they come from the
       simplified atlas (see playableCard). The side ("on your left") leaves the
       title so it stays one line; where the title sits says it. */
    .lmo-stands { --lmo-standcw: min(40px, calc(100vw / 10 - 8px)); gap: 6px; margin-bottom: 8px; }
    .lmo-stands.seats-3 { --lmo-standcw: min(40px, calc(100vw / 12 - 6px)); }
    .lmo-stand.lmo-stand-mine { padding: 4px 5px 5px; }
    .lmo-stand-mine .lmo-stand-cards { gap: 2px; flex-wrap: nowrap; }
    .lmo-stand-mine .lmo-stand-title {
        font-size: 10.5px;
        margin-bottom: 3px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .lmo-stand-side { display: none; }

    /* --- the hand -------------------------------------------------------------- */
    .lmo-handpanel { padding: 6px 10px 8px; margin-bottom: 10px; }
    .lmo-handpanel .panel-head { min-height: 0; }
    .lmo-handpanel .panel-head h3 { margin-bottom: 5px; font-size: 14px; }
    .lmo-handpanel .panel-head > .btn.small { top: -2px; }
}
/* At 360px the longest status took two lines at 15px, and with a hint three. */
@media (max-width: 389px) {
    .status.lmo-status { font-size: 14px; }
}
/* ==========================================================================
   Override
   ==========================================================================
   Two-to-four-player climbing from a standard pack, where a set's value is its
   card numbers read as digits. Every shared class (.card, .cardrow, .panel,
   .panel-head, .card.simple.back, .card.sel, .card.hinted, .card.clickable,
   .progress-score, .logline, .btn, .result-actions, .winner-banner,
   .result-note, ...) is reused as-is, so only what is genuinely Override's own
   lives here - all of it prefixed `ovr-`.

   THE ONE THING THIS LAYOUT HAS TO GET RIGHT: THE MIDDLE AND THE PICK-UP SIT
   SIDE BY SIDE. Beating a set hands you one of the cards you beat, and that is
   the rule players miss, so the pick-up is a panel of its own next to the middle
   rather than a corner of it. On a narrow screen it drops BELOW the middle rather
   than shrinking, because choosing a card to keep is a real decision and wants
   full-size cards.

   NOTHING IS OVERLAID ON A CARD FACE. Two of the thirty-seven faces do not print
   the number this game uses (the Ace is the 1, the Joker is the Cheat Code's 0), and an
   earlier draft badged every card with its number in the corner. The cards
   read better plain, so the translation is carried by the words instead - the
   rules panel, the middle's big value, the Play button and the log all speak in
   the game's numbers. Which is also why no rule here sets `position: relative` on a
   card: nothing needs an anchor. */

/* ---------- the opponents' strip ---------- */
.ovr-seats {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.ovr-seat {
    flex: 1 1 150px;
    max-width: 240px;
    padding: 7px 9px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
}
/* The seat that must act. Border only: a glow would nudge the strip's wrap. */
.ovr-seat-active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
/* Two cards or fewer means the hand is about to end and everything you are
   holding is about to be worth a point. Worth shouting about. */
.ovr-seat-close { border-color: #d4380d; }
.ovr-seat-name { font-weight: 700; font-size: 14px; }
.ovr-backs {
    display: flex;
    align-items: flex-end;
    margin: 5px 0 3px;
    min-height: 40px;
    flex-wrap: wrap;
}
.ovr-backs .card { --cw: 22px; margin-right: -13px; }
/* A card the whole table watched this seat pick up. It carries `.simple` in
   renderSeats, so the face is the flat rank-over-suit atlas rather than the
   photographic one: this is a note about what they are holding, not a card
   anybody can act on, and the plain art is what stays legible this small. Small
   enough to read as a note, with a gap of its own because it is real information
   rather than a count, and outlined so it cannot be mistaken for a card in
   play. */
.ovr-backs .card.ovr-known {
    --cw: 24px;
    margin-right: 2px;
    margin-left: 11px;
    outline: 1px solid var(--accent);
    outline-offset: 1px;
}
.ovr-backs .card.ovr-known:first-child { margin-left: 0; }
/* The card COUNT only: a seat's penalty total is on the scoreboard, and it used
   to be printed here as well. */
.ovr-seat-count { font-size: 12px; opacity: 0.75; }

/* ---------- the middle, and the card you take back ---------- */
.ovr-middle-row {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    align-items: flex-start;
}
.panel.ovr-middlepanel { flex: 1 1 320px; margin: 0; min-height: 150px; }
.panel.ovr-claimpanel {
    flex: 1 1 260px;
    margin: 0;
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}
.ovr-set { display: flex; gap: 6px; flex-wrap: wrap; padding: 4px 0; }
.ovr-set .card { --cw: 60px; }
/* The value the cards in the middle are worth: the number everything in the
   game is measured against, so it is the biggest thing in the panel. */
.ovr-value {
    font-size: 24px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
    color: var(--accent);
}
.ovr-empty { font-size: 13px; opacity: 0.7; padding: 12px 0; }
.ovr-note { font-size: 13px; opacity: 0.85; margin-top: 6px; }
.ovr-note.bad { color: #ff9a8a; opacity: 1; }
.ovr-note.ovr-warn { color: var(--accent); opacity: 1; }
.ovr-note.ovr-hint { color: var(--accent); opacity: 1; font-style: italic; }

/* ---------- the action bar ----------
   TWO ROWS OF RESERVED HEIGHT, AND THAT IS THE WHOLE POINT. The hand panel is
   the next element down the page, so anything this bar does to its own height
   slides the hand out from under the player's finger while they are still
   picking cards - and every control here appears and disappears with the
   selection (Play and Clear exist only once a card is picked). So renderActions
   puts buttons ONLY in `.ovr-controls` and text ONLY in `.ovr-notes`, and both
   rows hold their height whether or not they have anything in them. */
.ovr-actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    align-items: center;
    margin: 10px 0;
}
.ovr-controls,
.ovr-notes { flex: 1 1 100%; }
.ovr-controls {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    align-items: center;
    /* One row of .btn, held whether or not a button is in it. */
    min-height: 38px;
}
.ovr-notes {
    /* Two lines of 13px text at the 1.45 the page sets, so the common one and
       two line notes do not move anything, and only a rare third line does. */
    min-height: 2.9em;
    font-size: 13px;
    line-height: 1.45;
}
.ovr-notes .ovr-note { margin-top: 0; font-size: inherit; }
.ovr-notes .ovr-note + .ovr-note { margin-top: 4px; }

/* ---------- the hand ---------- */
/* Scoped through the class this game's page puts on #hand-panel, because the id
   belongs to every page. Unscoped, and with no media query to limit it, this one
   line set EVERY game's hand cards to 62px at every width, overriding the base
   72-96px curve the boards were tuned against. */
.ovr-handpanel #hand-cards .card { --cw: 62px; }
/* Cards that cannot be part of ANY legal play this turn (see renderHand). Shaded
   rather than disabled: assembling a set passes through illegal selections, and a
   card tapped by mistake still has to be tappable to untap. Desaturated as well as
   dimmed, because opacity alone is easy to miss against the felt. */
#hand-cards .card.ovr-dead {
    opacity: 0.34;
    filter: grayscale(0.85);
}
#hand-cards .card.ovr-dead.sel {
    /* A dead card that is somehow selected must still be obviously selected, or
       the player cannot see what to clear. */
    opacity: 0.75;
    filter: none;
}
/* One of your own cards that the whole table watched you pick up. A hairline, not
   a highlight: it is a standing fact about the card, not a call to action. */
#hand-cards .card.ovr-seen {
    box-shadow: 0 0 0 1px rgba(255, 209, 102, 0.75),
        0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}

/* ---------- what has left the hand ---------- */
.panel.ovr-discardpanel { margin-top: 10px; }
.ovr-discardrow { display: flex; gap: 3px; flex-wrap: wrap; padding: 4px 0; }
.ovr-discardrow .card { --cw: 30px; }
.card.ovr-mini { --cw: 30px; }

/* ---------- the scoreboard: LOW score wins, which is unusual here ---------- */
.progress.ovr-scoreboard {
    opacity: 1;
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    min-height: 34px;
}
.ovr-scorename { font-size: 13px; opacity: 0.75; }
.ovr-scorenum {
    display: inline-block;
    font-size: 22px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.ovr-scorecards { font-size: 12px; opacity: 0.6; }
/* `leading` marks the LOWEST total here, the opposite of every other scoreboard
   on the site, so the note spells it out rather than trusting the highlight. */
.progress-score.leading .ovr-scorenum { color: var(--accent); }
.ovr-scorenote {
    font-size: 12px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.55;
}

/* The final standings are lifted into a modal by the shell, for every game
   (GameShell.syncRoundOverlay, `#result-panel.result-overlay`). Override used to
   carry a duplicate of that geometry under `ovr-resultmodal`, from when the shell
   lifted a between-rounds summary only. */

/* ---------- the hand report and the final standings ---------- */
.ovr-report { flex: 1 1 100%; }
.ovr-report-title { font-weight: 700; margin-bottom: 4px; }
.ovr-report-line { font-size: 13px; font-variant-numeric: tabular-nums; }
.ovr-final { display: flex; flex-direction: column; gap: 4px; margin: 8px 0; }
.ovr-final-row {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 3px 8px;
    border-radius: 8px;
}
.ovr-final-winner { background: rgba(255, 209, 102, 0.14); }
.ovr-final-name { font-weight: 700; }
.ovr-final-score {
    font-variant-numeric: tabular-nums;
    font-weight: 800;
    font-size: 18px;
}

/* ---------- the log ---------- */
.logline.ovr-logout { color: var(--accent); font-weight: 700; }
.logline.ovr-loground { opacity: 0.9; font-style: italic; }

/* ---------- the rules and the Strategy tips ----------
   The Cheat_Sheet's lists carry `ovr-list` (pages/override.mjs), which reaches
   them on the setup screen, the board, and the Rules_Page alike; the element
   selectors match the shared `.rules ol` / `.rules ul li` rules' specificity,
   and this file loads after platform.css. The Strategy tips block keeps the
   `ovr-rules` class, so its list is styled the same way. */
.ovr-rules ol,
.ovr-rules ul,
ol.ovr-list,
ul.ovr-list { margin: 6px 0 0 18px; padding: 0; }
.ovr-rules li,
ol.ovr-list li,
ul.ovr-list li { margin-bottom: 5px; }

/* ---------- narrow screens ---------- */
@media (max-width: 620px) {
    /* The pick-up drops below the middle rather than shrinking: choosing which
       card to keep is a real decision and wants full-size cards. */
    .panel.ovr-claimpanel { flex: 1 1 100%; }
    .ovr-set .card { --cw: 46px; }
    .ovr-handpanel #hand-cards .card { --cw: 48px; }
    .ovr-backs .card { --cw: 18px; margin-right: -11px; }
    .ovr-backs .card.ovr-known { --cw: 22px; margin-left: 9px; }
    .ovr-seat { flex: 1 1 120px; }
    .ovr-value { font-size: 20px; }
    /* Three lines here, not two: the notes wrap sooner on a phone. */
    .ovr-notes { min-height: 4.3em; }
}

/* =========================================================================
   Balacious (src/balacious/) - every class prefixed `bal-`.

   The layout is driven by one unusual thing: THE PLAYER BOARD IS ON SCREEN THE
   WHOLE TIME. It is the input while the round's rules are being drafted and the
   reference afterwards, so the tiles have to be legible at a glance and also
   tappable - hence a grid of six dashed boxes whose tiles are buttons in one
   phase and plain blocks in the other, styled to look like the same object
   either way.
   ========================================================================= */
.bal-board-wrap { margin-bottom: 12px; }

/* ---------- the scoreboard ---------- */
.bal-score {
    display: flex;
    gap: 10px;
    align-items: stretch;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.bal-scorepill {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 6px 12px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--panel);
    min-width: 104px;
}
.bal-scorepill.active { border-color: var(--accent); }
.bal-scorename { font-weight: 700; font-size: 13px; }
.bal-scorepoints {
    font-size: 22px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1.05;
}
.bal-scorenote { font-size: 12px; opacity: 0.75; }

/* ---------- the pile chips ---------- */
.bal-piles .pile-chip::before { content: none; }
.bal-stockchip { border-style: dashed; }

/* ---------- the player board ----------
   Two columns of dashed boxes on a wide screen, one on a narrow one. The dashes
   are the printed board's own idiom and they carry the rule: a box is a group
   that takes ONE cube. */
.bal-board {
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--panel);
    padding: 8px 10px 10px;
    margin-bottom: 10px;
}
.bal-boardhead {
    display: flex;
    gap: 10px;
    align-items: baseline;
    flex-wrap: wrap;
    margin-bottom: 8px;
}
.bal-boardtitle { font-weight: 700; font-size: 14px; }
.bal-boardnote { font-size: 12px; opacity: 0.75; }
.bal-boxes {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
}
.bal-box {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 5px;
    border: 1.5px dashed color-mix(in srgb, var(--accent) 55%, transparent);
    border-radius: 10px;
}
/* A box with a cube in it is settled, and says so by going solid rather than by
   fading: fading the BOX would take the ringed tile inside it down too, and the
   whole point of the ring is that the round's four rules stay at full strength.
   The fade belongs to the individual unchosen tiles (.bal-tile.dim). */
.bal-box.filled { border-style: solid; }
/* Two words per box, so four tiles of small print get something to hang off. */
.bal-boxlabel {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.7;
    padding: 0 2px 1px;
}

.bal-tile {
    display: flex;
    gap: 7px;
    align-items: flex-start;
    width: 100%;
    text-align: left;
    padding: 5px 7px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--bg);
    color: inherit;
    font: inherit;
}
button.bal-tile.live { cursor: pointer; }
button.bal-tile.live:hover,
button.bal-tile.live:focus-visible {
    border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 12%, var(--bg));
}
/* CHOSEN: ringed, and the ring is the whole signal. A round's rules are four
   tiles out of twelve, so they have to be findable at a glance from across the
   board rather than read for. */
.bal-tile.taken {
    border-color: var(--accent);
    border-width: 2px;
    background: color-mix(in srgb, var(--accent) 16%, var(--bg));
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 32%, transparent);
}
/* NOT CHOSEN: faded. During the draft that is an option shut out by a cube
   elsewhere in its box; once play starts it is every option that did not get one.
   Grayed rather than removed, because "this is not in play" is information, and
   the printed board keeps them on the paper too. */
.bal-tile.dim { opacity: 0.4; }
.bal-tile.hinted {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.bal-tilenum {
    flex: 0 0 auto;
    width: 19px;
    height: 19px;
    border: 1.3px solid currentColor;
    border-radius: 50%;
    font-size: 11px;
    font-weight: 800;
    line-height: 17px;
    text-align: center;
    opacity: 0.8;
}
.bal-tile.taken .bal-tilenum {
    opacity: 1;
    border-width: 2px;
    border-color: var(--accent);
    line-height: 15px;
}
.bal-tilebody {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}
.bal-tiletitle { font-size: 12.5px; font-weight: 600; line-height: 1.25; }
.bal-tileeg { font-size: 11.5px; opacity: 0.7; font-variant-numeric: tabular-nums; }
/* The private count of what this hand holds for the option. The whole drafting
   decision, so it gets the accent rather than being another gray line. */
.bal-tilecount {
    font-size: 11.5px;
    color: var(--accent);
    font-weight: 600;
}
.bal-cube {
    flex: 0 0 auto;
    width: 12px;
    height: 12px;
    margin-left: auto;
    border-radius: 3px;
    background: var(--accent);
    align-self: center;
}

/* ---------- what the cubes did, in sentences ---------- */
.bal-rulesnow {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-bottom: 10px;
    padding: 6px 10px;
    border-left: 3px solid var(--accent);
    background: color-mix(in srgb, var(--accent) 7%, transparent);
    border-radius: 0 8px 8px 0;
}
.bal-ruleline { font-size: 12.5px; }

/* ---------- the opponent ---------- */
.bal-opp {
    border: 1px solid var(--line);
    border-radius: 10px;
    padding: 7px 10px;
    background: var(--panel);
    margin-bottom: 10px;
}
.bal-opphead {
    display: flex;
    gap: 10px;
    align-items: baseline;
    flex-wrap: wrap;
    margin-bottom: 6px;
}
.bal-oppname { font-weight: 700; }
.bal-oppcount { font-size: 13px; opacity: 0.8; }
.bal-oppdealer {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 1px 6px;
    border: 1px solid var(--line);
    border-radius: 999px;
    opacity: 0.8;
}
.bal-opprow { flex-wrap: wrap; gap: 4px; align-items: center; }
/* Overlapped backs: a seventeen-card hand has to fit without shrinking the
   table's own cards. */
.card.bal-oppback { --cw: 30px; margin-right: -16px; }
.card.bal-oppback:last-of-type { margin-right: 0; }

/* ---------- the table ---------- */
.bal-table { margin-top: 4px; }
.bal-tablelabel { font-size: 14px; margin-bottom: 6px; min-height: 2.6em; }
.bal-tablerow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    flex-wrap: wrap;
    min-height: 104px;
}
.bal-play { display: flex; flex-direction: column; gap: 4px; opacity: 0.6; }
.bal-play.top { opacity: 1; }
.bal-playwho { font-size: 12px; }
.bal-play.top .bal-playwho { font-weight: 700; color: var(--accent); }
.bal-playcards { gap: 5px; }
.bal-playcards .card { --cw: 52px; }
.bal-empty { font-size: 13px; opacity: 0.6; font-style: italic; }

/* ---------- the hand ---------- */
.bal-handrow { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }
.bal-handrow #hand-cards { flex: 1 1 320px; }
.bal-actions {
    flex: 0 0 232px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    align-items: stretch;
}
.bal-actionnote { font-size: 12.5px; opacity: 0.85; }
.bal-actionnote.bad { color: var(--red); opacity: 1; }
.bal-hint { color: var(--accent); opacity: 1; }

/* The inventory line: what the hand holds, and what of it the board forbids. */
.bal-handfacts {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    margin: 2px 0 8px;
}
.bal-fact {
    font-size: 11.5px;
    padding: 1px 8px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--bg);
}
/* A combination the hand HAS and the board does not allow. Kept visible on
   purpose - during the draft it is the argument for a cube. */
.bal-fact.off { opacity: 0.6; border-style: dashed; }
.bal-factoff { opacity: 0.8; }

/* Cards that cannot take part in any legal play this turn. filter rather than
   opacity, per the workspace note: these sit in a flat wrap row today, but a
   faded card in an overlapping fan reads as a hole. */
.card.bal-dim { filter: grayscale(0.75) brightness(0.68); }

/* ---------- the round summary ---------- */
.bal-roundsummary { display: flex; flex-direction: column; gap: 4px; }
.bal-roundline { font-size: 13.5px; }
.bal-roundrules { margin: 2px 0 2px 18px; padding: 0; font-size: 12.5px; opacity: 0.9; }
.bal-roundrules li { margin-bottom: 2px; }
.bal-roundtotals { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 4px; }
.bal-roundtotal { font-weight: 700; }

/* ---------- the log ---------- */
.logline.bal-logpick { color: var(--accent); }
.logline.bal-logpass { opacity: 0.85; font-style: italic; }

/* ---------- the rules and the printed-aid reference ----------
   The Cheat_Sheet's Play list carries `bal-list` (pages/balacious.mjs), which
   reaches it on the setup screen, the board, and the Rules_Page alike; the
   element selectors match or outrank the shared `.rules ol` / `.rules li`
   rules, and this file loads after platform.css. Meld types is the shared
   `.reflist`, which carries no `bal-list` and whose `.cheatsheet ul.reflist`
   rules outrank all of these. Suit glyphs in the sheet take the shared
   `.rules.cheatsheet .red` / `.club` colors, so no suit rule lives here. */
ol.bal-list { margin: 6px 0 0 18px; padding: 0; }
ol.bal-list li { margin-bottom: 5px; }
/* The reference mirrors the printed player aid: the same six dashed boxes, so
   the sheet and the live board teach each other. */
.bal-ref {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
    margin-top: 8px;
}
.bal-refbox {
    border: 1.5px dashed color-mix(in srgb, var(--accent) 55%, transparent);
    border-radius: 10px;
    padding: 7px 9px;
}
.bal-refhead {
    font-weight: 700;
    font-size: 12.5px;
    margin-bottom: 4px;
    color: var(--accent);
}
.bal-refrow {
    font-size: 12.5px;
    display: flex;
    gap: 8px;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 2px;
}
.bal-refeg { opacity: 0.75; font-variant-numeric: tabular-nums; white-space: nowrap; }
.bal-refnote { font-size: 11.5px; opacity: 0.85; margin-top: 4px; }

/* ---------- narrow screens ---------- */
@media (max-width: 720px) {
    /* One column of boxes, and the action column drops below the cards rather
       than squeezing them: choosing which reading to play is the decision and
       wants full-size faces. */
    .bal-boxes,
    .bal-ref { grid-template-columns: minmax(0, 1fr); }
    .bal-actions { flex: 1 1 100%; }
    .bal-handpanel #hand-cards .card { --cw: 48px; }
    .bal-playcards .card { --cw: 46px; }
    .card.bal-oppback { --cw: 20px; margin-right: -12px; }
}

/* =========================================================================
   Nana (nana, nana-)
   Layout intent: the players sit AROUND the middle, and the middle of the ring
   carries the dealt grid with THE RUN and WHAT WINS beside it rather than above
   it. That last part is a height decision, not a taste one: the center of the
   ring is about 830px wide at four seats on a desktop and the grid needs about
   230px, so stacking the two reading panels over the grid spent 150px of page
   height to leave 600px of the widest column empty, and the board did not fit on
   a laptop screen.
   ANTI-JITTER IS THE OTHER THING THIS FILE IS FOR. A seat's band grows as cards
   are revealed from it, and at four seats the top band is grid row 1, so its
   growth used to push the middle, the hand and everything below it down the page
   by up to 92px WHILE THE PLAYER WAS REACHING FOR A CARD. Every row inside a seat
   band therefore reserves its tallest shape, in terms of --nana-* card widths so
   the reservations follow the card scaling. Do not size one of them to its
   contents.
   Two things here are unlike the sibling games and are deliberate:
   .nana-tile is a card-shaped tile carrying a NUMBER, not a card face. What the
   table deduces is a number and never a card - nobody saw the suit - so drawing
   a real face would be telling the player something the table does not know. It
   is built on the same `card simple back` idiom as every face-down card, with
   the number laid over it, so a remembered card still reads as face down.
   .nana-gridrow sets its column count from --nana-cols, written by the renderer:
   the deal sizes run from 6 to 12 cards and a fixed grid would strand a row.
   ========================================================================= */
/* Two lines, because the status line says what to do and the longest of those
   sentences wraps once at a tablet width (and a long online name can wrap the
   result sentence at any width). Three at the phone breakpoint, where it was
   measured at 76px. The class is added by the renderer, not by the page: the
   shell owns this element. */
.status.nana-status { min-height: 2.9em; }
/* There is no scoreboard above the board: every seat's sets lie face up in that
   seat's own band, so a strip at the top counted the same cards twice. The
   renderer empties and hides #progress-line; nothing styles it. */
/* --- the table, laid out as a table ------------------------------------- */
/* The players sit AROUND the middle. The viewer is always the bottom band, the
   next player is the right column, the last is the left, and anybody between
   them runs along the top - so the direction the turn travels is a fact about
   the screen. Side and top slots collapse to nothing when a smaller table
   leaves them empty, which is why the outer columns are `auto`. */
.nana-board {
    /* The card sizes the seat bands are built from, and the reservations below
       are written in terms of them, so changing one moves both together. Both are
       painted from the simplified atlas (see def.ts): a set is a token at this
       size and a revealed card is read for its number, so full art would be a
       smudge at 24px and nearly one at 34px. */
    --nana-setcw: 24px;
    --nana-showncw: 34px;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
        ".      top     ."
        "left   center  right"
        "bottom bottom  bottom";
    gap: 8px;
    align-items: start;
    justify-items: stretch;
}
.nana-slot-top {
    grid-area: top;
    display: flex;
    gap: 8px;
    justify-content: center;
    flex-wrap: wrap;
}
.nana-slot-left { grid-area: left; }
.nana-slot-right { grid-area: right; }
.nana-slot-bottom { grid-area: bottom; }
/* The dealt grid at its own width, the two reading panels filling what is left.
   `align-items: start` so neither stretches to the other's height. */
.nana-center {
    grid-area: center;
    min-width: 0;
    display: grid;
    grid-template-columns: auto minmax(210px, 1fr);
    gap: 8px;
    align-items: start;
}
/* THE RUN AND WHAT WINS SIT SIDE BY SIDE WHERE THERE IS ROOM, and wrap into a
   column where there is not. Both are short and neither needs the whole width, so
   stacking them made the center column the tallest thing on the page (302px
   against the dealt grid's 176px) for no reason. It wraps on VIEWPORT width only,
   which is not the movement the reservations above are about: nothing here
   changes shape as a turn is played. */
.nana-read {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: flex-start;
    min-width: 0;
}
.nana-read > .panel { flex: 1 1 230px; min-width: 0; }
/* A seat on a side or the top is a narrow column; the viewer's own band gets
   the full width, because it carries the buttons they actually press. */
.nana-slot-left .nana-seat,
.nana-slot-right .nana-seat,
.nana-slot-top .nana-seat { width: 196px; }
.nana-panelhead {
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    opacity: 0.6;
    margin-bottom: 6px;
}
/* --- the run ------------------------------------------------------------ */
/* The height is pinned so nothing beside or below it jumps as a run grows: an
   empty run, a four-card run, and a review carrying a sentence, a button and the
   "remember these" note all occupy the same band. */
/* Reserved for the REVIEW state measured in a browser: head, the run of chips, a
   two-line sentence and the button. Reserving what the playing state needs left
   11px of movement on every review. The "remember these" note is a whole extra
   line, but it exists for a whole game or not at all (it belongs to the
   face-down-again option), so it gets its own reservation rather than making
   every public-information game carry the space for it. */
.nana-chase {
    padding: 9px 11px;
    min-height: 10.6em;
}
.nana-chase.nana-chase-note { min-height: 12.4em; }
.nana-runrow {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: 6px;
    min-height: 1.9em;
}
/* The number chip: the unit of public knowledge in this game. */
.nana-num {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.6em;
    padding: 1px 5px;
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.1);
    border: 1px solid var(--panel-border);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    font-size: 13px;
}
.nana-runnum { font-size: 17px; min-width: 1.9em; padding: 3px 7px; }
.nana-runlast { border-color: rgba(255, 255, 255, 0.45); }
/* The card that broke the run, which is the whole story of a failed turn. */
.nana-runbreak {
    color: var(--red);
    border-color: var(--red);
    background: rgba(212, 0, 0, 0.14);
}
/* A card still to find, so the length of the run reads as progress. It carries no
   text, so it needs its height stating: left to size itself an empty inline-flex
   box collapsed to its padding and the row read as a strikethrough rather than as
   two empty places. The figures match .nana-runnum beside it. */
.nana-runslot {
    font-size: 17px;
    min-width: 1.9em;
    min-height: 1.65em;
    padding: 3px 7px;
    background: none;
    border-style: dashed;
    opacity: 0.45;
}
.nana-chaseline { font-size: 13.5px; margin-bottom: 6px; }
.nana-chasenote { font-size: 12px; opacity: 0.7; margin-top: 6px; }
.nana-ok { margin-top: 2px; }
/* --- the middle --------------------------------------------------------- */
.nana-middle { padding: 9px 11px; }
.nana-gridrow {
    display: grid;
    grid-template-columns: repeat(var(--nana-cols, 5), minmax(0, max-content));
    gap: 5px;
    justify-content: center;
}
.nana-gridrow .card { --cw: 46px; }
.nana-cell { position: relative; }
button.nana-cell { padding: 0; border: none; cursor: pointer; }
.card.nana-armed { box-shadow: 0 0 0 2px var(--accent); }
.card.nana-hole { opacity: 0.4; }
.card.nana-open { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.45); }
/* --- number tiles (a remembered card) ----------------------------------- */
.nana-tile { position: relative; }
/* The number sits over the back rather than replacing it: the card IS still
   face down, and it has to keep reading that way. */
.nana-tilenum {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    font-size: 13px;
    background: rgba(12, 12, 12, 0.82);
    border-color: rgba(255, 255, 255, 0.5);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.card.nana-tile-known { filter: brightness(0.78) saturate(0.6); }
/* A DEDUCED bound rather than a number the table was shown: "their lowest is
   above the 3 that has gone into a set". Drawn where a seen number would be, so
   the eye reads the row as one hand, but as a comparison and in the accent, so a
   bound cannot be mistaken for a card anybody has actually seen. */
.nana-tilebound {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    font-size: 11px;
    min-width: 0;
    padding: 1px 3px;
    color: var(--accent);
    border-color: var(--accent);
    background: rgba(12, 12, 12, 0.88);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.card.nana-tile-bound { filter: brightness(0.88); }
/* An end of the hand, which is also the button that asks for it. */
button.card.nana-tile-armed {
    padding: 0;
    border: none;
    cursor: pointer;
    box-shadow: 0 0 0 2px var(--accent);
}
/* --- the seats ---------------------------------------------------------- */
.nana-seat {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 6px 8px;
    border: 2px solid transparent;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.03);
    box-shadow: inset 0 0 0 1px var(--panel-border);
    min-width: 0;
}
.nana-seat.nana-seat-mine { background: rgba(255, 255, 255, 0.06); }
/* WHOSE TURN IT IS. The loudest thing on the board on purpose: the first
   complaint about this game was not knowing. */
.nana-seat.nana-seat-turn {
    border-color: var(--accent);
    background: rgba(255, 255, 255, 0.09);
    box-shadow: inset 0 0 0 1px var(--accent), 0 0 12px rgba(0, 0, 0, 0.35);
}
/* The viewer's own band is a row, not a column: it is the widest slot and it
   carries the controls. */
.nana-slot-bottom .nana-seat {
    display: grid;
    grid-template-columns: minmax(130px, auto) minmax(0, 1fr) auto;
    grid-template-areas:
        "head sets asks"
        "hand hand hand";
    gap: 3px 12px;
    align-items: center;
}
.nana-slot-bottom .nana-seathead { grid-area: head; }
.nana-slot-bottom .nana-setrow { grid-area: sets; }
.nana-slot-bottom .nana-handrow { grid-area: hand; }
.nana-slot-bottom .nana-asks { grid-area: asks; }
/* NOWRAP, and this one is load-bearing. The head gains an "on turn" badge when the
   turn reaches the seat, and wrapping meant a 196px side column went to two lines
   at that moment: the band grew 18px, and at four seats the top band is grid row
   1, so the whole board below it stepped down exactly as the turn arrived. The
   name is what gives way instead, because an online peer's name can be any length
   and nothing else here can shrink. */
.nana-seathead {
    display: flex;
    flex-wrap: nowrap;
    gap: 2px 7px;
    align-items: baseline;
    min-width: 0;
}
.nana-seatname {
    font-size: 14px;
    font-weight: 700;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.nana-seatcount { font-size: 12px; opacity: 0.6; white-space: nowrap; }
.nana-seatturn {
    font-size: 11px;
    font-weight: 800;
    color: var(--accent);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    white-space: nowrap;
    flex: none;
}
/* RESERVED FOR ONE SET, not sized to none. The row goes from a line of text to a
   fan of cards the first time a seat takes a set, and without this that growth
   walked the whole board down the page. 1.39 is the card aspect. */
.nana-setrow {
    display: flex;
    flex-wrap: nowrap;
    gap: 4px 8px;
    align-items: center;
    min-height: calc(var(--nana-setcw) * 1.39);
    overflow-x: auto;
    scrollbar-width: none;
}
.nana-setrow::-webkit-scrollbar { display: none; }
.nana-setnone { font-size: 12px; opacity: 0.45; white-space: nowrap; }
/* A completed set is fanned tight: it is a token now, not cards to read. */
.nana-set { display: flex; flex: none; }
.nana-set .card.nana-setcard {
    --cw: var(--nana-setcw);
    margin-right: calc(var(--nana-setcw) * -0.62);
}
.nana-set .card.nana-setcard:last-child { margin-right: 0; }
/* RESERVED FOR A REVEALED CARD, which is the tallest thing this row ever holds
   and the one that arrives mid-turn. The number tiles are half its height, so
   sizing to contents made the row jump on every single reveal. */
.nana-handrow {
    display: flex;
    flex-wrap: nowrap;
    gap: 3px;
    align-items: center;
    min-height: calc(var(--nana-showncw) * 1.39);
    overflow-x: auto;
    scrollbar-width: none;
}
.nana-handrow::-webkit-scrollbar { display: none; }
.nana-handrow .card.nana-tile { --cw: 22px; flex: none; }
/* A card face up in front of its owner: bigger than the tiles beside it,
   because it is the only real information on the card. */
.nana-handrow .card.nana-shown {
    --cw: var(--nana-showncw);
    flex: none;
    margin: 0 2px;
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.45);
}
.nana-handhere { font-size: 12px; opacity: 0.55; white-space: nowrap; }
/* nowrap for the same reason as the head: two buttons dropping to two lines is a
   band that changes height, and both fit a 196px column. */
.nana-asks { display: flex; gap: 6px; flex-wrap: nowrap; }
.btn.nana-ask { white-space: nowrap; }
/* --- what wins --------------------------------------------------------- */
/* Reserved, like the run beside it: the winning-numbers row appears the moment a
   seat has a set, and the line under it changes length as the game goes on. */
/* 7em covers the measured tallest: the head, the winning-numbers row, and a line
   that runs to three at a narrow panel width. 5.6em was short by 17px, and the
   panel grew by that much mid-game. */
.nana-goal {
    padding: 9px 11px;
    min-height: 7em;
}
.nana-goalwins {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    margin-bottom: 5px;
    min-height: 1.5em;
}
.nana-goalwin { color: var(--accent); border-color: var(--accent); }
.nana-goalline { font-size: 12.5px; }
/* --- the hand ---------------------------------------------------------- */
/* A hand is held in numerical order by rule, so cards that are not at an end
   fade: the two ends are the only cards that can ever be revealed, and that is
   the constraint the whole game is built on. */
/* A Nana hand runs to 14 cards and is read for its NUMBERS, and only its two ends
   can ever be played, so it does not need the site's full card width: 62px keeps
   every rank legible (well clear of the 46px the full art needs) and buys back
   40px of page height, which is what stopped the board fitting a laptop screen.
   Scoped through the class this game's page puts on #hand-panel, because
   #hand-cards is on every page and an unscoped rule resizes every game's hand. */
.nana-handpanel #hand-cards .card { --cw: 62px; }
#hand-cards .card.nana-buried { filter: brightness(0.6) saturate(0.65); }
#hand-cards .card.nana-end { box-shadow: 0 0 0 2px var(--accent); }
/* WHICH OF YOUR OWN CARDS THE TABLE HAS SEEN, marked on the card itself. The
   class is scope enough on its own (only this game emits it), so it needs no id.
   Pale rather than accent-colored on purpose: the accent already means "you can
   play this end" two cards away, and the two must not read as one thing. */
.card.nana-seenbyall { position: relative; }
.card.nana-seenbyall::after {
    content: '';
    position: absolute;
    top: 4px;
    right: 4px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.92);
    box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.72);
}
/* An empty hand is a legal position (you keep taking turns with no cards), so the
   tray says so rather than going blank. Scoped through the page's own class,
   because #hand-cards is on every page. */
.nana-handpanel .nana-handempty {
    font-size: 12.5px;
    opacity: 0.65;
    max-width: 42ch;
    text-align: center;
}
/* --- the log ----------------------------------------------------------- */
.nana-lognum { margin-left: 2px; font-size: 12px; }
.nana-logset { font-weight: 700; }
.nana-logover { font-weight: 700; color: var(--accent); }
/* --- the result -------------------------------------------------------- */
.nana-summary .winner-banner { font-size: 21px; }
.nana-result .result-row {
    display: grid;
    grid-template-columns: minmax(110px, auto) minmax(0, 1fr);
    gap: 8px;
    align-items: center;
}
.nana-resultname { font-size: 13.5px; }
.nana-resultsets { display: flex; flex-wrap: wrap; gap: 5px; }
.result-row.nana-won .nana-resultname { color: var(--accent); font-weight: 800; }
/* --- the rules tables -------------------------------------------------- */
.nana-dealtable {
    border-collapse: collapse;
    margin: 8px 0;
    font-size: 12.5px;
}
.nana-dealtable th,
.nana-dealtable td {
    border: 1px solid var(--panel-border);
    padding: 3px 8px;
    text-align: left;
}
.nana-dealtable th { opacity: 0.75; font-weight: 700; }
.nana-deal-n { text-align: right; font-variant-numeric: tabular-nums; }
/* ---------- narrow screens ----------
   A ring needs width, so it unrolls into one column - but in TURN ORDER, which
   is what the ring was there to show: the middle, then the next player, then
   round the top to the last one, then you. */
@media (max-width: 720px) {
    .nana-board {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "center" "right" "top" "left" "bottom";
    }
    /* No width to put the dealt grid and the reading panels side by side, so
       they stack: the grid, the run, then what wins. */
    .nana-center { grid-template-columns: minmax(0, 1fr); }
    .nana-slot-left .nana-seat,
    .nana-slot-right .nana-seat,
    .nana-slot-top .nana-seat { width: auto; }
    .nana-slot-top { flex-direction: column; }
    /* The viewer's band drops to two rows: three columns at this width squeezed
       the ask buttons to one word per line. */
    .nana-slot-bottom .nana-seat {
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas: "head asks" "sets sets" "hand hand";
    }
    .nana-gridrow .card { --cw: 40px; }
    /* Re-derived at this width rather than inherited: the reservations above are
       written in terms of these two, so they follow without restating them. */
    .nana-board { --nana-showncw: 32px; --nana-setcw: 22px; }
    /* Every reservation whose content REFLOWS has to be re-derived here rather
       than inherited, because a narrower box wraps the same text onto more lines.
       Both figures are measured at 390px. */
    .status.nana-status { min-height: 4.3em; }
    .nana-goal { min-height: 8.4em; }
    /* Scoped through the class this game's page puts on #hand-panel, because the
       id belongs to every page. Unscoped, this resized every game's hand. */
    .nana-handpanel #hand-cards .card { --cw: 44px; }
}

/* ==========================================================================
   Palace
   ==========================================================================
   Two-to-five-player shedding: three cards in hand, three face up on a row and
   three face down under them. Every shared class (.card, .cardrow, .panel,
   .panel-head, .pile-chip, .card.empty, .card.simple.back, .card.sel,
   .card.hinted, .card.clickable, .logline, .btn, .result-actions,
   .winner-banner, .result-note, ...) is reused as-is, so only what is genuinely
   Palace's own lives here - all of it prefixed `palace-`.

   THREE THINGS THIS STYLESHEET HAS TO SOLVE, and they are the whole file:

   1. READING THE PILE. Palace's pile is not a stack you glance at, it is
      something you have to PARSE: the card that must be answered is not
      necessarily the top one, because a 3 is transparent and a stack of 3s just
      as much so. So the answering card is ringed and the 3s over it are drawn
      as see-through. Without that the player has to count 3s by eye, every turn.

   2. CARDS IN SOMEBODY ELSE'S HAND. Anything the table watched a player take
      off the pile is public, so it is painted FACE UP in their panel behind a
      "seen:" label, next to one back standing in for the private remainder.
      This is the only game here that draws an opponent's actual cards as a
      matter of course, and it needs to read as information rather than as a
      leak - hence the label and the tinted frame.

   3. FADING WITHOUT OPACITY. Unplayable hand cards and the transparent 3s are
      both dimmed with `filter`, never `opacity`. The pile is an overlapping fan
      and the hand can wrap into overlapping rows; a translucent card in an
      overlap shows the card underneath through it, which looks like a rendering
      fault rather than a hint. */

/* ---------- the opponents' strip ---------- */
.palace-table {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: center;
    margin-bottom: 10px;
}
.panel.palace-seat {
    flex: 1 1 190px;
    max-width: 300px;
    padding: 7px 9px;
    margin: 0;
}
/* The seat that must act. Border only: a glow would move its neighbors. */
.panel.palace-seat.palace-acting {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}
/* Out of the game, and kept on screen rather than removed: who got away, and in
   what order, is the score. */
.panel.palace-seat.palace-outseat { filter: brightness(0.82) saturate(0.7); }
.palace-seat-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 6px;
}
.palace-seat-name { font-weight: 700; font-size: 13px; }
.palace-seat-held {
    font-size: 12px;
    opacity: 0.7;
    font-variant-numeric: tabular-nums;
}
.palace-seat-hand {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px 6px;
    margin: 4px 0 5px;
    min-height: 26px;
}
/* THE WHOLE HAND ON ONE LINE: backs for the private cards, faces for the ones
   the table has seen. Two separate groups (a back with a count, then a "seen:"
   strip) read as two different facts - "one unknown and two seen" rather than
   "a hand of three we know two of" - so the hand is drawn card by card and the
   count is simply how long the line is. */
.palace-seatcards {
    justify-content: center;
    flex-wrap: wrap;
    gap: 3px;
}
.palace-seat-count { font-size: 12px; opacity: 0.8; }
/* One back standing in for a whole private hand: the number is in words, because
   a back per card would mean thirty nodes after a big pickup. */
.card.palace-tiny { --cw: 20px; }

/* ---------- cards the whole table has seen ----------
   The feature the knowledge tracking exists for. These came off the pile in
   front of everybody, so drawing them is the memory a player at a table would
   have - and the bots have it whether or not the screen does. */
.card.palace-known { box-shadow: 0 0 0 1.5px var(--accent); }
/* The same fact from the other side: what the table knows YOU are holding. */
.card.palace-mine-known { box-shadow: 0 0 0 2px var(--accent); }

/* ---------- a row: three STACKS of face-up over face-down ----------
   Drawn as the physical stack it is, the face-down card splayed out behind its
   cover. That is also the count of blind cards a seat has left, which is public
   information and one of the things a player reads most often. */
.palace-row { display: flex; gap: 8px; align-items: flex-start; justify-content: center; }
/* The column sizes itself to the FRONT card, with the back taken out of flow
   behind it so the splay costs no width; the padding is what stops the offset
   copy being clipped by the panel edge. */
.palace-col { position: relative; padding: 0 7px 7px 0; }
.palace-col .card { position: relative; z-index: 1; }
.card.palace-under {
    position: absolute;
    left: 7px;
    top: 7px;
    z-index: 0;
}
/* Nothing in front of it any more, so it comes back into flow and is the card
   that gets turned over next. */
.palace-col .card.palace-under:only-child {
    position: relative;
    left: 0;
    top: 0;
}
/* The blind card you may turn over now. It is the one irreversible press in the
   game, so it is marked - and the cursor and the title say so too. */
.card.palace-blind { box-shadow: 0 0 0 2px var(--accent); }
.panel.palace-seat .palace-row .card { --cw: 32px; }
.panel.palace-seat .palace-col { padding: 0 5px 5px 0; }
.panel.palace-seat .card.palace-under { left: 5px; top: 5px; }
/* `.card.palace-upcard` is deliberately style-free: it is a semantic hook the
   app test and any later styling key on, and a face-up row card should look
   exactly like a card. */

/* ---------- the middle: pile, burn pile, actions ---------- */
/* THE WHOLE BOARD IS CENTERED. Everything a Palace player reads is one column of
   things of different widths - a seat strip that wraps, a pile that grows a card
   at a time, a three-card row, a hand of three that becomes a hand of thirty -
   and left-aligning them made the pile drift away from the row below it as it
   grew. Centering is what keeps the pile, the row and the hand on one axis
   whatever their size. */
.palace-middle {
    display: flex;
    gap: 12px;
    align-items: flex-start;
    justify-content: center;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.palace-pilecol { flex: 1 1 340px; min-width: 0; }
.panel.palace-pilepanel { margin: 0; }
.palace-chips {
    display: flex;
    justify-content: center;
    gap: 6px;
    flex-wrap: wrap;
    margin-bottom: 6px;
}
.palace-chip-quiet { opacity: 0.65; }
.palace-fan { align-items: flex-start; justify-content: center; min-height: 68px; }
.palace-empty { font-size: 12.5px; opacity: 0.7; align-self: center; }
.palace-buried {
    font-size: 11.5px;
    opacity: 0.6;
    align-self: center;
    padding-right: 4px;
}
.card.palace-piled { --cw: 52px; }
/* THE CARD THAT MUST BE ANSWERED - not always the top one, because a 3 is
   transparent. Ringed, because working it out by eye every turn is the one
   avoidable difficulty in the game. */
.card.palace-answer { box-shadow: 0 0 0 2.5px var(--accent); z-index: 2; }
/* The 3s stacked over it, drawn as what they are: not there. `filter`, never
   `opacity` - see the header. */
.card.palace-seethrough {
    filter: saturate(0.25) brightness(1.06) contrast(0.8);
    outline: 1.5px dashed var(--panel-border);
    outline-offset: -3px;
}
.palace-pilenote {
    font-size: 12.5px;
    opacity: 0.85;
    margin-top: 7px;
    line-height: 1.45;
    text-align: center;
}
.palace-warn {
    opacity: 1;
    color: var(--red);
    font-weight: 600;
}

/* ---------- out of the game ----------
   A burn happens inside the same engine call as the play that caused it, so
   without a box for the cards to fly TO, the biggest event in the game would
   look like the pile silently vanishing. */
/* Below the hand, not beside the pile: a record rather than something you act
   on (see the comment in pages/palace.mjs). */
.panel.palace-outpanel { margin: 0 0 10px; }
.palace-outcount { font-size: 12px; opacity: 0.7; }
.palace-banner {
    font-size: 12.5px;
    font-weight: 600;
    color: var(--accent);
    margin: 4px 0 6px;
    line-height: 1.4;
}
.palace-outrow { align-items: center; justify-content: center; min-height: 44px; }
.card.palace-burned { --cw: 34px; filter: saturate(0.55) brightness(0.9); }

/* ---------- the action area ---------- */
.palace-actions {
    flex: 1 1 260px;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 7px;
}
.palace-note { font-size: 12.5px; opacity: 0.88; line-height: 1.45; }
.palace-note.bad { color: var(--red); opacity: 1; }
.palace-hint { color: var(--accent); font-weight: 600; opacity: 1; }
/* Taking the pile on purpose is a real move, but it is not the usual one, so its
   explanation is quieter than the rest of the area. */
.palace-takenote { font-size: 12px; opacity: 0.65; }
.palace-actions .btn.hinted { box-shadow: 0 0 0 2px var(--accent); }

/* ---------- the turned-over card ---------- */
.palace-revealbox {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.card.palace-revealed { --cw: 66px; box-shadow: 0 0 0 2.5px var(--accent); }
.palace-collabel {
    font-size: 10.5px;
    opacity: 0.6;
    line-height: 1.1;
    text-align: center;
}

/* ---------- the viewer's own row ---------- */
.panel.palace-minepanel { margin: 0 0 10px; }
.palace-myrow { margin: 6px 0 4px; }
/* Your own face-down cards, opened up once the game is decided. */
.card.palace-wasdown { box-shadow: 0 0 0 2px var(--panel-border); }
.palace-myrow .card { --cw: 52px; }
.palace-minenote {
    font-size: 12px;
    opacity: 0.7;
    line-height: 1.45;
    text-align: center;
}
/* A hand card that will not go. Dimmed rather than disabled, because during the
   opening face-up selection every card is choosable whatever the pile says. */
.card.palace-dead { filter: grayscale(0.65) brightness(0.85); }

/* ---------- the final table ---------- */
.palace-final {
    margin: 8px auto;
    display: grid;
    gap: 3px;
    max-width: 22em;
}
.palace-final-row {
    display: grid;
    grid-template-columns: 4.5em minmax(0, 1fr) auto;
    gap: 8px;
    align-items: baseline;
    padding: 3px 6px;
    border-radius: 5px;
}
.palace-final-winner {
    background: color-mix(in srgb, var(--accent) 14%, transparent);
    font-weight: 700;
}
.palace-final-loser { opacity: 0.75; }
.palace-final-place {
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.7;
}
.palace-final-name { font-weight: 600; }
.palace-final-cards {
    font-size: 12.5px;
    opacity: 0.75;
    font-variant-numeric: tabular-nums;
}

/* ---------- the log ---------- */
.logline.palace-logburn { color: var(--accent); font-weight: 600; }
.logline.palace-logpickup { opacity: 0.78; }
.logline.palace-logout { font-weight: 600; }

/* ---------- rules blocks ----------
   The Cheat_Sheet's lists carry their own classes so the same spacing applies
   in the Rules summary and on the palace/rules/ page. `.palace-rules` is now
   only the Strategy tips block's class. */
.palace-steps > li,
.palace-abilities { list-style: none; padding-left: 0; }
.palace-abilities > li { margin-bottom: 6px; }
.palace-rules ul { margin: 4px 0 0 0; }

/* ---------- narrow screens ----------
   The pile keeps its size and the rows shrink: the pile is the thing being read
   every turn, and a 3 you cannot tell from an 8 makes the game unplayable. */
@media (max-width: 720px) {
    .panel.palace-seat { flex: 1 1 140px; }
    .panel.palace-seat .palace-row .card { --cw: 26px; }
    .palace-myrow .card { --cw: 44px; }
    .card.palace-piled { --cw: 44px; }
    .card.palace-revealed { --cw: 56px; }
    .palace-handpanel #hand-cards .card { --cw: 50px; }
}

/* =========================================================================
   Indian Chief (indianchief)

   A scoresheet game: seven rounds, seven melds, each played once. The screen
   reads top to bottom in the order a round happens:

     this round's cells, one a seat: a generic back once a seat has locked in
     (never a stack: the card count IS the meld), the melds face up after the
     reveal; last round's melds stay underneath while the next is chosen
                                                              (.ic-board)
     the meld under construction: the seven meld chips, what the selection
     names and would score, and the lock-in                  (.ic-actions)
     the hand, multi-select                                   (#hand-panel)
     the printed scoresheet                                   (.ic-sheet)
   ========================================================================= */

/* --- the scoreboard ------------------------------------------------------ */
.ic-scorename { font-size: 12px; opacity: 0.8; margin-right: 5px; }
.ic-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }

/* --- the status line ----------------------------------------------------- */
.status.ic-status { min-height: 2.9em; }
/* Lit whenever this device may lock in: a sentence alone is not enough of a
   signal about whose turn it is. */
.status.ic-status.ic-turn { color: var(--accent); font-weight: 700; }

/* --- the board ----------------------------------------------------------- */
.ic-board { margin-bottom: 12px; }
.ic-board-title {
    font-size: 12px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.6;
    margin: 10px 0 6px;
}
.ic-cells { display: flex; flex-wrap: wrap; gap: 10px; align-items: stretch; }
.ic-cell {
    flex: 0 1 auto;
    min-width: 150px;
    padding: 8px 10px 9px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.ic-cell.ic-mine { border-color: rgba(255, 255, 255, 0.35); }
/* Freshly revealed melds carry the accent ring for the round-end beat. */
.ic-cell.ic-revealed { outline-color: rgba(255, 220, 120, 0.35); }
.ic-cell-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 6px;
    flex-wrap: wrap;
}
.ic-cell-name { font-weight: 700; font-size: 14px; }
.ic-cell-total { font-variant-numeric: tabular-nums; opacity: 0.75; font-size: 13px; }
.ic-cell-meld { font-size: 13px; opacity: 0.85; }
.ic-cell-points {
    margin-left: auto;
    font-size: 20px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    color: var(--accent);
}
.ic-cell-points.neg { color: #f08c8c; }
.ic-cell-body { display: flex; align-items: center; gap: 10px; min-height: 40px; }
.ic-cell-cards { min-height: 40px; }
.ic-cell-cards .card { --cw: 40px; }
/* The council's three seats in a revealed Indian Chief: lifted a touch, so
   the split the engine chose can be read off the cards. */
.ic-cell-cards .card.ic-council { transform: translateY(-4px); box-shadow: 0 0 0 2px var(--accent); }
/* A Joker, with its stand-in in the caption below. */
.ic-cell-cards .card.ic-joker { outline: 2px dashed rgba(255, 255, 255, 0.45); outline-offset: -2px; }
.ic-cell-note { font-size: 12px; opacity: 0.75; margin-top: 6px; line-height: 1.35; }
.ic-back { --cw: 40px; }
.ic-chip {
    padding: 2px 8px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.07);
    font-size: 12px;
    white-space: nowrap;
}
.ic-lockedchip { color: var(--accent); font-weight: 700; }
.ic-choosing { opacity: 0.7; font-style: italic; }
.ic-last .ic-cell { opacity: 0.8; }
.ic-last .ic-cell-cards .card { --cw: 32px; }

/* --- the meld under construction ----------------------------------------- */
.ic-actions { margin: 4px 0 12px; }
.ic-meldchips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
/* A chip is a real <button> (pressing it selects the meld's best cards), so
   the resets below keep it looking like its inert `used` sibling. */
.ic-meldchip {
    display: inline-flex;
    align-items: baseline;
    gap: 7px;
    padding: 3px 10px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
    color: var(--ink);
    font: inherit;
    font-size: 12px;
    white-space: nowrap;
    cursor: pointer;
}
button.ic-meldchip:hover { background: rgba(255, 255, 255, 0.14); }
.ic-meldchip.used { opacity: 0.4; cursor: default; }
.ic-meldchip.used .ic-meldchip-name { text-decoration: line-through; }
/* What this hand would score for the meld, best cards chosen. */
.ic-meldchip-pts { font-weight: 800; font-variant-numeric: tabular-nums; }
/* The meld the current selection names. */
.ic-meldchip.named { border-color: var(--accent); color: var(--accent); font-weight: 700; }
.ic-preview { font-size: 14px; min-height: 1.4em; }
.ic-hint { font-size: 13px; color: var(--accent); margin-top: 4px; }
/* With a Joker in hand the hint is two readings, each a button that selects
   its cards; the one currently selected is filled. */
.ic-hintopt {
    display: block;
    margin: 4px 0 0;
    padding: 4px 10px;
    border-radius: 8px;
    border: 1px solid var(--accent);
    background: transparent;
    color: var(--accent);
    font: inherit;
    font-size: 13px;
    text-align: left;
    cursor: pointer;
}
.ic-hintopt:hover { background: rgba(255, 220, 120, 0.12); }
.ic-hintopt.chosen { background: var(--accent); color: var(--accent-ink); font-weight: 700; }
.ic-lockrow { margin: 10px 0 0; }
.ic-next { margin-top: 4px; }
/* The hand: eight cards toggled into the meld, marked by the shared .card.sel. */

/* --- the scoresheet ------------------------------------------------------ */
.ic-sheet { margin-bottom: 14px; overflow-x: auto; }
.ic-scoresheet {
    border-collapse: collapse;
    font-size: 13px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    overflow: hidden;
}
.ic-scoresheet th,
.ic-scoresheet td {
    padding: 5px 12px;
    border-bottom: 1px solid var(--panel-border);
    text-align: left;
    vertical-align: middle;
}
.ic-scoresheet thead th { font-size: 12px; opacity: 0.75; font-weight: 700; }
.ic-scoresheet th.ic-num,
.ic-scoresheet td.ic-num { text-align: right; font-variant-numeric: tabular-nums; min-width: 56px; }
.ic-scoresheet .ic-mine { color: var(--accent); }
.ic-scoresheet td.ic-empty { opacity: 0.35; }
/* Scored this round: the beat the scoresheet is read for. */
.ic-scoresheet td.ic-new { font-weight: 800; background: rgba(255, 220, 120, 0.12); }
.ic-scoresheet tr.ic-total td { font-weight: 800; font-size: 15px; border-bottom: 0; }
.ic-sheet-meld { white-space: nowrap; }
/* The card count, which is the meld's name in this game: bold, fixed width,
   so the seven rows line up "1 Thief" down to "7 Indian Chief". */
.ic-sheet-size {
    display: inline-block;
    min-width: 1.1em;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    text-align: right;
}

/* --- the result, and the solo replays round by round --------------------- */
.ic-bots-title { margin-top: 18px; }
.ic-bots { margin-top: 10px; }
.ic-bots td.ic-botcell { vertical-align: top; min-width: 150px; }
.ic-bots th.ic-botcol { text-align: left; }
.ic-botplay { display: flex; gap: 8px; align-items: baseline; margin-bottom: 4px; }
.ic-botmeld { font-size: 12px; opacity: 0.85; }
.ic-botpts { font-weight: 800; font-variant-numeric: tabular-nums; color: var(--accent); }
.ic-botpts.neg { color: #f08c8c; }
.ic-bots .ic-cell-cards { min-height: 0; gap: 2px; }
.ic-bots .ic-cell-cards .card { --cw: 24px; }

/* --- the log ------------------------------------------------------------- */
.ic-logreveal { padding: 2px 0; }
.ic-logreveal .logline { border-bottom: 0; padding: 1px 0 1px 10px; }

/* ---------- narrow screens ---------- */
@media (max-width: 720px) {
    .ic-cell { min-width: 120px; flex: 1 1 140px; }
    .ic-cell-cards .card { --cw: 32px; }
    .ic-scoresheet th,
    .ic-scoresheet td { padding: 4px 8px; }
}

/* =========================================================================
   The Crew (crew, crew-)
   Layout intent: two columns. The left holds the crew (one chip per seat:
   commander, tasks owned, the shown card, the radio), the trick, and the
   table panel that changes shape with the phase (the task pool, JARVIS's
   double row, the drones' face-up hands). The right holds the two panels a
   player wants to glance at every trick: CARDS SEEN, laid out like the pack
   (four rows of Ace to 9, the rockets underneath), and the mission's tasks in
   their order. A settled trick stays on display until the next lead.
   Marks, all light so they read at a glance without shouting: a TASK card
   glows, the card TAKING the trick wears a ring, a SHOWN card carries an eye
   with its token word, a ROCKET a small rocket, and while the viewer chooses,
   everything they cannot play fades.
   ========================================================================= */
.status.crew-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.crew-status { min-height: 4.4em; }
}
.crew-progress { flex-wrap: wrap; }
.crew-progress-chip {
    font-size: 13px;
    padding: 3px 10px;
    border-radius: 999px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    opacity: 0.9;
}
.crew-progress-tokens { border-color: #3f9a5f; }
/* --- the board ------------------------------------------------------------ */
/* The board is the crew, the trick and whatever the phase puts on the table -
   one column, full width, so the seat boxes and the trick slots have room to be
   as many equal columns as there are seats (see .crew-seats-row).
   CARDS SEEN and THE MISSION are not part of it: they are reference, and they
   live below the hand in .crew-below. */
.crew-board { margin-bottom: 12px; }
.crew-below {
    display: flex;
    gap: 12px;
    align-items: flex-start;
    flex-wrap: wrap;
}
.crew-below .crew-seenpanel { flex: 1 1 360px; }
.crew-below .crew-missionpanel { flex: 1 1 260px; }
.crew-panelhead {
    font-size: 12px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
    margin-bottom: 8px;
}
.crew-tablenote,
.crew-tricknote { margin-top: 8px; font-size: 13px; opacity: 0.8; }
.crew-tricknote { min-height: 2.6em; }
/* --- the marks on cards -------------------------------------------------- */
/* The badges are positioned children, so every card face is a positioning box.
   NOT the flying ghosts: `.card-fly` is a single class and sets position: fixed,
   which a two-class rule here would out-specify - the ghost would then sit in
   normal flow at the bottom of the page with left/top as offsets. */
.card.crew-card:not(.card-fly) { position: relative; overflow: visible; }
/* Somebody's task card: a soft glow, the tooltip names whose. Light on
   purpose - up to ten cards may glow at once. */
.card.crew-task {
    box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.55), 0 0 12px 3px rgba(255, 209, 102, 0.45), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.card.crew-task.crew-task-ok { box-shadow: 0 0 0 2px rgba(120, 220, 150, 0.6), 0 0 10px 2px rgba(120, 220, 150, 0.4); }
.card.crew-task.crew-task-failed { box-shadow: 0 0 0 2px rgba(255, 120, 120, 0.7), 0 0 10px 2px rgba(255, 120, 120, 0.4); }
/* The card taking the trick. */
.card.crew-winning { outline: 3px solid var(--accent); outline-offset: 2px; }
/* Cards the viewer cannot play right now. */
.card.crew-faded { filter: brightness(0.5) saturate(0.6); }
/* The rocket badge: bottom-right, sized off the card width. */
.crew-rocket-badge {
    position: absolute;
    right: -4%;
    bottom: -4%;
    font-size: calc(var(--cw) * 0.34);
    line-height: 1;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.7));
    pointer-events: none;
}
/* The eye on a shown card: the SITE'S shared `.eye-badge` (webplay.css, built
   by eyeBadge in shared/ui/panels.ts) - same icon, pill and color as every
   other game's public-knowledge mark, instead of the emoji this game used to
   draw. What is this game's own is the ANCHOR: the badge moves to the LEFT
   edge at the height of the printed token, because that height is the
   information (top = highest of the suit, middle = only, bottom = lowest).

   The shared rule anchors bottom-right, so both of its offsets are cleared
   here rather than relied on to lose a specificity race. */
.crew-eye {
    left: -6%;
    right: auto;
    top: auto;
    bottom: auto;
    border: 1px solid rgba(255, 255, 255, 0.5);
}
.crew-eye-top { top: -8%; }
.crew-eye-only { top: 40%; }
.crew-eye-bottom { bottom: -8%; top: auto; }
/* --- the seats ----------------------------------------------------------- */
.crew-seats {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
/* ONE COLUMN PER SEAT, in play order from the leader, over the trick slots that
   use the same template (--crew-cols is set by the renderer). Tracking whose
   turn it is was the thing this row failed at while it was a plain flex list in
   seat order: the trick area was already ordered from the leader, so the two
   disagreed and neither read as the turn order. The padding matches the trick
   panel's 12px plus its 1px border, which is what actually lines the columns up. */
.crew-seats.crew-seats-row {
    display: grid;
    grid-template-columns: repeat(var(--crew-cols, 4), minmax(0, 1fr));
    align-items: start;
    gap: 8px;
    padding: 0 13px;
}
.crew-seats-row .crew-seat { max-width: none; min-width: 0; }
@media (max-width: 720px) {
    /* Five columns of 60px is not a layout. Below this the row wraps again and
       the alignment is given up rather than faked. */
    .crew-seats.crew-seats-row { display: flex; flex-wrap: wrap; padding: 0; }
    .crew-seats-row .crew-seat { max-width: 230px; min-width: 110px; }
}
.crew-seat {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 132px;
    /* Grows to share a row, but capped: a chip that wraps alone would otherwise
       stretch the full width of the column and read as a panel of its own. */
    flex: 1 1 140px;
    max-width: 230px;
    padding: 6px 10px 8px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.crew-seat.crew-seat-mine { border-color: rgba(255, 255, 255, 0.35); }
.crew-seat.crew-seat-turn { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.crew-seathead { display: flex; gap: 6px; align-items: baseline; flex-wrap: wrap; }
.crew-seatname { font-size: 13px; font-weight: 700; }
.crew-commander {
    font-size: 10px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-ink);
    font-weight: 700;
}
.crew-seatkind { font-size: 11px; opacity: 0.6; }
.crew-seatfacts { display: flex; gap: 8px; flex-wrap: wrap; font-size: 12px; opacity: 0.75; }
.crew-radio { font-size: 11px; padding: 0 6px; border-radius: 999px; border: 1px solid transparent; }
.crew-radio-fresh { border-color: #3f9a5f; color: #9fe0b4; }
.crew-radio-used { border-color: #a04040; color: #ff9c9c; opacity: 0.8; }
.crew-seattasks { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 2px; align-items: center; }
/* "Goal", in front of the task cards of a seat whose hand is also on screen. */
.crew-seatgoal {
    font-size: 10px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
}
.crew-seatshown { display: flex; gap: 8px; align-items: center; margin-top: 2px; }
.crew-shownlabel { font-size: 12px; opacity: 0.8; font-style: italic; }
.card.crew-showncard { --cw: 34px; }
/* --- task cards (the small simplified faces) and their tokens ------------ */
.crew-taskwrap { position: relative; display: inline-flex; flex-direction: column; align-items: center; gap: 2px; }
.card.crew-taskcard { --cw: 34px; }
.crew-token {
    position: absolute;
    top: -7px;
    right: -7px;
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    border-radius: 4px;
    background: #2b2246;
    color: #e8dcff;
    border: 1px solid rgba(255, 255, 255, 0.45);
    font-size: 11px;
    font-weight: 800;
    line-height: 15px;
    text-align: center;
    letter-spacing: -0.05em;
}
.crew-taskmark {
    position: absolute;
    left: -6px;
    bottom: 14px;
    font-size: 14px;
    font-weight: 800;
    color: #9fe0b4;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}
.crew-taskmark-failed { color: #ff9c9c; }
.crew-taskwrap-ok .card.crew-taskcard { filter: brightness(0.7); }
.crew-taskwrap-failed .card.crew-taskcard { filter: brightness(0.45) saturate(0.5); }
.crew-taskowner { font-size: 11px; opacity: 0.8; white-space: nowrap; }
/* --- the trick ----------------------------------------------------------- */
.crew-trickpanel { padding: 10px 12px; margin-bottom: 10px; }
.crew-trickrow {
    display: grid;
    grid-template-columns: repeat(var(--crew-cols, 4), minmax(0, 1fr));
    align-items: start;
    gap: 8px;
}
.crew-trickcell { display: flex; flex-direction: column; align-items: center; gap: 3px; padding-bottom: 14px; }
.crew-trickcell.crew-mine { padding-top: 14px; padding-bottom: 0; }
.crew-trickcap { font-size: 11px; opacity: 0.65; white-space: nowrap; }
.crew-trickcell.crew-mine .crew-trickcap { opacity: 0.9; }
.crew-trickcell.crew-winningcell .crew-trickcap { color: var(--accent); font-weight: 700; opacity: 1; }
.crew-trickcell.crew-next .card.empty { border-color: var(--accent); }
/* --- the table: pool, JARVIS, drones ------------------------------------- */
.crew-tablepanel { padding: 10px 12px; margin-bottom: 10px; }
.crew-taskpool { display: flex; gap: 12px; flex-wrap: wrap; padding: 6px 4px 2px; }
.crew-taskpool .card.crew-taskcard { --cw: 44px; }
.crew-jarvisrow { display: flex; gap: 8px; flex-wrap: wrap; }
.crew-jarviscol { position: relative; width: 52px; height: calc(52px * 89 / 64 + 16px); }
.crew-jarviscol .card { --cw: 52px; position: absolute; left: 0; }
.crew-jarviscol .crew-jarvisback { top: 16px; opacity: 0.85; }
.crew-jarviscol .crew-jarvisup { top: 0; }
.crew-jarviscol .card.empty { top: 0; }
/* --- Solo Running: the drones around the table --------------------------- */
/* Solo has no hidden cards, so the seats are laid out POSITIONALLY the way
   Hearts lays out its opponents: the drone one seat clockwise on the left, the
   far one up, the last one right, and the player at the bottom over their own
   hand. Each drone's cards and its goal sit inside its own box, so "what will
   Huey do with this lead" is read in one place - and a reseat (a radio token
   during setup) visibly moves a drone around the table.
   Bottoms aligned with the far seat lifted clear, as in Hearts: a margin rather
   than a transform, so the space is still reserved and nothing overlaps. */
.crew-seats.crew-seats-table {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-areas:
        'left up right'
        '.    down .';
    /* TOPS aligned, with the outer two dropped to lift the far seat - the arc
       Hearts gets from aligning bottoms, but growing DOWNWARD. These boxes
       change height mid-trick (the active drone gains its token buttons, a hand
       wraps to one row fewer as it empties), and with bottoms aligned every such
       change shoved the whole row up and down the screen. */
    align-items: start;
    gap: 10px;
}
/* The row cap that keeps a wrapped chip from stretching (see .crew-seat) has no
   job in the grid, where the columns already decide the width. */
.crew-seats-table .crew-seat { max-width: none; min-width: 0; }
.crew-seats-table .crew-seat.pos-left { grid-area: left; margin-top: 22px; }
.crew-seats-table .crew-seat.pos-up { grid-area: up; }
.crew-seats-table .crew-seat.pos-right { grid-area: right; margin-top: 22px; }
.crew-seats-table .crew-seat.pos-down { grid-area: down; }
@media (max-width: 720px) {
    /* One column, in seating order, rather than three unreadably narrow ones. */
    .crew-seats.crew-seats-table {
        grid-template-columns: 1fr;
        grid-template-areas: 'left' 'up' 'right' 'down';
    }
    .crew-seats-table .crew-seat.pos-left,
    .crew-seats-table .crew-seat.pos-right { margin-top: 0; }
}
/* A drone's hand: ten cards inside a third of the table's width, so they are
   small and on the simplified atlas (rank over suit), which is all that is
   being read at this size. */
.crew-dronehand { gap: 3px; flex-wrap: wrap; margin-top: 4px; }
.crew-dronehand .card { --cw: 32px; }
/* The card the drone is about to play (or the tied candidates), in the drone's
   own color rather than the accent - the accent means "your move" everywhere
   else on the screen, and this is a statement about somebody else. */
.card.crew-intent { outline: 2px solid #9fe0b4; outline-offset: 2px; }
/* A red token on a drone's card. */
.crew-redmark {
    position: absolute;
    top: -6%;
    right: -6%;
    font-size: calc(var(--cw) * 0.42);
    line-height: 1;
    color: #e04848;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
    pointer-events: none;
}
.card.crew-red { filter: brightness(0.6) saturate(0.7); }
.crew-droneactions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
/* Inside a seat box these are narrow, so they wrap to their own lines rather
   than squeezing their labels. */
.crew-seats-table .crew-droneactions .btn { font-size: 12px; padding: 5px 9px; }
/* --- the mission panel ----------------------------------------------------- */
.crew-missionpanel { padding: 10px 12px; }
.crew-missiontasks { display: flex; gap: 12px; flex-wrap: wrap; padding: 6px 4px 2px; }
/* --- the seen cards -------------------------------------------------------- */
.crew-seenpanel { padding: 10px 12px; }
.crew-seengrid { display: flex; flex-direction: column; gap: 3px; }
.crew-seenrow { display: flex; gap: 3px; }
.crew-seenrow .card { --cw: 30px; border-radius: 4px; }
.crew-seenrow .card.crew-task { box-shadow: 0 0 0 1.5px rgba(255, 209, 102, 0.7), 0 0 6px 1px rgba(255, 209, 102, 0.5); }
.crew-seenrockets { margin-top: 4px; }
.card.crew-seen-unseen { opacity: 0.16; box-shadow: none; }
.card.crew-seen-mine { opacity: 0.9; outline: 2px solid rgba(255, 255, 255, 0.75); outline-offset: 0; }
.card.crew-seen-shown { opacity: 0.95; }
.card.crew-seen-played { opacity: 1; }
/* The compact seen row: the same badge, scaled down (it is an SVG in a fixed
   box, so size it rather than setting a font size). */
.crew-seenrow .crew-eye { left: -20%; width: 13px; height: 13px; }
.crew-seenrow .crew-eye svg { width: 9px; height: 9px; }
.crew-seenrow .crew-rocket-badge { font-size: 12px; }
/* --- the hand and the actions ---------------------------------------------- */
#hand-cards .card.crew-card { margin-bottom: 8px; }
.crew-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 4px 0 10px; }
.crew-actions:empty { display: none; }
.crew-actionnote { font-size: 13px; opacity: 0.85; }
/* --- the result: banner, tabs, the review ---------------------------------- */
.crew-result .winner-banner.crew-won { color: #9fe0b4; }
.crew-result .winner-banner.crew-lost { color: #ff9c9c; }
.crew-tabs { display: flex; gap: 6px; margin: 12px 0 10px; }
.crew-tab {
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    padding: 6px 14px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
    color: var(--ink);
    cursor: pointer;
}
.crew-tab.selected { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
.crew-replay-note { margin-top: 0; margin-bottom: 8px; }
.crew-review { display: flex; flex-direction: column; gap: 10px; }
.crew-review-setup { display: flex; flex-direction: column; gap: 2px; font-size: 13px; margin-bottom: 4px; }
.crew-review-seatline { display: flex; gap: 10px; flex-wrap: wrap; }
.crew-review-seatname { font-weight: 700; min-width: 110px; }
.crew-review-seatcomm { opacity: 0.75; font-style: italic; }
.crew-review-trick { padding: 8px 10px; border-radius: 8px; background: var(--panel); border: 1px solid var(--panel-border); }
.crew-review-head { display: flex; gap: 10px; flex-wrap: wrap; align-items: baseline; font-size: 13px; margin-bottom: 6px; }
.crew-review-num { font-weight: 700; }
.crew-review-winner { opacity: 0.85; }
.crew-review-task { font-size: 12px; padding: 1px 8px; border-radius: 999px; border: 1px solid transparent; }
.crew-review-task.ok { border-color: #3f9a5f; color: #9fe0b4; }
.crew-review-task.failed { border-color: #a04040; color: #ff9c9c; }
.crew-review-row { display: flex; gap: 8px; flex-wrap: wrap; }
.crew-review-cell { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.crew-review-cell .card { --cw: 40px; }
.crew-review-cap { font-size: 11px; opacity: 0.7; white-space: nowrap; }
/* --- the rules ------------------------------------------------------------- */
.crew-suit { font-weight: 700; }
.crew-suit.red { color: var(--red); }
.crew-suit.red.diamond { color: var(--red-diamond); }
.crew-suit.club { color: var(--blue-club); }
.crew-reference p { font-size: 13.5px; }
@media (max-width: 720px) {
    .crew-board .crew-side { flex: 1 1 100%; }
    .crew-trickcell .card { --cw: 50px; }
    .crew-seenrow .card { --cw: 26px; }
    .crew-seat { min-width: 110px; }
}

/* =========================================================================
   SECRET AGENT (sag-)

   Layout intent, top to bottom, in the order a turn is read:

     1. #sag-board: the seat chips, then the round being played.
     2. #sag-desk: the viewer's hand beside the deduction pad, directly under
        the Agents they are played against. Side by side when there is room,
        stacked on a phone, hand first.
     3. #sag-clues: every settled round of the mission with ALL of its Agents
        and the one that made contact ringed, plus every name tried.

   The Secret Agent itself has no panel. The Director sees it as a small chip
   in the round's heading and as a marked square on the pad; everybody sees it
   the same way once the mission is over.

   The pad is automatic. Each square is one of four states, and they are told
   apart by weight rather than by color alone, so they survive a color-blind
   reading:
     STRUCK (.sag-pad-seen)   visible somewhere, so out: faint and struck through;
     NAMED  (.sag-pad-named)  somebody named it and was wrong: faint, struck,
                              with a red strike;
     DIM    (.sag-pad-ruled)  the clues rule it out: faint, not struck;
     LIT    (.sag-pad-open)   still possible: full weight, accent border.
   The square a guess would name is OUTLINED, the Director's own Secret Agent is
   FILLED, and the answer lights up green when the mission is over.

   Nothing on this board ever says WHY a card made contact: that is the secret.
   ========================================================================= */
.status.sag-status { min-height: 3.6em; }
@media (max-width: 620px) {
    .status.sag-status { min-height: 5.6em; }
}
/* --- the scoreboard ------------------------------------------------------- */
.sag-scoreboard { flex-wrap: wrap; }
.sag-scorename { opacity: 0.8; margin-right: 6px; }
.sag-scorenum { font-weight: 700; }
.sag-progress-round {
    font-size: 13px;
    padding: 3px 10px;
    border-radius: 999px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    opacity: 0.9;
}
.sag-panelhead {
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    opacity: 0.65;
    white-space: nowrap;
}
/* --- the board: seats and the round -------------------------------------- */
.sag-board { margin-bottom: 12px; }
.sag-seats {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: center;
    margin-bottom: 10px;
}
.sag-seat {
    min-width: 130px;
    padding: 6px 12px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.sag-seat.sag-seat-mine { border-color: rgba(255, 255, 255, 0.4); }
/* The seat that has to act. The accent border is load-bearing: with up to four
   chairs a status sentence alone is not enough to find whose turn it is. */
.sag-seat.sag-seat-turn {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.07);
}
.sag-seat.sag-seat-out { opacity: 0.55; }
.sag-seathead {
    display: flex;
    gap: 8px;
    align-items: baseline;
    justify-content: center;
    white-space: nowrap;
}
.sag-seatname { font-size: 13.5px; font-weight: 700; }
.sag-seatrole { font-size: 11px; opacity: 0.6; }
.sag-seatrole.sag-director {
    opacity: 1;
    color: var(--accent);
    font-weight: 700;
}
.sag-seattokens {
    display: flex;
    gap: 5px;
    flex-wrap: wrap;
    justify-content: center;
    margin-top: 4px;
}
.sag-token {
    font-size: 10.5px;
    padding: 1px 7px;
    border-radius: 999px;
    border: 1px solid transparent;
    white-space: nowrap;
}
.sag-token-sniper { border-color: #a04040; color: #ff9c9c; }
.sag-token-double { border-color: #3f9a5f; color: #9fe0b4; }
.sag-token-out { border-color: var(--panel-border); opacity: 0.75; }
/* --- the round ----------------------------------------------------------- */
.sag-roundpanel { padding: 10px 12px; margin-bottom: 0; }
/* Reserved at one line whether or not the Director's chip is in it, so the
   panel does not grow a step when the Secret Agent is chosen. */
.sag-roundhead {
    display: flex;
    gap: 10px;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    min-height: 26px;
    margin-bottom: 8px;
}
.sag-secretchip {
    font-size: 12.5px;
    padding: 2px 10px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    white-space: nowrap;
}
.sag-secretchip.sag-secretchip-mine {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.1);
}
.sag-playrow {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: center;
}
.sag-playcell { display: flex; flex-direction: column; align-items: center; gap: 7px; }
/* The viewer's own slot sits a step lower, as in the sibling games, AND every
   slot is captioned: with up to four chairs plus the deck, stagger alone cannot
   say whose Agent is whose. */
.sag-playcell.sag-mine { padding-top: 14px; }
.sag-playcell.sag-pending { opacity: 0.45; }
.sag-playcap { font-size: 11px; opacity: 0.7; white-space: nowrap; }
.sag-playcell .card { --cw: 62px; }
.card.sag-contact { outline: 3px solid var(--accent); outline-offset: 2px; }
/* Deleted by the Sniper: still on the table, but out of the reckoning. */
.card.sag-deleted { filter: grayscale(0.85) brightness(0.55); }
.sag-roundnote {
    font-size: 13px;
    opacity: 0.85;
    margin-top: 8px;
    min-height: 1.3em;
    text-align: center;
}
.sag-actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    margin-top: 8px;
}
.sag-actionnote { font-size: 13px; opacity: 0.85; }
/* --- the desk: the hand beside the pad ----------------------------------- */
.sag-desk {
    display: flex;
    gap: 12px;
    align-items: flex-start;
    flex-wrap: wrap;
    margin-bottom: 12px;
}
.sag-desk > .sag-handpanel { flex: 1 1 420px; min-width: 0; margin-bottom: 0; }
.sag-desk > .sag-padpanel { flex: 1 1 340px; max-width: 480px; margin: 0 auto; }
/* --- the deduction pad -------------------------------------------------- */
.sag-padpanel { padding: 10px 12px; }
.sag-padtop {
    display: flex;
    gap: 10px;
    align-items: baseline;
    justify-content: center;
    margin-bottom: 8px;
}
.sag-padcount { font-size: 12.5px; font-weight: 600; white-space: nowrap; }
.sag-padgrid { display: flex; flex-direction: column; gap: 3px; }
.sag-padrow { display: flex; gap: 3px; align-items: center; }
.sag-padcolor {
    flex: 0 0 74px;
    font-size: 11.5px;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.sag-padcolor.red { color: var(--red); }
.sag-padcolor.red.diamond { color: var(--red-diamond); }
.sag-padcolor.club { color: var(--blue-club); }
.sag-padgrid .sag-padrank {
    flex: 1 1 0;
    text-align: center;
    font-size: 10.5px;
    opacity: 0.55;
}
.sag-padcell {
    flex: 1 1 0;
    min-width: 0;
    font: inherit;
    font-size: 12px;
    font-weight: 600;
    line-height: 1.6;
    text-align: center;
    padding: 2px 0;
    border-radius: 5px;
    border: 1px solid var(--panel-border);
    background: transparent;
    color: var(--ink);
}
/* The four states, and the key that shows them in the note underneath. */
.sag-pad-seen { opacity: 0.22; text-decoration: line-through; }
.sag-pad-named {
    opacity: 0.35;
    text-decoration: line-through;
    text-decoration-color: #ff6b6b;
}
.sag-pad-ruled { opacity: 0.3; }
.sag-pad-open {
    background: rgba(255, 209, 102, 0.1);
    border-color: rgba(255, 209, 102, 0.6);
}
button.sag-padcell { cursor: pointer; }
button.sag-padcell:hover { border-color: var(--accent); }
/* The square the guess button names. */
.sag-padcell.sag-pad-pick {
    outline: 2px solid rgba(255, 255, 255, 0.85);
    outline-offset: 1px;
}
/* The Director's own Secret Agent, on the Director's pad only. */
.sag-padcell.sag-pad-secret {
    background: rgba(255, 209, 102, 0.3);
    border-color: var(--accent);
    color: var(--accent);
}
/* The answer, once the mission is over and it can be shown. */
.sag-padcell.sag-pad-answer {
    background: rgba(63, 154, 95, 0.35);
    border-color: #3f9a5f;
    color: #d8f5e2;
    opacity: 1;
    text-decoration: none;
}
.sag-padnote {
    font-size: 12px;
    opacity: 0.8;
    margin-top: 8px;
    text-align: center;
}
/* A key square is a real pad square in miniature, so it reads exactly like the
   squares it explains. */
.sag-padcell.sag-padkey {
    display: inline-block;
    flex: none;
    width: 20px;
    font-size: 11px;
    line-height: 1.4;
    padding: 0;
    margin: 0 4px 0 10px;
}
.sag-padcell.sag-padkey:first-child { margin-left: 0; }
/* --- naming the Secret Agent -------------------------------------------- */
.sag-guessbox {
    margin-top: 10px;
    padding-top: 9px;
    border-top: 1px solid var(--panel-border);
    display: flex;
    flex-direction: column;
    gap: 6px;
    align-items: center;
    text-align: center;
}
.sag-guesshead { font-size: 13px; font-weight: 600; }
.sag-guessnote { font-size: 12px; opacity: 0.72; }
/* --- the hand ------------------------------------------------------------ */
.sag-handpanel #hand-cards .card.clickable { cursor: pointer; }
/* --- the clues this mission --------------------------------------------- */
.sag-cluespanel { padding: 10px 12px; }
.sag-cluespanel > .sag-panelhead { text-align: center; margin-bottom: 8px; }
.sag-cluesnote { font-size: 13px; opacity: 0.7; text-align: center; }
.sag-clue {
    display: flex;
    gap: 12px;
    align-items: center;
    flex-wrap: wrap;
    padding: 6px 0;
    border-top: 1px solid var(--panel-border);
}
.sag-cluespanel > .sag-panelhead + .sag-clue { border-top: none; }
.sag-cluelabel {
    flex: 0 0 76px;
    font-size: 12px;
    font-weight: 700;
    opacity: 0.7;
    white-space: nowrap;
}
.sag-cluecards { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; }
/* The gap clears the contact ring (3px outline, 2px offset) off the caption. */
.sag-cluecell { display: flex; flex-direction: column; align-items: center; gap: 7px; }
.sag-cluecell .card { --cw: 40px; border-radius: 4px; }
.sag-cluecap { font-size: 10.5px; opacity: 0.65; white-space: nowrap; }
.sag-clue-won .sag-cluecap { opacity: 1; font-weight: 700; color: var(--accent); }
.sag-clueguess { font-size: 13px; }
.sag-cluenote { font-size: 12px; opacity: 0.7; }
/* --- the result --------------------------------------------------------- */
.sag-summary .sag-result .result-row {
    display: grid;
    grid-template-columns: 120px repeat(var(--sag-cols, 3), minmax(0, 1fr));
}
.sag-summary .result-cell.sag-won { color: #9fe0b4; font-weight: 700; }
/* --- the rules blocks --------------------------------------------------- */
.sag-suit { font-weight: 700; }
.sag-suit.red { color: var(--red); }
.sag-suit.red.diamond { color: var(--red-diamond); }
.sag-suit.club { color: var(--blue-club); }
/* --- narrow screens ----------------------------------------------------- */
@media (max-width: 720px) {
    .sag-desk > .sag-handpanel,
    .sag-desk > .sag-padpanel { flex: 1 1 100%; max-width: none; }
    .sag-playcell .card { --cw: 50px; }
    .sag-cluecell .card { --cw: 34px; }
    .sag-cluelabel { flex-basis: 100%; }
    .sag-padcolor { flex: 0 0 16px; font-size: 12px; }
    .sag-padword { display: none; }
    .sag-padcell { font-size: 11px; }
    .sag-seat { min-width: 110px; }
}
/* =========================================================================
   Linko (linko)

   One comparison, made visible. Everything on this screen exists to answer
   "same number of cards, higher number?" before the cards leave the hand, so
   the layout reads top to bottom in the order a turn happens:

     one panel a seat, clockwise from the seat after the viewer, so the
     viewer's own stack lands next to their hand. The TOP group is real cards
     because it is the only one still at risk; buried groups are
     count-and-number chips, since suits do nothing in Linko    (.lk-table)
     the Duel ghost's face-up hand, in groups by number         (.lk-ghost)
     the pile, the discards and the six row positions, all of which become
     buttons while somebody owes draws                          (.lk-row)
     the decision of the moment: the play under construction, the snatch
     dilemma, the robbed player's answer, the draws owed        (.lk-actions)
     the hand, multi-select                                     (#hand-panel)

   Two classes carry the teaching. `.lk-target` marks every stack (and ghost
   group) the current selection would snatch - the rule, shown rather than
   explained - and `.lk-pickable` marks anything the local player may press
   right now, which in the draw phase is scattered across the row and the pile.
   ========================================================================= */
/* --- the scoreboard ------------------------------------------------------ */
.lk-scorename { font-size: 12px; opacity: 0.8; margin-right: 5px; }
.lk-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }
/* --- the status line ----------------------------------------------------- */
/* Reserved height: the dilemma lines are long and the board must not jump
   between a one-line and a three-line status. */
.status.lk-status { min-height: 2.9em; }
/* Lit whenever this device may act. Linko asks four different seats for four
   different things, so a sentence alone is not enough of a signal. */
.status.lk-status.lk-turn { color: var(--accent); font-weight: 700; }
/* --- the table ----------------------------------------------------------- */
.lk-table {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: stretch;
    margin-bottom: 12px;
}
.lk-seat {
    flex: 1 1 190px;
    max-width: 320px;
    padding: 8px 10px 9px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
    outline-offset: 2px;
}
.lk-seat.lk-mine { border-color: rgba(255, 255, 255, 0.35); }
/* Whose turn it is, and (a stronger mark) whose input the game is waiting on:
   in Linko those differ, because the ROBBED player answers the dilemma. */
.lk-seat.lk-active { background: rgba(255, 255, 255, 0.11); }
.lk-seat.lk-acting { outline-color: var(--accent); }
/* The comparison, before the cards are committed. */
.lk-seat.lk-target { outline-color: #7fd1a0; outline-style: dashed; }
.lk-seat-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 6px;
    flex-wrap: wrap;
}
.lk-seat-name { font-weight: 700; font-size: 14px; }
.lk-seat-hand { font-size: 12px; opacity: 0.75; }
.lk-seat-score {
    margin-left: auto;
    font-size: 20px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    color: var(--accent);
}
.lk-seat-score.neg { color: var(--red); }
/* --- a stack: buried chips, then the group still at risk ------------------ */
.lk-buried {
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
    align-items: center;
    min-height: 4px;
    margin-bottom: 5px;
}
.lk-buried-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 2px;
    padding: 1px 6px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.09);
    font-size: 11px;
    line-height: 1.5;
    white-space: nowrap;
}
.lk-buried-n { opacity: 0.7; }
.lk-buried-n::after { content: '\00d7'; margin-left: 1px; }
.lk-buried-num { font-weight: 700; font-variant-numeric: tabular-nums; }
.lk-buried-total { font-size: 11px; opacity: 0.55; margin-left: 2px; }
.lk-stack { display: flex; flex-direction: column; gap: 4px; }
/* WRAPPING IS LOAD-BEARING: .cardrow does not wrap unless asked, and a seat
   panel is 190px wide while a group can be seven cards. Without this the top
   group runs out past the panel, which on a phone means over the panel next to
   it. Same for the two other card rows inside a fixed-width box below. */
.lk-top { min-height: 46px; align-items: flex-end; flex-wrap: wrap; row-gap: 4px; }
.lk-top .card { --cw: 44px; }
.lk-target .lk-top .card { box-shadow: 0 0 0 2px #7fd1a0, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
.lk-top-label {
    display: flex;
    align-items: baseline;
    gap: 5px;
    font-size: 12px;
    flex-wrap: wrap;
}
.lk-top-count { opacity: 0.7; font-variant-numeric: tabular-nums; }
.lk-top-num { font-weight: 800; font-size: 14px; }
.lk-top-flag { color: #7fd1a0; font-weight: 700; margin-left: 2px; }
.lk-empty { font-size: 12px; opacity: 0.5; font-style: italic; min-height: 46px; }
/* --- the Duel ghost ------------------------------------------------------ */
.lk-ghost {
    padding: 8px 10px 9px;
    border-radius: 10px;
    background: rgba(160, 190, 255, 0.07);
    border: 1px dashed rgba(180, 205, 255, 0.35);
    margin-bottom: 12px;
}
.lk-ghost-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 6px;
    flex-wrap: wrap;
}
.lk-ghost-name { font-weight: 700; font-size: 14px; }
.lk-ghost-count { font-size: 12px; opacity: 0.7; }
.lk-ghost-groups { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; }
/* A group is a <button> when it may be chosen and a <div> otherwise; the resets
   keep the two identical. */
.lk-ghost-group {
    appearance: none;
    background: rgba(255, 255, 255, 0.05);
    color: inherit;
    font: inherit;
    text-align: left;
    padding: 4px 6px 3px;
    border: 1px solid transparent;
    border-radius: 8px;
}
.lk-ghost-cards { flex-wrap: wrap; row-gap: 3px; }
.lk-ghost-cards .card { --cw: 30px; }
.lk-ghost-cards .card + .card { margin-left: -13px; }
.lk-ghost-label {
    font-size: 11px;
    opacity: 0.8;
    font-variant-numeric: tabular-nums;
    margin-top: 2px;
}
.lk-ghost-group.lk-target { border-color: #7fd1a0; border-style: dashed; }
/* --- the pile, the discards and the row ---------------------------------- */
.lk-row {
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
    align-items: flex-start;
    margin-bottom: 12px;
}
/* The two piles sit in their own box to the LEFT of the row, with a rule between
   them: sharing one flat strip with the six row positions, a face-down pile read
   as a seventh position of the row, which is confusing in exactly the way a
   face-down card in a face-up row would be. */
.lk-piles {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    padding-right: 14px;
    border-right: 1px solid var(--panel-border);
}
.lk-rowbox { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.lk-rowhead { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.lk-rowlabel {
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.6;
}
/* Draws owed, beside the cards they come off. The status line and the action
   panel say it too, but this is where the player is looking. */
.lk-drawnote {
    font-size: 12px;
    font-weight: 700;
    color: var(--accent-ink);
    background: var(--accent);
    padding: 1px 8px;
    border-radius: 999px;
    white-space: nowrap;
}
.lk-pilebox { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.lk-pilebox .card { --cw: 46px; }
.lk-pilecount {
    font-size: 11px;
    opacity: 0.7;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.lk-rowcards { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; }
/* A row position, which in the Duel can hold a pair: the second card is offset
   rather than stacked, so a pair is unmistakably two cards for one draw. */
.lk-slot {
    appearance: none;
    background: transparent;
    color: inherit;
    font: inherit;
    border: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
}
.lk-slot .card { --cw: 46px; }
.lk-slot .card + .card { margin-top: -50px; margin-left: 16px; }
.lk-slot-pair { font-size: 10px; color: var(--accent); white-space: nowrap; }
/* Anything the local player may press this instant. Linko scatters these across
   the row and the pile during a draw, so the affordance has to be loud. */
.lk-pickable { cursor: pointer; }
.lk-pickable .card, .card.lk-pickable {
    box-shadow: 0 0 0 2px var(--accent), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.lk-pickable:hover { transform: translateY(-3px); }
.lk-pickable { transition: transform 120ms ease; }
/* --- the decision panel -------------------------------------------------- */
.lk-actions { margin: 4px 0 12px; }
.lk-numchips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.lk-numchip {
    appearance: none;
    background: rgba(255, 255, 255, 0.07);
    color: inherit;
    font: inherit;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    padding: 3px 10px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    cursor: pointer;
}
.lk-numchip:hover { background: rgba(255, 255, 255, 0.13); }
.lk-numchip.chosen {
    background: var(--accent);
    color: var(--accent-ink);
    border-color: var(--accent);
    font-weight: 700;
}
.lk-preview { font-size: 13px; line-height: 1.5; margin-bottom: 8px; min-height: 2.8em; }
.lk-waiting { font-size: 13px; opacity: 0.7; font-style: italic; }
.lk-dilemma-cards { margin-bottom: 8px; flex-wrap: wrap; row-gap: 6px; }
.lk-dilemma-cards .card { --cw: 52px; }
.lk-playrow { flex-wrap: wrap; }
.lk-hint {
    font-size: 13px;
    color: var(--accent);
    line-height: 1.45;
    margin: 0 0 8px;
}
/* --- the result ---------------------------------------------------------- */
.lk-scoretable { margin: 10px 0; font-variant-numeric: tabular-nums; }
.lk-scorerow {
    display: grid;
    grid-template-columns: minmax(90px, 1.6fr) repeat(3, minmax(56px, 1fr));
    gap: 6px;
    padding: 4px 6px;
    border-radius: 8px;
    align-items: baseline;
}
.lk-scorerow.header { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.6; }
.lk-scorerow.won { background: rgba(255, 209, 102, 0.14); }
.lk-scorecell { font-size: 13px; text-align: right; }
.lk-scoreseat { text-align: left; font-weight: 700; }
.lk-scoretotal { font-weight: 800; color: var(--accent); }
/* ---------- narrow screens ----------
   The top group and the row keep enough size to be read at a glance: a 4 you
   cannot tell from a 9 makes the one comparison in the game impossible. The
   buried chips are already text and shrink for free. */
@media (max-width: 720px) {
    .lk-seat { flex: 1 1 150px; }
    .lk-top .card { --cw: 38px; }
    .lk-slot .card, .lk-pilebox .card { --cw: 40px; }
    /* Stacked on a phone, so the rule between the piles and the row goes
       underneath them instead of beside them. */
    .lk-piles { padding-right: 0; padding-bottom: 8px; border-right: 0; border-bottom: 1px solid var(--panel-border); }
    .lk-row { flex-direction: column; }
    .lk-slot .card + .card { margin-top: -44px; margin-left: 13px; }
    .lk-dilemma-cards .card { --cw: 44px; }
    /* Scoped through the class this game's page puts on #hand-panel, because the
       id belongs to every page. Unscoped, this resized every game's hand. */
    .lk-handpanel #hand-cards .card { --cw: 46px; }
    .lk-row { gap: 10px; }
}

/* =========================================================================
   Biscuits (biscuits)
   Two things this screen has to make unmissable, both of them partnership
   facts rather than card facts:

     WHO IS ON YOUR SIDE. Partners sit across, so the three other seats are
     drawn where they sit - partner opposite (pos-up), opponents left and
     right - and the partner's panel is tinted with the accent while the two
     opponents stay neutral. "The opponent clockwise of the trick winner" is a
     rule stated in those words, so the ring has to be legible at a glance.

     WHAT IS ON THE TABLE, AND WHOSE IT IS. The whole climb stays on screen
     rather than just the top of it, one step per play, each step labeled with
     the player and what the cards ARE (.bsc-step-combo). A step from our side
     of the table gets the accent edge, one from theirs a cool edge, so
     "my partner is winning this trick" is answered without reading a name. The
     step holding the trick is raised (.bsc-step-top).

   NOTHING MOVES. Every panel that regularly changes size reserves its tallest
   ordinary shape instead of growing into its neighbors - see the RESERVED SPACE
   block below for the three devices and what each was worth. Measured with a
   headless browser over whole matches: everything above the hand is now stable to
   within a pixel at 1280px and at 390px, where it used to shift by 105px and
   333px respectively.

   THE SMALL CARDS USE THE SIMPLIFIED ATLAS (`.card.simple`, flat rank-over-suit
   art) while the pile and the hand keep the full art. The two tiny zones - the
   seat strips at 22-26px and the discard row at 20-24px - are separate panels
   from the two full-art ones, so there is no mixed row to jar, and at 22px the
   full art is a smear whichever resolution it comes from. This board was one of
   the measurements that turned that observation into the general rule; see the
   note on `.card.simple` up the file and the card-art section of
   .kiro/steering/ui-conventions.md.

   The shared platform chrome (.panel, .panel-head, .controls, .card,
   .winner-banner, .result-note, .logline, .progress) is reused as-is, so only
   what is genuinely Biscuits's own lives here - all of it prefixed `bsc-`.
   ========================================================================= */

.bsc-board {
    display: grid;
    gap: 12px;
    margin-bottom: 12px;
}

/* ---------- RESERVED SPACE ----------
   Everything on this screen changes size as a hand runs: the climb grows a step
   a play, the hand shrinks a card a play, notes come and go per phase, and the
   discard fills up. Left alone, each of those shoves its neighbors, and the
   thing under the pointer is never where it was a moment ago.

   So the panels that regularly change size RESERVE their tallest ordinary shape
   instead. Three devices, in order of how much they buy:

     - .bsc-climb never wraps and reserves one step's height, so the pile is the
       same height whether it holds nothing or eight plays. That alone was 105px
       of movement at desktop and 333px at phone width, all of it directly above
       the hand.
     - #hand-cards reserves its full two rows, so the row count falling as the
       hand empties does not walk the action bar up the screen.
     - the status and progress lines reserve two lines each, which is what they
       need at phone width and what they were jumping between.

   Heights are expressed in terms of the card size (--cw, and the card aspect
   89/64 = 1.39) rather than as bare pixels, so they follow the fluid card
   scaling instead of drifting out of step with it. */

/* One step of the climb: card, plus the name above and the combination below. */
.bsc-board { --bsc-stepcw: 42px; }
.bsc-step-cards .card { --cw: var(--bsc-stepcw); }

/* --- the two partnership scores ------------------------------------------ */
/* Two chips, not four. Biscuits is scored by the PAIR, and a per-seat column
   would invite reading your own total as the one that matters. */
.bsc-scoreboard {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
}
.bsc-teamchip {
    display: flex;
    align-items: baseline;
    gap: 8px;
}
.bsc-teamname { font-size: 12px; opacity: 0.75; }
.bsc-teamscore {
    font-size: 20px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

/* --- the three other seats, drawn where they sit ------------------------- */
/* A three-column grid with the far chair in the middle: the viewer's own hand
   panel is the fourth side of the table, below. */
.bsc-seats {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-areas: 'left up right';
    align-items: end;
    gap: 10px;
}
.bsc-seat.pos-left { grid-area: left; }
.bsc-seat.pos-up {
    grid-area: up;
    /* Raised clear of its neighbors. With the row's bottoms aligned, bottom
       margin is what lifts it - and unlike a transform it still reserves the
       space, so nothing underneath is overlapped. */
    margin-bottom: 20px;
}
.bsc-seat.pos-right { grid-area: right; }
.bsc-seat {
    min-width: 0;
    /* All three the same height, so the partner's extra "2 cards you gave" chip
       cannot make its panel taller than its neighbors and nudge the row. */
    min-height: 104px;
    padding: 8px 10px 10px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default so the .active ring never shifts the layout. */
    outline: 2px solid transparent;
}
/* Your partner, tinted. The one seat whose success is your success. */
.bsc-seat.bsc-mate {
    border-color: rgba(255, 209, 102, 0.45);
    background: rgba(255, 209, 102, 0.07);
}
/* The seat on turn. Gold is the platform's "this is where the action is". */
.bsc-seat.active { outline-color: var(--accent); }
/* A seat that has gone out: no cards, still at the table, and its exit already
   scored. Faded rather than hidden, because the SECOND player out ends the
   hand and the count of who is left is the whole tension. */
.bsc-seat.bsc-outseat { opacity: 0.6; }
.bsc-seat-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 4px;
}
.bsc-seat-name { font-weight: 700; font-size: 14px; }
.bsc-seat-role {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.55;
    white-space: nowrap;
}
.bsc-seat.bsc-mate .bsc-seat-role { color: var(--accent); opacity: 0.85; }
/* The concealed hand: one back per card held. The COUNT is public at a real
   table; the identities are not, and are never rendered - except the cards the
   VIEWER handed this seat, which the viewer already knows about. */
.bsc-seat-cards {
    display: flex;
    gap: 0;
    min-height: 34px;
    align-items: flex-end;
    flex-wrap: nowrap;
    overflow: hidden;
}
.bsc-seat-cards .card { --cw: 22px; margin-right: -13px; }
.bsc-seat-cards .card.bsc-known {
    --cw: 26px;
    margin-right: 3px;
    /* Ringed rather than merely bigger: this is a card you PUT there, and
       telling it apart from the backs is the point of showing it. */
    outline: 1px solid rgba(255, 209, 102, 0.75);
    border-radius: 3px;
}
.bsc-seat-line {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 6px;
    margin-top: 6px;
    /* Two lines' worth, because the chips wrap at phone width. */
    min-height: 17px;
    font-size: 12px;
    opacity: 0.75;
}
.bsc-seat-count { white-space: nowrap; }
/* Two cards left is the moment to start worrying, and one is the moment to
   stop planning. */
.bsc-seat.bsc-foe .bsc-seat-count { font-weight: 700; opacity: 1; }
.bsc-seat-ready { color: var(--accent); font-weight: 700; opacity: 1; }
.bsc-seat-passed { opacity: 0.6; font-style: italic; }
.bsc-seat-note { opacity: 0.6; }

/* --- the pile: the whole climb, oldest first ----------------------------- */
.bsc-tablecombo {
    font-size: 13px;
    font-weight: 700;
    color: var(--accent);
    white-space: nowrap;
}
/* Who leads next. Always present, in every phase - the answer is what a player
   most often wants from this panel, and a line that appears and disappears is a
   line that moves the board. */
.bsc-leadline {
    font-size: 13px;
    font-weight: 700;
    color: var(--accent);
    opacity: 0.9;
    margin: 2px 0 8px;
    min-height: 18px;
}
.bsc-climb {
    display: flex;
    /* NOWRAP, and this is the single biggest stabilizer on the screen: a climb
       that wraps is a panel whose height depends on how many plays a trick has
       had, which is the one thing about it that keeps changing. Eight steps fit a
       desktop row; a phone scrolls, and scrollToEnd() in the render keeps the
       newest step - the one that has to be beaten - in view. */
    flex-wrap: nowrap;
    overflow-x: auto;
    align-items: flex-end;
    gap: 10px;
    /* One step: the cards, the name above, the combination below, and the 8px the
       winning step is raised by. */
    min-height: calc(var(--bsc-stepcw) * 1.39 + 62px);
}
.bsc-step {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 6px 8px 8px;
    border-radius: 9px;
    /* A left edge rather than a full border: four steps side by side with four
       boxes around them reads as a table of its own. */
    border-left: 3px solid var(--panel-border);
    background: rgba(255, 255, 255, 0.04);
}
/* Ours and theirs. Not names: "is my side winning this trick" has to be
   answerable without reading. */
.bsc-step.bsc-step-us { border-left-color: rgba(255, 209, 102, 0.7); }
.bsc-step.bsc-step-them { border-left-color: rgba(130, 180, 230, 0.6); }
/* The combination currently holding the trick, raised and brightened. */
.bsc-step.bsc-step-top {
    background: rgba(255, 255, 255, 0.09);
    margin-bottom: 8px;
}
.bsc-step-who {
    font-size: 12px;
    font-weight: 700;
    opacity: 0.85;
    white-space: nowrap;
}
.bsc-step-cards { display: flex; gap: 3px; }
/* nowrap, because a long label ("Full House (2s over 5s)") wrapping to a
   second line makes its step taller than its neighbors and the climb taller
   than the height reserved for it. */
.bsc-step-combo { font-size: 11px; opacity: 0.7; white-space: nowrap; }
.bsc-step.bsc-step-top .bsc-step-combo { opacity: 1; color: var(--accent); }
/* Top-aligned inside the reserved climb area, where the first step would be,
   rather than floating in the middle of it. */
.bsc-empty { font-size: 13px; opacity: 0.65; align-self: flex-start; padding-top: 6px; }

/* --- the discard: everything spent this hand ----------------------------- */
.bsc-discardrow {
    display: flex;
    flex-wrap: wrap;
    gap: 2px;
    /* Two rows reserved and capped: the pile fills from nothing to 40-odd cards
       over a hand, and without this the log below it walks steadily down the
       page. */
    min-height: calc(2 * 24px * 1.39 + 2px);
    max-height: calc(2 * 24px * 1.39 + 2px);
    overflow-y: auto;
}
.bsc-discardrow .card.bsc-mini { --cw: 24px; }

/* --- the hand ------------------------------------------------------------ */
/* A card that cannot be part of ANY legal play this turn. Exact, not a rule of
   thumb (usableCardIds), and DIMMED rather than disabled, because a
   combination is assembled through illegal intermediate selections and a dead
   card can still be the one tapped by mistake. */
/* Opacity AND a filter, which is the platform's convention for a dead card
   (.em-dead, .crisps-dead, .l3-dead and .polarite-dead all pair the two). A
   filter on its own was tried first and measured too subtle to read at a
   glance against the recolored sprites - grayscale leaves a hue-shifted card
   looking merely printed on cheaper stock. Safe here because the hand is a
   spaced .cardrow rather than an overlapping fan, so transparency never shows
   one card through another. */
#hand-cards .card.bsc-dead {
    opacity: 0.38;
    filter: grayscale(0.7);
}
/* And it does not lift under the pointer: there is nothing there to pick up. */
#hand-cards .card.bsc-dead:hover { transform: none; }
/* A card your partner passed you: the one thing in your hand that somebody
   else at the table knows about. */
#hand-cards .card.bsc-fromMate {
    outline: 2px solid rgba(255, 209, 102, 0.55);
    border-radius: 4px;
}

/* --- the action column --------------------------------------------------- */
.bsc-actions {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 10px;
}
/* THE CONTROLS ROW, always first and always one button tall. See the comment on
   renderActions: buttons and notes used to share one wrapping box in whatever
   order each phase read best, and because a note is full-width, a phase that
   explained itself first pushed its buttons onto a second row - the Play button
   moved 119px between phases. */
.bsc-actionrow {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    min-height: 38px;
}
/* The prose under them, free to be as long as it needs to be: nothing the player
   aims at lives below this point. */
.bsc-actionnotes {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
/* The two shared lines the shell paints ABOVE the board, reserved at two lines
   each: both of them wrap at phone width, and anything that moves up there moves
   the whole screen. Scoped through the classes this game's own renderer adds
   (`bsc-status`, `bsc-scoreboard`), because the ids themselves belong to every
   game's page. */
.status.bsc-status { min-height: 44px; }
.progress.bsc-scoreboard { min-height: 56px; align-content: center; }
.bsc-note {
    font-size: 13px;
    line-height: 1.5;
    opacity: 0.85;
}
.bsc-note.bad { color: #ff8a80; opacity: 1; }
.bsc-note.bsc-warn { color: #ffcc80; opacity: 1; }
/* The partnership prompt: passing to let your partner keep the trick. The one
   piece of advice this game genuinely needs to give. */
.bsc-note.bsc-tip { color: var(--accent); opacity: 1; }
.bsc-note.bsc-quiet { font-size: 12px; opacity: 0.55; }
.bsc-note.bsc-hint { color: var(--accent); opacity: 1; }
/* A card named inside a sentence, colored the way the log colors card labels. */
.bsc-card { font-weight: 700; white-space: nowrap; }

/* --- the between-hands report ------------------------------------------- */
.bsc-report {
    flex: 1 1 100%;
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-bottom: 8px;
}
.bsc-report-title { font-weight: 700; font-size: 14px; }
.bsc-report-line { font-size: 13px; opacity: 0.85; }
.bsc-report-total {
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

/* --- the final result --------------------------------------------------- */
.bsc-final {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 8px 0;
}
.bsc-final-row {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 4px 8px;
    border-radius: 8px;
}
.bsc-final-row.bsc-final-winner { background: rgba(255, 209, 102, 0.14); }
.bsc-final-name { font-weight: 700; }
.bsc-final-score {
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

/* --- the log ------------------------------------------------------------ */
.logline.bsc-logout { color: var(--accent); font-weight: 700; }
.logline.bsc-logtrick { opacity: 0.75; }

/* --- cards named in the Cheat_Sheet's prose ------------------------------
   Classes on the elements themselves rather than on a wrapper, so the same
   rules style the Rules summary and the biscuits/rules/ page. */
.bsc-suit { font-weight: 700; }
/* A card named in a sentence: suit glyph and rank kept on one line, so the
   line never breaks between them. */
.bsc-egcard { white-space: nowrap; font-weight: 700; }

/* ---------- narrow screens ----------
   The ring collapses to a single column: the far chair is still first, so
   reading top to bottom is still reading around the table from the partner
   down. Cards in the climb stay big enough to tell a 9 from a 10, because
   ranks are the only thing this game compares. */
@media (max-width: 720px) {
    .bsc-seats {
        grid-template-columns: 1fr 1fr;
        grid-template-areas:
            'up up'
            'left right';
    }
    .bsc-seat.pos-up { margin-bottom: 0; }
    /* The step card size moves through the VARIABLE, not through a rule on the
       card, so the climb's reserved height follows it instead of staying at the
       desktop figure. */
    .bsc-board { --bsc-stepcw: 36px; }
    /* Both shared lines take a third line at this width, and the scoreboard's
       team chips wrap. */
    .status.bsc-status { min-height: 50px; }
    .progress.bsc-scoreboard { min-height: 66px; }
    /* The lead line takes two lines at this width, and it sits above the hand. */
    .bsc-leadline { min-height: 36px; }
    .bsc-discardrow { min-height: calc(2 * 20px * 1.39 + 2px); max-height: calc(2 * 20px * 1.39 + 2px); }
    .bsc-discardrow .card.bsc-mini { --cw: 20px; }
    /* Scoped through the class this game's page puts on #hand-panel, because the
       id belongs to every page. Unscoped, this resized every game's hand. The
       three rows a 13-card hand takes at this size are reserved at runtime by
       ui/handspace.ts, so there is no min-height to keep in step with it. */
    .bsc-handpanel #hand-cards .card { --cw: 46px; }
}

/* =========================================================================
   Measure and Tempo (measuretempo)
   Layout intent: the board is a vertical sandwich - the opponent's strip and
   Guard card on top, the trump box beside either the circling clock (Out of
   Measure) or the tempo row (In Measure) in the middle, the viewer's Guard
   and strip below. The tempo row is the rulebook's own diagram: one column
   per tempo, their card above and yours below, facing each other, with an
   arrow between them pointing at whoever took it. Nothing is captured in
   this game, so the winner is MARKED (.mt-tookit, .mt-arrow) instead of
   sliding anywhere, and the whole row stays up through the between-plays
   screen. A live Feint is a card back (.mt-feintback) to everyone but the
   player who led it, whose own copy gets a dashed edge (.mt-facedown). Turn
   state rides the acting seat's chip (.mt-toact), its side border
   (.mt-active) and the lit status line (.mt-turn). The Out of Measure menu
   is a side action column INSIDE the hand panel (the Schnapsen recipe), so
   arming Strike or Change Guard never moves the cards.
   ========================================================================= */
/* Two lines' worth, always: the wording grows when it explains the follow
   obligation or what a Disengage would bank. */
.status.mt-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.mt-status { min-height: 4.4em; }
}
/* The status line lights up whenever THIS device is being asked to act. */
.status.mt-status.mt-turn {
    border-left: 3px solid var(--accent);
    padding-left: 10px;
    font-weight: 600;
}
/* Progress and strip chips: play number, race, scores, tempo tally, deck. */
.mt-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    margin-right: 8px;
    padding: 3px 10px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.18);
    border: 1px solid var(--panel-border);
    font-size: 12px;
    white-space: nowrap;
}
.mt-chip.mt-score { font-weight: 700; font-variant-numeric: tabular-nums; }
.mt-chip.mt-handcount { opacity: 0.75; }
/* The acting seat's marker: the one loud chip on the board. */
.mt-chip.mt-toact {
    background: rgba(232, 112, 58, 0.22);
    border-color: rgba(232, 112, 58, 0.65);
    font-weight: 700;
}
/* --- the board: their strip · trump + clock/tempos · my strip ------------ */
.mt-board {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-bottom: 12px;
}
.mt-side {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 8px 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
.mt-side.mt-mine { background: rgba(38, 120, 60, 0.12); }
/* The side whose turn it is carries the accent, on top of any tint. */
.mt-side.mt-active { border-color: rgba(232, 112, 58, 0.75); }
.mt-seathead {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}
.mt-seatname { font-size: 13px; font-weight: 700; }
/* --- the Guard cards, the only public cards outside a play --------------- */
.mt-guardrow {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}
.mt-guardrow .card { --cw: 46px; }
.mt-slotlabel {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    min-width: 96px;
}
.mt-guardnote { font-size: 12px; opacity: 0.7; }
/* The opponent's cards you have already seen, under their Guard. Small and
   read-only, so they come off the simplified atlas (`mini simple` at the
   call site) - at this width the full art is a smudge. They sit in the
   opponent's own strip because that is where the player is looking when
   they ask what the other hand still holds. */
.mt-known {
    display: flex;
    align-items: center;
    gap: 4px;
    flex-wrap: wrap;
}
.mt-known .card { --cw: 30px; }
@media (max-width: 620px) {
    .mt-known .card { --cw: 26px; }
}
/* The Guard actually deciding the trump: ringed, and its note says so. */
.card.mt-guardcard.mt-deciding { outline: 2px solid rgba(232, 112, 58, 0.75); }
.card.mt-guardcard:not(.mt-deciding) { opacity: 0.7; }
.card.empty.mt-guardempty { --cw: 46px; }
/* --- the middle: trump box beside the clock or the tempo row ------------- */
.mt-mid {
    display: flex;
    gap: 12px;
    align-items: stretch;
    flex-wrap: wrap;
}
.mt-trumpbox,
.mt-measure {
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
.mt-trumpbox {
    flex: 0 1 auto;
    display: flex;
    flex-direction: column;
    gap: 6px;
    justify-content: center;
    max-width: 260px;
}
.mt-trumpline {
    display: flex;
    align-items: baseline;
    gap: 8px;
    white-space: nowrap;
}
.mt-trumplabel {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
}
.mt-trumpvalue { font-size: 15px; font-weight: 700; }
.mt-trumpwhy { font-size: 11px; opacity: 0.6; line-height: 1.35; }
.mt-boxtitle {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    margin-bottom: 6px;
}
/* The circling clock: the added rule made visible, one tick per action
   still available. A trap the player cannot see is not a rule. */
.mt-measure {
    flex: 1 1 260px;
    display: flex;
    flex-direction: column;
    justify-content: center;
}
.mt-clock { display: flex; gap: 4px; margin-bottom: 6px; }
.mt-tick {
    width: 14px;
    height: 8px;
    border-radius: 3px;
    background: rgba(232, 112, 58, 0.75);
}
.mt-tick.mt-spent { background: rgba(255, 255, 255, 0.14); }
.mt-clocknote { font-size: 12px; opacity: 0.8; }
.mt-clocknote.mt-forced {
    opacity: 1;
    font-weight: 700;
    color: var(--accent);
}
/* --- the tempo row: the rulebook's diagram ------------------------------- */
.mt-tempos {
    flex: 1 1 260px;
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    align-items: flex-start;
    padding: 8px 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
    min-height: 96px;
}
.mt-tempo {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 4px 6px;
    border-radius: 10px;
}
/* A won column tints toward its winner's side, so the row reads as a score
   at a glance without counting arrows. */
.mt-tempo.mt-wontop { background: rgba(232, 112, 58, 0.10); }
.mt-tempo.mt-wonbottom { background: rgba(38, 120, 60, 0.14); }
.mt-tempocell { display: flex; }
.mt-tempocell .card { --cw: 48px; }
.mt-tempomid {
    display: flex;
    align-items: center;
    gap: 5px;
    min-height: 16px;
}
.mt-temponum {
    font-size: 10px;
    opacity: 0.55;
    font-variant-numeric: tabular-nums;
}
.mt-arrow { font-size: 11px; color: var(--accent); line-height: 1; }
.mt-feintmark {
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 1px 5px;
    border-radius: 999px;
    background: rgba(90, 95, 116, 0.55);
}
/* The card that took the tempo. There is no capture pile in this game, so
   this ring is the whole record of who won what. */
.card.mt-tookit { outline: 2px solid var(--accent); }
/* A Feint: a back to the answerer, a dashed edge to the player who led it
   (who obviously knows what it is). */
.card.mt-feintback { opacity: 0.92; }
.card.mt-facedown { outline: 2px dashed rgba(255, 255, 255, 0.5); }
@media (max-width: 620px) {
    .mt-tempocell .card { --cw: 38px; }
    .mt-guardrow .card,
    .card.empty.mt-guardempty { --cw: 40px; }
    .mt-slotlabel { min-width: 0; }
}
/* --- trump marking and muting in the hand -------------------------------- */
.card.mt-trumpcard { outline: 2px solid rgba(232, 112, 58, 0.55); }
.card.mt-mute { opacity: 0.45; }
/* A card just drawn by a Cycle, so the two new ones are findable. */
.card.mt-drawn { box-shadow: 0 0 0 3px rgba(120, 200, 255, 0.55); }
/* Picked for the bury. Deliberately louder than .mt-drawn above and in a
   different key - red and faded, "this one is leaving" - because the two
   marks appear on the same nine cards at the same moment and a player has
   to be able to tell at a glance which two they have chosen. */
.card.mt-burying {
    outline: 3px solid rgba(214, 74, 74, 0.95);
    filter: saturate(0.45) brightness(0.82);
}
/* --- the hand panel's action column (the Schnapsen recipe) --------------- */
.mt-handrow {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    flex-wrap: wrap;
}
.mt-handrow #hand-cards { flex: 1 1 260px; }
.mt-actions {
    display: none;
    flex-direction: column;
    gap: 8px;
    max-width: 320px;
}
/* The action column only ever fills when THIS device must act, so the
   filled state may shout a little. */
.mt-actions.filled {
    display: flex;
    padding: 10px 12px;
    border-radius: 12px;
    border: 1px solid rgba(232, 112, 58, 0.55);
    background: rgba(232, 112, 58, 0.08);
}
.mt-actionnote { font-size: 13px; opacity: 0.95; font-weight: 600; line-height: 1.4; }
.mt-actionhint { font-size: 12px; opacity: 0.7; }
.mt-actrow {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-wrap: wrap;
}
.mt-actbtn { min-width: 56px; }
/* The Feint answer. It is only ever on screen while the question is open,
   so it needs no selected state; `primary` already carries the emphasis. */
.mt-feintbtn { min-width: 56px; }
.mt-actions-inner { display: contents; }
/* --- the between-plays summary ------------------------------------------- */
.mt-summary .result-table.mt-result { margin-top: 10px; }
.mt-result .result-cell.mt-won { font-weight: 800; color: var(--accent); }

/* The foot-of-page game strip is two rows now (tools/gen-pages.mjs
   otherGamesStrip): the similar games first, then the full index. The second
   row drops the divider and the top margin so the pair reads as one block with
   two labeled lines rather than two separate sections. */
.setup-othergames.othergames-rest {
    margin-top: 8px;
    padding-top: 0;
    border-top: 0;
}
.setup-othergames.othergames-rest a { opacity: 0.82; }
.setup-othergames.othergames-rest a:hover { opacity: 1; }

/* ------------------------------------------------------------------------- */
/* CrossCribb                                                                */
/* ------------------------------------------------------------------------- */
/*
 * Prefix policy: every CrossCribb-specific class is `ccb-` (matching `sono-`,
 * `vid-`, `mt-`). Shared classes are reused as-is and get no rules here: the
 * face-down pile and the opponent's discards are `.card.simple.back`, the
 * action row is a `.controls`, cards flash with `.card.hinted`, and the
 * outcome uses `.winner-banner` and `.result-note`.
 *
 * The board is ONE six-by-six grid rather than a grid plus separate score
 * strips (the Sono construction), so the row chips down the right edge and the
 * column chips along the bottom stay aligned with their lines by construction
 * - no shared width variable to keep in sync, and nothing to re-align at a
 * media query.
 *
 * TURN CLARITY is carried in four places at once, because a status sentence
 * alone has never been enough: the acting side's panel takes an accent border
 * (.ccb-acting), the status line lights up when THIS device must act
 * (.ccb-turn), the peg chip for the local player is tinted (.ccb-peg.me), and
 * every open square is outlined the moment it becomes playable (.ccb-cell.legal).
 */
/*
 * Reserved so the board never jumps as the wording changes from "AI is
 * thinking" to a full explanation of what is being asked. The two numbers are
 * MEASURED rather than guessed: over a played-out round the longest status wraps
 * to one line at desktop width and two on a phone, so this reserves exactly
 * that and no more. An earlier pair of eyeballed values reserved nearly double
 * and left a visible hole under the status on every turn.
 */
.status.ccb-status { min-height: 1.7em; }
@media (max-width: 620px) {
    .status.ccb-status { min-height: 3em; }
}
.status.ccb-status.ccb-turn {
    border-left: 3px solid var(--accent);
    padding-left: 10px;
    font-weight: 600;
}
/* --- the peg track: the match score, on screen at all times --------------- */
.ccb-pegs {
    display: flex;
    align-items: stretch;
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 10px;
}
.ccb-peglabel {
    align-self: center;
    font-size: 12px;
    opacity: 0.7;
    margin-right: 2px;
}
.ccb-peg {
    display: flex;
    align-items: baseline;
    gap: 7px;
    padding: 4px 12px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.18);
    border: 1px solid var(--panel-border);
    font-size: 12px;
    white-space: nowrap;
}
.ccb-peg.me { background: rgba(38, 120, 60, 0.16); }
/* Someone has crossed 31: the match is decided, so say so loudly. */
.ccb-peg.out { border-color: var(--accent); }
.ccb-pegname { font-weight: 600; }
.ccb-pegscore {
    font-size: 17px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
.ccb-peg.out .ccb-pegscore { color: var(--accent); }
.ccb-pegrole { opacity: 0.65; }
/* The round's running total, kept visually subordinate to the match score
   beside it: two numbers in one chip need a clear senior partner. */
.ccb-peground {
    padding-left: 7px;
    margin-left: 2px;
    border-left: 1px solid var(--panel-border);
    opacity: 0.8;
    font-variant-numeric: tabular-nums;
}
/* --- board beside the turn panel ----------------------------------------- */
/*
 * The board is as tall as five readable cards, so stacking the turn panel under
 * it pushed the ONE card the turn is about off the bottom of the screen. Side by
 * side, the card, the crib, the action button and the squares are all in view at
 * once - and at the end of a round the summary takes the same column, so the
 * finished board stays next to the counting that came out of it.
 */
.ccb-layout {
    display: flex;
    align-items: flex-start;
    gap: 18px;
    flex-wrap: wrap;
}
.ccb-side {
    flex: 1 1 300px;
    min-width: 260px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
/* The panels in the side column own their spacing through the flex gap. */
.ccb-side .panel { margin: 0; }
.ccb-side .controls { margin: 0; }
/*
 * No opt-out needed any more. This tray is always ONE row - a card, a pile and
 * two crib slots - and the runtime reservation (ui/handspace.ts) measures that
 * rather than assuming rows, so there is nothing here to cancel. What used to
 * sit here was `#hand-panel #hand-cards { min-height: 0 }`, which was two ids,
 * so it beat the then-shared reservation on EVERY page and turned the site's
 * anti-jitter default off entirely.
 */
/* --- the board ----------------------------------------------------------- */
.ccb-board { display: flex; flex-direction: column; gap: 6px; flex: 0 0 auto; }
.ccb-roleline {
    font-size: 13px;
    opacity: 0.85;
    text-align: center;
    font-variant-numeric: tabular-nums;
}
.ccb-grid {
    --ccb-gap: 5px;
    --ccb-cw: min(var(--cw), calc((100vw - 800px) / 5));
    display: grid;
    grid-template-columns: repeat(5, var(--ccb-cw)) auto;
    gap: var(--ccb-gap);
    width: fit-content;
    margin: 2px auto;
    align-items: center;
    justify-items: center;
}
/* Less the cell's 1px border on each side, so a card fills its square exactly. */
.ccb-grid .card { --cw: calc(var(--ccb-cw) - 2px); }
.ccb-cell {
    position: relative;
    width: var(--ccb-cw);
    height: calc(var(--ccb-cw) * 89 / 64); /* CARD_ASPECT */
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 8px;
    border: 1px solid transparent; /* so highlights never shift layout */
}
.ccb-cell.empty {
    background: rgba(255, 255, 255, 0.05);
    border: 1px dashed rgba(255, 255, 255, 0.25);
}
/* The Cut Card's square. Ringed all round rather than tinted, because the card
   sitting in it is a real card that both players count - the ring says "this
   one was cut, not played", which is what makes row 3 and column C special. */
.ccb-cell.cut {
    box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.75);
}
.ccb-cutflag {
    position: absolute;
    bottom: -1px;
    left: 50%;
    transform: translateX(-50%);
    padding: 0 4px;
    border-radius: 4px 4px 0 0;
    background: rgba(255, 209, 102, 0.92);
    color: #1a1a1a;
    font-size: 9px;
    font-weight: 800;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    pointer-events: none;
}
/* Every open square is legal - there is no adjacency rule - so the lit set is
   simply "everything still empty" and there is no inert region to gray out. */
.ccb-cell.legal {
    cursor: pointer;
    background: rgba(255, 255, 255, 0.14);
    border: 1px solid rgba(255, 255, 255, 0.55);
}
.ccb-cell.legal:hover, .ccb-cell.legal:focus-visible {
    background: rgba(255, 255, 255, 0.22);
    outline: none;
}
/* --- the preview: the cross a considered square sits on -------------------- */
/*
 * Playing a card lands it on exactly one of your lines and one of theirs, and
 * that pair is the whole decision. Washing the two lines makes the cross
 * visible, which on a board of twenty-five cards it otherwise is not.
 */
.ccb-cell.previewing {
    background: rgba(232, 112, 58, 0.3);
    border-color: var(--accent);
}
.ccb-cell.oncross { box-shadow: 0 0 0 2px rgba(232, 112, 58, 0.28) inset; }
.ccb-cell.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
/* Score chips: the right-hand column is the rows, the bottom row the columns.
   Tinted for whose line it is, so the board itself says who is building what. */
/*
 * CONTRAST IS THE POINT OF THESE CHIPS, and two things had been eating it.
 *
 * 1. They are <button>s (the arithmetic is a tap away), and nothing reset the
 *    browser's button background, so every chip sat on the user agent's
 *    rgb(239, 239, 239). Gold text on that is close to invisible. `background`
 *    and `color` are therefore set on EVERY state below, never inherited.
 * 2. A chip with no points yet was faded to 0.55 opacity, which on the felt put
 *    a zero below any readable contrast. A zero is information too (this line
 *    is open, or blocked), so nothing here uses opacity.
 *
 * Owner is carried by a FILL, not by a thin border: yours are solid gold with
 * dark text, theirs solid near-black with white text. Both read at well over
 * 7:1, and at a glance the two sides of the board are two different colors.
 */
.ccb-score {
    position: relative;
    min-width: 2.1em;
    padding: 2px 6px;
    border-radius: 6px;
    border: 2px solid rgba(255, 255, 255, 0.55);
    background: #14201a;
    color: #fff;
    font-family: inherit;
    font-size: 15px;
    font-weight: 800;
    line-height: 1.3;
    text-align: center;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
}
.ccb-score.mine {
    border-color: var(--accent);
    background: var(--accent);
    color: #1a1a1a;
}
/* An empty line still reads, it just reads as unpainted: the same colors with
   the fill dropped back to the felt. */
.ccb-score.theirs:not(.scoring) { background: #14201a; color: rgba(255, 255, 255, 0.85); }
.ccb-score.mine:not(.scoring) { background: #14201a; color: var(--accent); }
.ccb-score:hover, .ccb-score:focus-visible {
    outline: 3px solid #fff;
    outline-offset: 1px;
}
/* The chip whose breakdown is open. */
.ccb-score.open {
    outline: 3px solid #fff;
    outline-offset: 1px;
    box-shadow: 0 0 0 5px rgba(232, 112, 58, 0.8);
}
/* --- the forecast: what a considered square would do to this line ---------- */
/*
 * Both chips a considered square touches are marked, because the chips sit
 * OUTSIDE the grid and so the board's cross wash does not reach them. Only a
 * chip that would actually move carries an arrow: `.flat` is the one that is in
 * play and gains nothing, which in a blocking game is worth knowing.
 */
.ccb-score.up, .ccb-score.flat { border-color: #fff; }
.ccb-score.flat { border-style: dashed; }
/*
 * The chip's own number, as a block so an outline drawn on it hugs the whole
 * chip. It is a separate passive element for a reason beyond layout: the chip is
 * a <button> now, and the tutorial's explanation beats may not point at anything
 * pressable (tutorial_drive_worker's ack rule), so they point at THIS instead
 * and say exactly the same thing.
 */
.ccb-scorenow { display: block; }
/* The forecast sits on its own dark pill so it reads on a gold chip and a dark
   one alike; a bare green on gold was the same contrast trap as before. */
.ccb-scorenext {
    display: block;
    margin-top: 2px;
    padding: 0 3px;
    border-radius: 4px;
    background: #0d1a12;
    color: #8fe39a;
    font-size: 11px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
/* --- Double Trouble ------------------------------------------------------ */
/*
 * One mark, used everywhere the die reaches: an orange "x2" tag. It sits on the
 * die badge beside the pegs, on the two score chips the roll doubles, and on
 * the crib boxes when the roll is a 6, so the badge's words and the board point
 * at each other. Before this the only signs were a clause at the end of the
 * progress line, naming a "column C" the board never labels, and a faint tint
 * on two chips.
 */
.ccb-x2 {
    display: inline-block;
    padding: 0 4px;
    border-radius: 4px;
    background: #e8703a;
    color: #fff;
    font-size: 11px;
    font-weight: 900;
    line-height: 1.45;
    letter-spacing: 0.02em;
    white-space: nowrap;
}
/* On a chip the tag is a corner badge, so the score number keeps its place. */
.ccb-score .ccb-x2 {
    position: absolute;
    top: -9px;
    right: -11px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
    pointer-events: none;
}
.ccb-score.doubled { border-color: #e8703a; }
.ccb-uplabel .ccb-x2 { margin-left: 5px; }
/* The die itself: a white die face with real pips, then the words. */
.ccb-die {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px 10px 4px 5px;
    border-radius: 999px;
    border: 2px solid #e8703a;
    background: rgba(232, 112, 58, 0.18);
    font-size: 13px;
    font-weight: 700;
    white-space: nowrap;
}
.ccb-dieface {
    display: grid;
    grid-template-columns: repeat(3, 6px);
    grid-template-rows: repeat(3, 6px);
    gap: 2px;
    padding: 4px;
    border-radius: 6px;
    background: #fff;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.ccb-pip { width: 6px; height: 6px; border-radius: 50%; }
.ccb-pip.on { background: #1a1a1a; }
/*
 * A column chip is a compact badge centered under its column, NOT the full width
 * of the square above it. Stretched to `width: 100%` the chips sat a bare gap
 * apart, and any outline they wear - the tutorial's `.coach-mark` is 3px with a
 * 2px offset, five in total against a five-pixel gap - collided with its
 * neighbors' into one continuous band. Compact badges leave the outlines room
 * to read as five separate highlights, and match the row chips down the edge.
 */
.ccb-score-column { justify-self: center; }
.ccb-corner { width: 100%; }
/* --- the turn panel: one card, a pile, and two crib slots ----------------- */
.panel.ccb-acting { border-color: rgba(232, 112, 58, 0.75); }
/* Scoped through .ccb-side, the wrapper this game's page puts both panels in.
   Both ids belong to other pages (#opp-cards to three other games), so unscoped
   this set every game's hand row to an 18px gap and bottom-aligned it, against
   the .cardrow default of 8px and flex-start. */
.ccb-side #hand-cards, .ccb-side #opp-cards { align-items: flex-end; gap: 18px; }
.ccb-up, .ccb-pile, .ccb-crib {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.ccb-uplabel {
    font-size: 11px;
    opacity: 0.7;
    text-align: center;
    white-space: nowrap;
}
/*
 * EVERY CARD SIZE BELOW IS SCOPED TO ITS PANEL ID, and has to be. An unscoped
 * `#hand-cards .card { --cw: 62px }` lives further up this stylesheet, and at
 * one id plus one class it out-specifies any pair of classes - so a bare
 * `.card.ccb-upcard { --cw: 88px }` silently rendered at 62px and the turned-up
 * card came out SMALLER than a board card. Prefixing with the panel id
 * (one id, one class, one class) is what wins, and the phone overrides below
 * need the same treatment.
 */
/* The one card the turn is about, drawn a size up from everything else. */
#hand-panel .card.ccb-upcard, #opp-panel .card.ccb-upcard { --cw: 96px; }
/*
 * The face-down pile: backs PEEKING out from under the top one, so the stack
 * reads as physically deep. A box-shadow was not obvious enough.
 *
 * The panel-id scoping is load-bearing here too, and for a second unscoped
 * rule: `#hand-cards .card { position: relative }` (Pop's Joker ribbons) also
 * sits further up this stylesheet, so a bare `.card.ccb-under { position:
 * absolute }` lost and the pile rendered as three separate cards in a column.
 * The z-indexes are explicit because both layers end up positioned, and a
 * positioned element otherwise paints ABOVE its static siblings - which would
 * put the peeking backs in front of the card they are meant to hide behind.
 */
/*
 * `isolation: isolate` is what keeps the two z-indexes below LOCAL, and it is
 * load-bearing rather than tidiness. They exist only to order two backs against
 * each other, but as page-level values a positive z-index lifted the pile out of
 * the reach of the tutorial's coach panel, which docks over the board at narrow
 * widths (platform.css, z-index 30) - the pile painted straight through the
 * coaching text. Raising the panel did NOT fix it and zeroing the card's
 * z-index did, which is the tell that the competition was never between the two
 * numbers. Isolating the stack confines them to it, so the pile can never again
 * out-paint page chrome no matter what z-index that chrome picks.
 */
.ccb-stack { position: relative; isolation: isolate; }
#hand-panel .ccb-stack .card, #opp-panel .ccb-stack .card {
    --cw: 52px;
    position: relative;
    z-index: 2;
}
#hand-panel .ccb-pile.mini .ccb-stack .card,
#opp-panel .ccb-pile.mini .ccb-stack .card { --cw: 36px; }
/* After the rule above, so the equal-specificity tie falls this way. */
#hand-panel .card.ccb-under, #opp-panel .card.ccb-under {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1;
}
/* Crib slots. The empty outline and the card that lands in it are driven by ONE
   variable, so they cannot drift apart and nothing moves when a discard lands. */
.ccb-crib { --ccb-crib-cw: 42px; }
.ccb-crib.mini { --ccb-crib-cw: 32px; }
.ccb-cribrow { display: flex; gap: 4px; align-items: flex-end; }
#hand-panel .ccb-crib .card,
#opp-panel .ccb-crib .card { --cw: var(--ccb-crib-cw); }
.ccb-cribslot {
    width: var(--ccb-crib-cw);
    height: calc(var(--ccb-crib-cw) * 89 / 64); /* CARD_ASPECT */
    border-radius: 5px;
    border: 1px dashed rgba(255, 255, 255, 0.3);
    background: rgba(255, 255, 255, 0.04);
}
.ccb-note { font-size: 12px; opacity: 0.75; align-self: center; }
.controls.ccb-acting .btn { border-color: rgba(232, 112, 58, 0.6); }
.btn.hinted { animation: hintpulse 0.9s ease-in-out infinite alternate; }
.ccb-error { margin-top: 4px; font-size: 13px; color: #ff9c8a; }
/* --- the breakdown behind one score chip ---------------------------------- */
.ccb-detail { border-color: var(--accent); }
.ccb-detailhead {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
}
.ccb-detailhead h3 { margin: 0; font-size: 14px; }
/* The five cards being counted. Empty squares keep their room, so a line that
   is still filling does not shuffle the list below it as cards arrive. */
.ccb-detailcards { display: flex; gap: 4px; margin: 8px 0; align-items: flex-end; }
.ccb-detail .card { --cw: 38px; }
.ccb-detailgap {
    width: 38px;
    height: calc(38px * 89 / 64); /* CARD_ASPECT */
    border-radius: 5px;
    border: 1px dashed rgba(255, 255, 255, 0.22);
}
.ccb-detaillist { list-style: none; margin: 0; padding: 0; }
.ccb-detailitem {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 2px 0;
    font-size: 13px;
    border-top: 1px solid rgba(255, 255, 255, 0.07);
}
.ccb-detailpts {
    min-width: 1.6em;
    text-align: right;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
.ccb-detaillabel { flex: 0 0 auto; opacity: 0.85; }
.ccb-detailmade { flex: 1 1 auto; text-align: right; }
.ccb-detailmade .cardlabel { margin-left: 5px; }
.ccb-detailtotal {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-top: 6px;
    padding-top: 5px;
    border-top: 1px solid var(--panel-border);
    font-weight: 700;
}
.ccb-detailnone, .ccb-detailnote, .ccb-detailahead {
    margin: 6px 0 0;
    font-size: 12px;
    opacity: 0.75;
}
.ccb-detailahead {
    opacity: 1;
    color: var(--accent);
    font-weight: 600;
}
/* --- the round summary --------------------------------------------------- */
.ccb-results { display: flex; flex-wrap: wrap; gap: 14px; }
.ccb-resultcol {
    flex: 1 1 260px;
    padding: 8px 10px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
}
.ccb-resultcol.won { border-color: var(--accent); }
.ccb-resultcol h3 { margin: 0 0 6px; font-size: 14px; }
.ccb-resultline {
    display: flex;
    align-items: baseline;
    gap: 8px;
    font-size: 13px;
    opacity: 0.6;
}
.ccb-resultline.scoring { opacity: 1; }
.ccb-resultname { min-width: 5.5em; opacity: 0.8; }
.ccb-resultlabel { flex: 1 1 auto; }
/* The crib's five cards, named. They need real separation or "2♥A♠10♠J♠4♠"
   runs together into something nobody can read a rank out of. */
.ccb-resultcards { margin-right: 2px; }
.ccb-resultcards .cardlabel { margin-right: 6px; }
.ccb-resultpts { font-weight: 700; font-variant-numeric: tabular-nums; }
.ccb-resulttotal {
    margin-top: 6px;
    padding-top: 5px;
    border-top: 1px solid var(--panel-border);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
@media (max-width: 720px) {
    .ccb-grid { --ccb-cw: min(52px, calc((100vw - 58px) / 5)); --ccb-gap: 3px; }
    .ccb-roleline { font-size: 12px; }
    /* Panel-scoped for the same reason as the rules above. */
    #hand-panel .card.ccb-upcard, #opp-panel .card.ccb-upcard { --cw: 68px; }
    #hand-panel .ccb-stack .card, #opp-panel .ccb-stack .card { --cw: 40px; }
    #hand-panel .ccb-pile.mini .ccb-stack .card,
    #opp-panel .ccb-pile.mini .ccb-stack .card { --cw: 30px; }
    .ccb-crib { --ccb-crib-cw: 34px; }
    .ccb-crib.mini { --ccb-crib-cw: 28px; }
    .ccb-side #hand-cards, .ccb-side #opp-cards { gap: 12px; }
    /* Same contrast, sized for a 52px board. A chip here is barely wider than
       its number, so a corner badge covered the score itself: the x2 tag drops
       into the chip's own flow, as a second line under the number. */
    .ccb-score { font-size: 13px; padding: 1px 4px; min-width: 1.9em; }
    .ccb-score .ccb-x2 {
        position: static;
        display: block;
        margin: 2px -2px 0;
        padding: 0 2px;
        font-size: 10px;
        box-shadow: none;
    }
    .ccb-die { font-size: 12px; white-space: normal; }
}
body.motion-off .ccb-cell.hinted,
body.motion-off .btn.hinted { animation: none; }
/* =========================================================================
 * Expeditions (inspired by Lost Cities)
 *
 * Prefix policy: every Expeditions-specific class is `xpd-`. Not `exp-`, which
 * would give `exp-expscore` for an expedition's score chip, and not `xp-`, which
 * Exchange Poker already uses for two element ids. The shared layout
 * classes (.card, .cardrow, .panel, .chip, .linebtn, .result-*, .replay-*) are
 * reused as-is, so only what is genuinely new lives here.
 *
 * The board is four COLUMNS, one per expedition, laid out as the printed
 * board is: the discard piles across the middle, each under its suit's glyph,
 * and the two sides' cards running outward from them.
 *
 * NOTHING HERE MOVES DURING A TURN. The lanes are reserved at a six-card
 * expedition's height, every turn control is an overlay (.xpd-drop), and the
 * status line is reserved at two lines on a phone. Measure with
 * tools/exp-measure.mjs after touching any of it.
 * ========================================================================= */

.xpd-board {
    /*
     * One knob for every card on the board, sized by the viewport's HEIGHT as
     * well as its width: the whole table, the opponent's side down to your
     * hand, has to be on one screen at once. --xpd-chrome is everything that
     * is not the board's cards (the toolbar, the status and score lines, the
     * chips and padding inside the board, and the hand panel: 396px measured
     * at 1280x720, plus a little slack), and 7.12 card widths is the board's
     * own height (two lanes of 3.06 each and a sideways pile). Measured with
     * tools/exp-fit.mjs; re-measure after changing any of it. Never under
     * 40px, the phone size: on a very short screen the page scrolls rather
     * than the cards shrinking past legibility.
     */
    --xpd-chrome: 400px;
    --xpd-rows: 7.12;
    --xpd-cw: clamp(40px, min(calc(26px + 1.9vw),
        calc((100vh - var(--xpd-chrome)) / var(--xpd-rows))), 60px);
    --xpd-cw: clamp(40px, min(calc(26px + 1.9vw),
        calc((100dvh - var(--xpd-chrome)) / var(--xpd-rows))), 60px);
    /* Fraction of an overlapped card left visible, up to the reserved length. */
    --xpd-step: 0.3;
    /*
     * The length every lane is reserved at, so the band is as tall on the
     * first turn as on the last. Five, one short of the bonus: a sixth card
     * and beyond tighten the lane to the same height (see .xpd-lane), which
     * still leaves each index readable, and the sixth row of headroom was
     * most of why the board did not fit on a laptop screen.
     */
    --xpd-fit: 5;
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 10px;
}

/* The partnership game keeps a row for its pass between the board and the
   hand (.xpd-passrow, 46px with its margin), so its board has that much less. */
.xpd-board.xpd-four { --xpd-chrome: 446px; }
/* The solitaire has no other side, so its board is one lane and the piles. */
.xpd-board.xpd-solo { --xpd-rows: 4.06; }

/* Panel-scoped, because an unscoped `.card` size rule leaks into every other
   game's hand (this stylesheet warns about that twice already). */
#xpd-board .card.xpd-card { --cw: var(--xpd-cw); }
#xpd-board .card.xpd-card.xpd-tiny { --cw: 30px; }

/*
 * Four expeditions across, three BANDS down: the other side's cards climbing away
 * from the middle, then the four discard piles, then the viewer's own cards
 * climbing down toward them. The viewer is always the bottom band.
 *
 * Bands rather than four self-contained columns, because a band is one flex row:
 * its four cells are the same height, so the piles line up across the board
 * however uneven the expeditions are.
 */
.xpd-table {
    display: flex;
    flex-direction: column;
}

.xpd-band {
    display: flex;
    gap: 6px;
}
/* The other side's cards hang from the middle upward, so their band packs to the
   BOTTOM; the viewer's grows down from it and packs to the top. */
.xpd-bandthem { align-items: flex-end; }
.xpd-bandmine { align-items: flex-start; }
.xpd-bandmid { align-items: center; }

.xpd-cell {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 3px 2px;
}
/* The stripe runs the whole height of an expedition, across all three bands, so
   a column still reads as one place even though it is built as rows. */
.xpd-cell:nth-child(even) { background: rgba(255, 255, 255, 0.05); }
.xpd-bandthem .xpd-cell { border-radius: 10px 10px 0 0; padding-top: 6px; }
.xpd-bandmine .xpd-cell { border-radius: 0 0 10px 10px; padding-bottom: 6px; }

/*
 * A lane is RESERVED at the height of --xpd-fit cards overlapped at --xpd-step,
 * whatever it holds, so a band is the same height on the first turn and the
 * last. Past --xpd-fit cards the step tightens so the lane still fits:
 * def.ts sets `--xpd-n` to the card count and this does the arithmetic,
 * (fit - 1) steps shared among (n - 1) gaps, never looser than --xpd-step.
 *
 * Stretched to the cell's width (the cards stay centered in it) because the
 * lane is also the host of the "play here" overlay, which should be the whole
 * column rather than one card wide. `isolation` keeps the cards' inline
 * z-indexes inside the lane.
 */
.xpd-lane {
    --xpd-lstep: min(var(--xpd-step),
        var(--xpd-step) * (var(--xpd-fit) - 1) / max(1, var(--xpd-n, 1) - 1));
    position: relative;
    isolation: isolate;
    align-self: stretch;
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: calc(var(--xpd-cw) * 89 / 64 * (1 + (var(--xpd-fit) - 1) * var(--xpd-step)));
}
/*
 * Overlap: each card partially covers the one before, as a table pile does. The
 * step is a fraction of a card's HEIGHT here, not its width.
 *
 * EVERY card is positioned, not only the investments. A positioned element
 * paints above an unpositioned sibling whatever the DOM order says, so when only
 * the courts were `position: relative` (for their badge) a King lay on top of
 * the 5 played after it and hid its index. With all of them positioned the
 * inline z-index def.ts sets is the only thing deciding the order.
 */
#xpd-board .xpd-lane .card { position: relative; }
.xpd-lane .card + .card {
    margin-top: calc(var(--xpd-cw) * 89 / 64 * (var(--xpd-lstep) - 1));
}
/* Growing upward: the LAST card is nearest the far edge, so the stack is laid
   out bottom-up. def.ts also reverses the paint order for this lane, which is
   what keeps every card's index uncovered. */
.xpd-lanethem { flex-direction: column-reverse; }
.xpd-lanethem .card + .card {
    margin-top: 0;
    margin-bottom: calc(var(--xpd-cw) * 89 / 64 * (var(--xpd-lstep) - 1));
}

.xpd-emptylane {
    color: var(--muted);
    opacity: 0.5;
    padding: 4px 0;
    font-size: 13px;
}

/* --- the middle: the discard piles ------------------------------------ */

.xpd-bandmid .xpd-cell {
    flex-direction: row;
    justify-content: center;
    padding: 4px 2px;
    border-top: 1px solid var(--panel-border);
    border-bottom: 1px solid var(--panel-border);
}

/*
 * A pile lies SIDEWAYS, in a box that is always the same landscape shape,
 * empty or not, so nothing beside it moves as it fills and empties.
 *
 * Its cards are turned by a rotation, not drawn as differently-shaped
 * elements: the atlas tier is chosen from offsetWidth (see
 * src/shared/ui/sprites.ts), which reports the untransformed box, so a rotate()
 * is safe where a scale() would pick the wrong resolution. They are absolutely
 * centered, so their portrait layout box never pushes on the landscape one,
 * and EVERY card in the pile is there, stacked exactly, with only the top one
 * seen (see discardSlot in def.ts for why). A card's flight turns it on the
 * way: shared/ui/animate.ts reads the turn of the face it leaves and the face
 * it lands on, so a discard lies down as it travels rather than on arrival.
 */
.xpd-pile, .xpd-deck {
    position: relative;
    isolation: isolate;
    flex: none;
    width: calc(var(--xpd-cw) * 89 / 64);
    height: var(--xpd-cw);
    border-radius: 7px;
}
#xpd-board .xpd-pile > .card.xpd-card,
#xpd-board .xpd-deck > .card.xpd-card {
    position: absolute;
    left: 50%;
    top: 50%;
    /* `translate` and `rotate`, never `transform`: the pop-in, the hover lift
       and the flight ghost all write `transform`, and replacing a turn kept
       there painted the card upright until they finished (ui-conventions, "A
       sideways card turns with rotate"). */
    translate: -50% -50%;
    rotate: 90deg;
}
/* Under the top card: seen by nobody, but a stack of their shadows would
   darken the felt round the pile. */
#xpd-board .xpd-pile > .card.xpd-under { box-shadow: none; }
.xpd-emptypile {
    position: absolute;
    inset: 0;
    border: 1px dashed rgba(255, 255, 255, 0.35);
    border-radius: 7px;
    /* Pale enough that the dark inks (Clubs, Spades) read on it. */
    background: rgba(255, 255, 255, 0.2);
}

/*
 * The expedition's sign, laid over its pile: the suit, large and a little
 * transparent, so the card underneath still reads. The inks are the SATURATED
 * ones the card faces print (the "hue" set in tools/card-palettes.mjs), not the
 * site's --red / --blue-club, which are light inks for text on the dark board;
 * this glyph sits on a white card face as often as on the felt. The halo is
 * what keeps a black spade visible on the felt when the pile is empty.
 */
.xpd-glyph {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 2;
    transform: translate(-50%, -54%);
    font-size: calc(var(--xpd-cw) * 0.86);
    line-height: 1;
    opacity: 0.55;
    pointer-events: none;
    text-shadow: 0 0 2px rgba(255, 255, 255, 0.9), 0 0 5px rgba(255, 255, 255, 0.5);
}
/* Over an empty slot there is no card face to see through to, so the glyph is
   the whole content of the box and is drawn nearly solid. */
.xpd-emptypile ~ .xpd-glyph { opacity: 0.85; }
.xpd-ink0 { color: #26384a; }
.xpd-ink1 { color: #d94a2f; }
.xpd-ink2 { color: #202020; }
.xpd-ink3 { color: #d32f2f; }

.xpd-pilecount {
    position: absolute;
    right: -3px;
    bottom: -3px;
    z-index: 3;
    min-width: 15px;
    padding: 0 3px;
    border-radius: 8px;
    /* Opaque: it sits on a white card face, where the translucent panel fill
       left the number all but invisible. */
    background: rgba(0, 0, 0, 0.78);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.35);
    font-size: 11px;
    line-height: 15px;
    text-align: center;
    font-variant-numeric: tabular-nums;
    pointer-events: none;
}

/* A live destination or source. */
.xpd-target { outline: 2px dashed var(--accent); outline-offset: 3px; border-radius: 8px; }
.xpd-blocked { opacity: 0.55; }
/* During the draw, a pile whose top card the viewer could no longer play. Still
   takeable; the fade is the same arithmetic the hand does (.xpd-dead). */
#xpd-board .xpd-pile.xpd-deadpile > .card { filter: brightness(0.62) saturate(0.55); }

/*
 * Every turn control is an OVERLAY on the thing it acts on: the lane, the pile,
 * the deck. Inserted into the flow beside its target, as it once was, a button
 * pushed the board around each time a card was chosen. The host is the
 * positioned element it covers; the visible word is a small pill, and the whole
 * area is the tap target.
 */
.xpd-drop {
    position: absolute;
    inset: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 8px;
    background: rgba(255, 209, 102, 0.1);
    color: inherit;
    font: inherit;
    cursor: pointer;
}
/* On a lane the new card lands at the far end, so the pill sits there too, over
   empty reserved space rather than over the cards already played. */
.xpd-lane > .xpd-drop { align-items: flex-end; padding-bottom: 6px; }
.xpd-droplabel {
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-ink);
    font-size: 12px;
    font-weight: 700;
    line-height: 17px;
    white-space: nowrap;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
}
.xpd-drop:hover { background: rgba(255, 209, 102, 0.2); }
.xpd-drop:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- the investment badge --------------------------------------------- */

/*
 * The courts ARE the printed game's handshake cards, so they are labeled.
 * pointer-events: none is load-bearing: an armed card face is a real <button>
 * and the badge must never become the click target.
 */
#xpd-board .card.xpd-invest,
#hand-cards .card.xpd-invest,
#result-panel .card.xpd-invest { position: relative; }
/* In a lane only the TOP strip of a card shows (the next card covers the rest),
   so the badge moves up into it, beside the index rather than over it. */
#xpd-board .xpd-lane .card.xpd-invest .xpd-badge {
    top: calc(var(--cw) * 89 / 64 * 0.05);
    transform: translateX(-50%);
    width: calc(var(--cw) * 0.36);
    height: calc(var(--cw) * 0.36);
    font-size: calc(var(--cw) * 0.27);
}
/* In a result-screen row the next card covers the RIGHT of this one, and the
   small face is all rank and suit. The rank is what says "court", and the row
   already names the suit, so the badge sits low, over the suit, clear of the
   letter. */
#result-panel .xpd-minilane .card.xpd-invest .xpd-badge {
    left: 36%;
    top: auto;
    bottom: 3%;
    transform: translateX(-50%);
    width: calc(var(--cw) * 0.4);
    height: calc(var(--cw) * 0.4);
    font-size: calc(var(--cw) * 0.3);
}
.card.xpd-invest .xpd-badge {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    pointer-events: none;
    /* A ring around a multiplication sign. Sized off the card, so it is legible
       on a 30px result-screen thumbnail and on a full-size card in hand. */
    width: calc(var(--cw) * 0.46);
    height: calc(var(--cw) * 0.46);
    border-radius: 50%;
    border: max(1px, calc(var(--cw) * 0.045)) solid #b8860b;
    background: rgba(255, 248, 225, 0.94);
    color: #6b4c07;
    font-size: calc(var(--cw) * 0.34);
    font-weight: 800;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
}

/* --- per-expedition score chips --------------------------------------- */

.xpd-expscore {
    flex: none;
    display: flex;
    flex-direction: row;
    align-items: baseline;
    justify-content: center;
    gap: 5px;
    min-width: 0;
    padding: 1px 6px;
    border-radius: 7px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    line-height: 1.2;
    white-space: nowrap;
}
.xpd-expnum {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    font-size: 15px;
}
.xpd-expscore.pos .xpd-expnum { color: var(--good, #6fbf73); }
.xpd-expscore.neg .xpd-expnum { color: var(--bad, #d97a7a); }
.xpd-expwork {
    font-size: 10px;
    color: var(--muted);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.xpd-minescore { border-color: var(--accent); }

/* --- the fifth column: the deck --------------------------------------- */

/*
 * One fixed width in every band (a sideways card and its cell's padding), so
 * the four flexible expedition columns come out the same width in all three
 * bands and stay aligned. Only the middle band's has anything in it but the
 * solitaire's note; the deck box itself shares .xpd-pile's landscape shape.
 */
.xpd-cell.xpd-side {
    flex: none;
    width: calc(var(--xpd-cw) * 89 / 64 + 4px);
    background: none;
    justify-content: center;
}
/* The count sits in the middle of the deck, so its pill goes to the foot. */
.xpd-deck > .xpd-drop { align-items: flex-end; padding-bottom: 2px; }
.xpd-decknum {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 2;
    transform: translate(-50%, -50%);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    padding: 1px 5px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    font-size: 13px;
    pointer-events: none;
}
.xpd-sidenote {
    font-size: 11px;
    line-height: 1.25;
    color: var(--muted);
    text-align: center;
    overflow-wrap: anywhere;
}
.xpd-dim { opacity: 0.7; }

/*
 * Where the other side's cards fly from and to (see laneCards): the far edge of
 * their lane, out of flow. Zero height, so the flight starts at the edge.
 */
.xpd-reach {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 0;
    pointer-events: none;
}
/* The viewer's own lane grows DOWN, so its far edge (where a partner's cards
   come from) is the bottom. */
.xpd-lanemine > .xpd-reach { top: auto; bottom: 0; }

/*
 * What the other seats played since the viewer last moved (recentPlays in
 * def.ts). A cool blue ring, the hue the site's other last-play markers use
 * (.duel-board .card.last-played), because gold already means a live target or
 * a selection here.
 */
/*
 * An INSET ring, drawn inside the card's own box: in a lane most of the card is
 * covered by the next one, and an outline outside the box ran down both sides
 * of the cards covering it, so it framed three cards instead of one.
 */
#xpd-board .card.xpd-recent {
    box-shadow: inset 0 0 0 3px #6aa9e0, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.xpd-warn { color: var(--bad, #d97a7a); }

/*
 * The deck warming up as the round runs out. The rules invite counting the deck
 * to plan the finish, so the number is always there; this is what makes "nearly
 * over" impossible to miss. Graded in TURNS LEFT rather than cards, so the
 * warning means the same thing at every table size (see deckUrgency in def.ts).
 */
.xpd-yellow { color: #e3c05a; }
.xpd-orange { color: #e08a45; }
.xpd-red, .xpd-spent { color: #e2685f; }
.xpd-decknum.xpd-yellow, .xpd-decknum.xpd-orange, .xpd-decknum.xpd-red {
    background: rgba(0, 0, 0, 0.72);
}
.xpd-decknum.xpd-red { animation: xpd-deckpulse 1.6s ease-in-out infinite; }
.xpd-deckchip.xpd-yellow { border-color: #e3c05a; }
.xpd-deckchip.xpd-orange { border-color: #e08a45; }
.xpd-deckchip.xpd-red, .xpd-deckchip.xpd-spent { border-color: #e2685f; }
@keyframes xpd-deckpulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.55; }
}

/* The round-by-round pad, under the hand: it appears after the first round of
   three, and below the hand it moves nothing a player is reaching for. */
.xpd-scores { display: flex; justify-content: center; margin-top: 10px; }
.xpd-pad { border-collapse: collapse; font-size: 12px; }
.xpd-pad th, .xpd-pad td {
    padding: 2px 8px;
    text-align: right;
    border-bottom: 1px solid var(--panel-border);
}
.xpd-pad th:first-child, .xpd-pad td.xpd-padround { text-align: left; color: var(--muted); }
.xpd-padnum { font-variant-numeric: tabular-nums; font-weight: 600; }
.xpd-pad .xpd-mine { color: var(--accent); }
.xpd-padtotal td { border-bottom: 0; border-top: 2px solid var(--panel-border); font-weight: 700; }

/* The deck counter in the scoreboard line. Deliberately the same pill shape as
   .progress-score beside it, but quieter: it is information, not a score. */
.xpd-deckchip {
    display: inline-flex;
    align-items: baseline;
    gap: 5px;
    padding: 3px 12px;
    border-radius: 999px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.xpd-deckchipnum { font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; }
.xpd-deckchipname { font-size: 13px; opacity: 0.75; }

/* --- the action box --------------------------------------------------- */

.xpd-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px;
}
/* The partnership game's pass, the only thing this row ever holds (see
   renderActions). Reserved at one .btn's height (38px, measured), so the offer
   appearing for the play step and going for the draw does not shift the hand
   under the cursor. At one and two seats the row is empty and takes no room. */
.xpd-actions.xpd-passrow {
    min-height: 38px;
    margin-bottom: 8px;
}
.status.xpd-turn { border-left: 3px solid var(--accent); padding-left: 8px; }
/* An explicit line height, so the phone reservation below is exact. */
#status-line.xpd-status { line-height: 1.35; }

/* --- the hand --------------------------------------------------------- */

/*
 * A card the expeditions have already passed by can never be played, only
 * discarded. Fading it is the one piece of arithmetic the hand can do for the
 * player. filter, not opacity, so an overlapped fan does not show the card
 * beneath through it.
 */
#hand-cards .card.xpd-dead { filter: brightness(0.62) saturate(0.55); }
#hand-cards .card.xpd-dead.sel { filter: none; }

/* --- the result screen ------------------------------------------------ */

.xpd-resultgrid {
    display: flex;
    gap: 14px;
    flex-wrap: wrap;
    margin-top: 8px;
}
.xpd-resultcol {
    flex: 1 1 280px;
    min-width: 0;
    padding: 8px;
    border-radius: 8px;
    border: 1px solid var(--panel-border);
}
.xpd-resultcol.xpd-mine { border-color: var(--accent); }
.xpd-resulthead { font-weight: 700; margin-bottom: 6px; }
/*
 * The next deal, at the foot of the round summary. Lifted over the board, the
 * summary is its own scroll box and two boards overflow a phone's, so the row
 * sticks to the bottom of the box: the button is always in view and the boards
 * scroll under it. Painted the dialog's own color so they pass beneath cleanly.
 */
.panel.result-overlay .xpd-roundactions {
    position: sticky;
    /* Past the panel's 14px bottom padding, and out to its 16px sides, so
       nothing scrolls by below or beside the bar. */
    bottom: -14px;
    z-index: 1;
    justify-content: center;
    margin: 8px -16px 0;
    padding: 10px 16px 20px;
    /* The dialog's color as it APPEARS: the shell's dimming backdrop
       (.panel.result-overlay::before) paints over the panel's own background,
       and this bar sits above the backdrop. */
    background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)),
        linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
    box-shadow: 0 -8px 12px -6px rgba(0, 0, 0, 0.5);
}

.xpd-mini { display: flex; flex-direction: column; gap: 3px; }
.xpd-minirow { display: flex; align-items: center; gap: 6px; }
.xpd-mininame { flex: none; width: 62px; font-size: 12px; color: var(--muted); }
.xpd-minilane { flex: 1 1 auto; display: flex; align-items: center; min-width: 0; }
/* Positioned for the reason the board's lanes are (see .xpd-lane): DOM order,
   which is play order, then decides the overlap, so no court lies on top of the
   number played after it. */
#result-panel .xpd-minilane .card { position: relative; }
.xpd-minilane .card + .card { margin-left: -9px; }
.xpd-minilane .xpd-dim { font-size: 12px; }
.xpd-miniscore { min-width: 48px; }
.xpd-minitotal {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-top: 4px;
    padding-top: 4px;
    border-top: 1px solid var(--panel-border);
    font-weight: 700;
}
#result-panel .card.xpd-tiny { --cw: 30px; }

/* The solo comparison's tab strip: one runner a tab, its panel below carries
   that runner's whole board round by round. */
.xpd-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 6px; }
.xpd-tab {
    padding: 4px 10px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
    color: inherit;
    font: inherit;
    font-size: 13px;
    cursor: pointer;
}
.xpd-tab.selected { border-color: var(--accent); color: var(--accent); font-weight: 700; }
.xpd-tab.xpd-dim { cursor: default; opacity: 0.6; }
.xpd-tabpanel { display: flex; flex-direction: column; gap: 10px; }

/* --- phones ----------------------------------------------------------- */

@media (max-width: 720px) {
    /* Four columns have to fit across the screen, and a sideways pile is a
       card's HEIGHT wide. */
    .xpd-board { --xpd-cw: min(40px, calc((100vw - 48px) / 7)); --xpd-step: 0.3; }
    /* Two lines reserved: every status line is written to fit in two at this
       width (see renderStatus), and a line that went from two to one moved the
       whole board up under the player's finger. */
    #status-line.xpd-status { min-height: calc(2 * 1.35 * 18px); }
    .xpd-table { gap: 3px; }
    .xpd-expscore { padding: 0 3px; gap: 3px; }
    .xpd-expnum { font-size: 13px; }
    /* The working behind each score is the first thing to go: the number itself
       is what a player needs mid-turn. */
    .xpd-expwork { display: none; }
    /* Wide enough for "Diamonds", the longest name, at 12px. */
    .xpd-mininame { width: 60px; }
    .xpd-belowboard { gap: 8px; }
    .xpd-scores { margin-left: 0; }
}

body.motion-off .xpd-tab,
body.motion-off .card.xpd-dead { transition: none; }
body.motion-off .xpd-decknum.xpd-red { animation: none; }
/* =========================================================================
   Short Zoot Suit (zootsuit)
   A must-follow trick taker whose score is a DIAGONAL: tricks won against cards
   you could not follow with, paying 5 a pair when the two are level and 3 when
   they are not. So the layout is built to keep three things in view at once,
   and everything prefixed `szs-` exists for one of them:

     the two counts, side by side, on every seat        (.szs-pairbox)
     the shorts pile, face up, on every seat            (.szs-shorts)
     what the round is currently paying                 (.szs-chip.szs-prov)

   Bands, from the public table down to the private cards:
     the other seats, arranged AROUND the viewer        (.szs-table)
     the trick as a cross, a fixed slot per seat        (.szs-trick)
     this player's own counters and shorts pile         (.szs-mine)
     this player's draw deck, face up and sorted        (.szs-deck)
     the action wanted from this device                 (.szs-actions)
     this player's hand                                 (#hand-panel)
     what the scoring card in play pays                 (.szs-ref)

   ANTI-JITTER. Nothing above the hand may change height as a round runs, and
   this game has four things that want to: the shorts piles grow every trick,
   the "trick just taken" strip appears on one seat, the action box empties
   during trick play, and the status line changes line count per phase. Each is
   reserved rather than allowed to size itself - see the min-heights below and
   the one-row card strips, which scroll instead of wrapping.

   Every shared class (.card, .cardrow, .panel, .panel-head, .progress-score,
   .status, .logline, .btn, .result-table, .winner-banner, .result-note, .seg)
   is reused as is; rules that name a SHARED id are scoped through a class this
   game owns (.szs-status on the status line, .szs-handpanel on the hand panel),
   because those ids are on all 87 pages.
   ========================================================================= */
/* --- the scoreboard ------------------------------------------------------ */
/* HIGH score wins here, so `.progress-score.leading` marks the MAXIMUM. */
.szs-scorename { font-size: 12px; opacity: 0.8; margin-right: 5px; }
.szs-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }
/* Spaced off the score chips beside it: the shared .progress line sets no gap,
   so "AI East 19Trick 10Unique Suits" ran together. */
.szs-progress-trick { opacity: 0.7; margin-left: 10px; }
.szs-scoreboard .szs-bonuschip { margin-left: 10px; }
/* Which scoring card is in play, and whether the variant is on. Reference
   rather than news, so quiet - but always there, because both change how every
   decision in the game is priced. */
.szs-bonuschip {
    font-size: 12px;
    padding: 1px 8px;
    border-radius: 999px;
    border: 1px dashed var(--panel-border);
    opacity: 0.85;
    white-space: nowrap;
}
.szs-bonuschip.szs-variantchip { border-style: solid; }
/* Two rows reserved: at four seats the chips plus the trick counter plus both
   reference chips wrap, and this line sits above the whole board. */
.szs-scoreboard { min-height: 2.6em; align-items: center; }
/* --- the status line ----------------------------------------------------- */
/* Reserved height, so naming the suit to follow, or listing who a simultaneous
   phase is waiting on, cannot make the board jump. */
.status.szs-status { min-height: 2.9em; }
/* LIT whenever this device may act. A sentence on its own is not enough of a
   signal, least of all in the two phases where several seats act at once - the
   seat chip, the panel ring, the action box and this share that job. */
.status.szs-status.szs-turn {
    color: var(--accent);
    font-weight: 700;
}
.szs-hintnote { opacity: 0.85; font-weight: 400; }
/* --- the table: every seat but the one on screen ------------------------- */
.szs-table {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-areas: 'left up right';
    align-items: end;
    gap: 10px;
    margin-bottom: 12px;
}
/* Three seats have no far chair; the two that remain take the outer columns,
   which keeps the neighbors symmetrical instead of leaving a lopsided gap. Two
   seats face each other, so the one opponent takes the middle. */
.szs-table.seats-3 { grid-template-areas: 'left . right'; }
.szs-table.seats-2 { grid-template-areas: '. up .'; }
.szs-seat.pos-left { grid-area: left; }
.szs-seat.pos-right { grid-area: right; }
.szs-seat.pos-up {
    grid-area: up;
    /* Raised clear of its neighbors so the row reads as an arc around a table.
       Bottom margin rather than a transform, so the space stays reserved and
       nothing underneath is overlapped. */
    margin-bottom: 18px;
}
.szs-seat,
.szs-mine {
    min-width: 0;
    padding: 8px 10px 10px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default, so the accent ring never shifts the layout. */
    outline: 2px solid transparent;
    /* NO min-height HERE ON PURPOSE. Every band inside a panel reserves its own
       tallest shape (the chips row, the suit strip, the shorts strip, the
       trick-just-taken band), which makes the panel's height constant by
       construction rather than by a guessed floor that goes stale the moment a
       band changes. Measured at 271px throughout a four-seat match, against 244
       to 403 before the bands were reserved. */
}
/* The seat being waited on. Gold is the platform's "this is where the action
   is", and in the two simultaneous phases more than one seat can carry it. */
.szs-seat.szs-active,
.szs-mine.szs-active { outline-color: var(--accent); }
/* --- a seat's two rows -------------------------------------------------- */
/* `.szs-seat-top` and `.szs-seat-bot` are ONE renderer (fillSeatBox) laid out two
   ways. Here the rows stack, which is what the positional table wants: the name
   and the match total share a line and the chips take the next one, which is the
   arrangement these boards have always had. On a phone the same DOM lays out
   ACROSS the full width instead - see the media block at the foot of this file -
   and that is what takes a board from ~300px to ~55px.
   The `order` values are what let one DOM do both: the total is written between
   the chips and the bottom row, and pulled back up beside the name. */
.szs-seat-top {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 4px 8px;
    margin-bottom: 4px;
}
.szs-seat-name {
    font-weight: 700;
    font-size: 14px;
    white-space: nowrap;
    order: 0;
}
/* The match total, the number that decides the game, hard right. */
.szs-seat-total {
    font-size: 20px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    order: 1;
    margin-left: auto;
}
.szs-seat-top .szs-chips { order: 2; flex-basis: 100%; }
.szs-seat-bot { min-width: 0; }
/* --- the two concealed piles -------------------------------------------- */
/* A hand and a draw deck. Both HEIGHTS are public at a real table; neither set
   of identities is, and neither is ever rendered. Drawn as a physical STACK
   with the edges of the cards underneath peeking out, because a box shadow is
   not a readable cue that a pile has depth. */
.szs-piles {
    display: flex;
    gap: 14px;
    align-items: flex-end;
    margin-bottom: 6px;
    /* Both piles on one line at every width the panels are laid out for: they are
       compared, so they belong side by side. The labels are short for the same
       reason (see backStack). */
    flex-wrap: nowrap;
}
.szs-pile { display: flex; align-items: baseline; gap: 5px; min-width: 0; }
.szs-pile-label {
    font-size: 10px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
    white-space: nowrap;
}
.szs-stack {
    position: relative;
    width: 30px;
    height: 34px;
    flex: 0 0 auto;
}
.szs-stack .card { --cw: 22px; position: absolute; }
.szs-pile-count {
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    font-size: 14px;
}
/* An exhausted pile: the zero is the whole message, so it is dimmed rather than
   captioned. A caption inside the stack overlapped the label beside it. */
.szs-pile-count.szs-pile-none { opacity: 0.45; font-weight: 600; }
/* --- the chips every seat carries --------------------------------------- */
.szs-chips {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-wrap: wrap;
    font-size: 12px;
    /* TWO ROWS RESERVED, AT EVERY WIDTH. Whether this row wraps depends both on
       the board's width and on whether a "drew" chip is present that repaint, so
       it is reserved at its tallest ordinary shape rather than measured per
       breakpoint: at 744px it wrapped with three chips and did not with two,
       which grew every board mid-round by 24px.
       THREE ROWS, because this layout is only used above 900px but a board at
       four seats is still only ~290px wide at the bottom of that range. Below
       900px the boards become single strips and this reserve is dropped
       entirely; see the media block at the foot of the file. */
    min-height: 72px;
}
.szs-chip {
    padding: 1px 6px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.07);
    white-space: nowrap;
    opacity: 0.85;
}
/* Who is being waited on. Accented and never abbreviated: the whole board is
   read off this one chip. */
.szs-chip.szs-toact {
    background: var(--accent);
    color: var(--accent-ink);
    font-weight: 700;
    opacity: 1;
}
/* WHAT THE ROUND IS CURRENTLY PAYING THIS SEAT, out of the engine's own
   roundScores, so it cannot disagree with the score that lands. */
.szs-chip.szs-prov {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    opacity: 1;
    background: rgba(255, 255, 255, 0.12);
}
/* THE TWO NUMBERS THE GAME IS ABOUT, always together and always in the same
   order, with the sign between them saying whether they are level. Level is
   what pays 5 a pair instead of 3, so it gets a color AND a glyph: a
   color-blind player and a grayscale screenshot both still read the '='. */
.szs-pairbox {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    padding: 2px 8px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    white-space: nowrap;
}
.szs-pairnum {
    font-weight: 800;
    font-size: 15px;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.szs-pairlabel { font-size: 11px; opacity: 0.7; }
.szs-pairsign { font-weight: 800; opacity: 0.6; margin: 0 1px; }
.szs-pairbox.szs-matched {
    border-color: rgba(60, 200, 120, 0.75);
    background: rgba(60, 160, 90, 0.22);
}
.szs-pairbox.szs-matched .szs-pairsign { color: #7ce8a4; opacity: 1; }
/* --- the suit strip, on every board ------------------------------------- */
/* ALL FOUR SUITS, ALWAYS, in the display order (clubs, diamonds, spades,
   hearts), so the strip is read by POSITION rather than by spotting what is
   present. On the viewer's own board each glyph carries how many of that suit
   are in hand, which is exactly what decides whether the next lead shorts them.
   On every other board it carries only what that seat has SHOWN the table by
   failing to follow, and a suit nobody knows about yet stays faint. */
.szs-suits {
    display: flex;
    gap: 9px;
    align-items: center;
    margin-top: 4px;
    /* Reserved, so a panel does not grow the first time a suit runs out. */
    min-height: 17px;
    font-size: 12px;
}
.szs-suit {
    display: inline-flex;
    align-items: baseline;
    gap: 2px;
    /* Not known to be void, and no count to show: reference, not information. */
    opacity: 0.45;
}
.szs-suits-own .szs-suit { opacity: 1; }
.szs-suit-glyph { position: relative; font-size: 13px; line-height: 1; }
.szs-suit-count { font-weight: 700; font-variant-numeric: tabular-nums; }
/* HOLDS NONE OF THIS SUIT, which is public the moment a seat plays off it.
   Crossed out with two bars over the glyph: a shape, not a color, so it
   survives a grayscale screenshot and a color-blind player reads it the same
   way. Pseudo-elements rather than an x character, so a text harvest cannot
   pick the mark up and read it as part of the hand. */
.szs-suit-void { opacity: 0.9; }
.szs-suit-void .szs-suit-glyph::before,
.szs-suit-void .szs-suit-glyph::after {
    content: '';
    position: absolute;
    left: -3px;
    right: -3px;
    top: 50%;
    height: 2px;
    border-radius: 2px;
    background: #ff8a80;
}
.szs-suit-void .szs-suit-glyph::before { transform: rotate(45deg); }
.szs-suit-void .szs-suit-glyph::after { transform: rotate(-45deg); }
.szs-suit-void .szs-suit-count { color: #ff8a80; }
/* --- the shorts pile ---------------------------------------------------- */
/* Face up on EVERY seat, because it is public at a real table and because it is
   the whole of the scoring card. It is also where a card its player could not
   follow with slides to, under the same card id the trick cell used.
   ONE ROW, SCROLLING RATHER THAN WRAPPING: this pile grows every trick, and a
   strip that wrapped would push the entire board down a step at a time. */
.szs-shorts { margin-top: 5px; }
.szs-shorts-label {
    font-size: 10px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
    white-space: nowrap;
}
/* The nowrap, the overflow and the thin scrollbar all come from the shared
   `.cardrow.scroll`, which the renderer opts into: that class is ALSO what
   exempts the strip from the site's below-560px rule, which wraps every plain
   `.cardrow`. Only the gap, the alignment and the reservation are this game's.
   The reservation is one mini row plus `.cardrow.scroll`'s own 8px of vertical
   padding, expressed through --cw and the card aspect so it follows the fluid
   card scaling rather than drifting out of step with it. */
.szs-shorts-cards {
    gap: 3px;
    align-items: center;
    min-height: calc(26px * 1.39 + 12px);
}
.szs-shorts-cards .card.mini { --cw: 26px; flex: 0 0 auto; }
.szs-shorts-empty { font-size: 11px; opacity: 0.5; white-space: nowrap; }
/* --- the trick just taken ----------------------------------------------- */
/* It rides its WINNER's own panel rather than sitting in a box of its own, so
   the same card ids leaving the trick cells make the capture visibly SLIDE to
   whoever took it. The cards somebody was SHORT with are not here: the rules
   hand those back, and they slide to their own players' shorts piles instead,
   so the trick splits on screen exactly the way it splits in the rules. It also
   means the round's last trick is still readable on the summary screen, where
   these panels stay up.

   THE BAND IS ALWAYS THERE, and empty on the seats that did not win it: the
   dashed rule and the cards are what come and go. Reserving it exactly, rather
   than reserving a guess on the panel, is what keeps the board still - measured
   at 40px of movement a trick before this, walked down the screen every time a
   different seat took one. */
.szs-lasttrick {
    display: flex;
    align-items: center;
    gap: 3px;
    flex-wrap: nowrap;
    overflow-x: auto;
    margin-top: 5px;
    padding-top: 5px;
    /* Transparent, not absent: the rule must not change the height it reserves. */
    border-top: 1px dashed transparent;
    /* The card, plus this band's own padding and rule, because min-height is a
       BORDER-box figure here: reserving only the card left the filled band 3px
       taller than the empty one. */
    min-height: calc(24px * 1.39 + 6px);
}
.szs-lasttrick-filled { border-top-color: var(--panel-border); }
.szs-lasttrick .card.mini { --cw: 24px; flex: 0 0 auto; }
.szs-lasttrick-label {
    font-size: 10px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
    white-space: nowrap;
}
/* --- the trick in progress ---------------------------------------------- */
/* A CROSS: one fixed slot per seat, placed where that seat sits, so a card
   appears under the player who played it and never moves once it lands. */
.szs-trick {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    grid-template-areas: '. up .' 'left . right' '. down .';
    align-items: center;
    justify-items: center;
    gap: 6px 10px;
    padding: 12px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.16);
    margin-bottom: 12px;
    /* Reserved, so the board does not step up and down as the trick fills. */
    min-height: 250px;
}
.szs-trick.seats-3 {
    grid-template-areas: 'left . right' '. down .';
    min-height: 180px;
}
.szs-trick.seats-2 {
    grid-template-areas: '. up .' '. down .';
    min-height: 180px;
}
.szs-play.pos-up { grid-area: up; }
.szs-play.pos-left { grid-area: left; }
.szs-play.pos-right { grid-area: right; }
.szs-play.pos-down { grid-area: down; }
.szs-play {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.szs-play .card { --cw: 54px; }
/* Wide enough for the longest caption this builds, "AI North . leads". The
   ellipsis stays as a backstop for a long name relayed by an online peer. */
.szs-play-name {
    font-size: 11px;
    opacity: 0.7;
    max-width: 108px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* A slot still owed a card: outlined, not blank, so it reads as "waiting". */
.szs-play.empty .card.empty { border-style: dashed; }
/* The seat on turn, so the eye knows where the next card is coming from. */
.szs-play.onturn .card.empty { border-color: var(--accent); }
/* WHO IS CURRENTLY WINNING, called out on the card AND captioned underneath:
   color alone would leave it invisible to a color-blind player, and the caption
   also survives a grayscale screenshot. */
.szs-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
/* A CARD PLAYED OFF SUIT, which is not a loss but a SCORE: it is going back to
   the player who threw it. Tinted and captioned, because it leaves the trick by
   a different door from every other card in it. */
.szs-shortcard {
    outline: 2px dashed rgba(120, 200, 255, 0.8);
    outline-offset: 2px;
}
/* Always painted, empty when there is nothing to flag, so the row it occupies is
   reserved: letting it appear grew the trick grid mid-trick and pushed the hand
   down the screen while a player was reaching for a card. */
.szs-play-flag {
    font-size: 10px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--accent);
    white-space: nowrap;
    /* The line height is pinned so an EMPTY flag box is exactly as tall as a
       filled one: leaving it at `normal` made the trick grid 6px shorter with
       nothing to flag, three rows of the cross at 2px each. */
    line-height: 1.3;
    min-height: 1.3em;
}
.szs-play-flag.szs-flag-short { color: #8fd0ff; }
/* --- this player's own strip -------------------------------------------- */
.szs-mine { margin-bottom: 12px; }
/* --- this player's draw deck ------------------------------------------- */
/* FACE UP and SORTED. Its owner has this pile in front of them, so looking
   through it is theirs to do; the order is unknown to everybody, the engine's
   own shuffles included, and sorting the display is what stops it being misread
   as the draw order - which is why the note beside the title says so in words
   rather than leaving it to be inferred. */
.szs-deck { margin-bottom: 12px; }
.szs-deck-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 6px;
}
.szs-deck-title { margin: 0; font-size: 14px; }
.szs-deck-note { font-size: 11px; opacity: 0.65; }
/* One row, scrolling (shared `.cardrow.scroll`, opted into by the renderer):
   under Short Shorts a deck can grow to most of a hand. */
.szs-deck-cards {
    gap: 4px;
    align-items: center;
    min-height: calc(34px * 1.39 + 14px);
}
.szs-deck-cards .card.mini { --cw: 34px; flex: 0 0 auto; }
.szs-deck-empty { font-size: 12px; opacity: 0.6; }
/* --- contextual actions ------------------------------------------------- */
/* The one box that says what this device is being asked for. Controls only, so
   the primary button never moves between phases - and RESERVED, because trick
   play arms the cards themselves and puts no button here at all, which would
   otherwise collapse the box and walk the hand up the screen every trick. */
.szs-actions {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
    min-height: 46px;
    margin-bottom: 8px;
    padding: 0 10px;
    border-radius: 10px;
    border: 1px solid transparent;
}
.szs-actions.szs-armed {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.08);
}
/* --- the hand ----------------------------------------------------------- */
/* Scoped through .szs-handpanel, the class this game's own page puts on the
   panel: #hand-cards is on every page, so an unscoped rule here would resize
   and re-reserve every game's hand. */
/* Nothing to change at desktop widths: the shared two-row reservation is what
   this hand needs, and the cards top-align in it like every other game's. The
   phone block below is where this game's hand differs, because it runs to 16
   cards at three seats. */
/* Between rounds the whole panel closes, along with the draw deck. Both used to
   stay in the flow holding public pile counts, to keep the action bar from
   moving; the bar is above them in the page now, so they can simply go. */
/* Picked for the opening stash, or picked to be buried. Ringed and lifted, so
   "these five are leaving my hand" is visible without reading a counter. */
.card.szs-picked {
    outline: 3px solid #8fd0ff;
    outline-offset: 2px;
    transform: translateY(-8px);
}
/* Illegal on this turn: dimmed rather than removed, because knowing what you
   CANNOT play is half of knowing what the position is. */
.card.szs-mute { opacity: 0.4; filter: saturate(0.5); }
/* --- the scoring card in play ------------------------------------------ */
/* Read off the engine's own table, with this player's current count marked, so
   the reference and the arithmetic cannot drift apart. */
.szs-ref { margin-bottom: 12px; }
.szs-ref-title { margin: 0 0 4px; font-size: 14px; }
.szs-ref-what { font-size: 12px; opacity: 0.75; margin-bottom: 8px; }
.szs-ref-table {
    display: inline-grid;
    grid-template-columns: auto auto;
    gap: 1px 14px;
    font-size: 13px;
}
.szs-ref-row { display: contents; }
.szs-ref-row.szs-ref-head .szs-ref-count,
.szs-ref-row.szs-ref-head .szs-ref-points {
    font-size: 10px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
}
.szs-ref-count { white-space: nowrap; }
.szs-ref-points {
    text-align: right;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
/* Where this player stands on the card right now. */
.szs-ref-row.szs-ref-here .szs-ref-count,
.szs-ref-row.szs-ref-here .szs-ref-points {
    color: var(--accent);
    font-weight: 800;
}
.szs-ref-standing {
    font-size: 12px;
    opacity: 0.8;
    margin-top: 8px;
    /* Reserved: the sentence changes length as the pile grows. */
    min-height: 2.6em;
}
/* --- the round summary and the final result ---------------------------- */
.szs-summary .result-table.szs-result { margin-top: 8px; }
/* An EXPLICIT track per seat: the shared .result-row auto-fits 180px columns,
   which at four seats drops the last seat onto a line of its own. */
.szs-result.szs-cols-2 .result-row { grid-template-columns: 128px repeat(2, 1fr); }
.szs-result.szs-cols-3 .result-row { grid-template-columns: 128px repeat(3, 1fr); }
.szs-result.szs-cols-4 .result-row { grid-template-columns: 118px repeat(4, 1fr); }
.szs-result .result-cell { font-variant-numeric: tabular-nums; }
/* Came out level, which is what pays 5 a pair. */
.szs-result .result-cell.szs-won { color: #7ce8a4; font-weight: 800; }
.szs-result .result-row.szs-roundrow {
    border-top: 1px solid var(--panel-border);
    font-weight: 700;
}
/* --- the setup screen's six scoring cards ------------------------------- */
/* Six options is far more than the two or three `.segmented` is built for (it is
   an inline-flex with overflow hidden, so a sixth button is simply clipped on a
   phone). This group therefore drops the shared shell and becomes a wrapping
   row of chips. The id belongs to this page alone, so nothing else is touched;
   the game class on the .opt-group is what lets the label sit above the row. */
.opt-group.szs-optcard { align-items: flex-start; flex-wrap: wrap; }
.opt-group.szs-optcard .opt-label { padding-top: 7px; }
#opt-card {
    flex-wrap: wrap;
    gap: 4px;
    border: 0;
    overflow: visible;
}
#opt-card .seg {
    border: 1px solid var(--panel-border);
    border-radius: 8px;
}
#opt-card .seg + .seg { border-left: 1px solid var(--panel-border); }
/* --- the page's own reference tables ----------------------------------- */
.szs-scoretable { width: 100%; border-collapse: collapse; font-size: 13px; }
.szs-scoretable th,
.szs-scoretable td { padding: 3px 6px; text-align: left; vertical-align: top; }
.szs-scoretable + .szs-scoretable { margin-top: 12px; }
.szs-scoretable .szs-score-value {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    font-weight: 700;
}
.szs-scoretable .szs-score-note { opacity: 0.7; }
/* --- narrow screens ----------------------------------------------------- */
/* Every reservation above is re-derived here rather than left at its desktop
   figure, which is the same bug with different numbers. */
/* ON A PHONE THE WHOLE GAME HAS TO FIT WITHOUT SCROLLING, down to the bottom of
   the hand. That is one budget, not a set of independent tweaks, so it is written
   as one: at 390x844 the bands measured 79 + 59 + 625 + 312 + 256 + 46 + 145 +
   249 = 1771px of content and needed to come under about 700. Where it came from:

     the seat boards 625 -> ~180   two rows laid out ACROSS the width, not stacked
     the trick       312 -> ~110   one row of slots, not a cross
     this seat's own 256 -> ~60    the same strip treatment
     the draw deck   145 -> ~70    smaller cards, tighter panel
     the hand        249 -> ~130   smaller cards, two rows reserved
     status/progress 138 -> ~80    fewer reserved lines, and no score chips

   `tools/zs-fit.mjs` prints that budget band by band and says whether the hand
   clears the fold, so an overspend is attributable. Everything below the hand
   (the scoring-card reference, the rules) is reference material and may sit under
   the fold.

   Every reservation is ALSO re-derived here rather than left at its desktop
   figure, which is the same anti-jitter bug with different numbers, and
   `tools/zs-measure.mjs` is what proves it. */
@media (max-width: 900px) {
    /* --- the two lines at the top --------------------------------------- */
    /* Two lines, which is what the longest of them takes at this width now the
       draw-window line has been shortened (see statusText). */
    .status.szs-status { min-height: 2.7em; }
    /* The per-seat chips come off: every seat board carries its own match total,
       including this player's, so the scoreboard keeps only what is not on a
       board - which round this is, which trick, and the scoring card in play. */
    .szs-scoreboard .progress-score { display: none; }
    /* The scoring card and the variant come off too: both are reference, both are
       fixed for the whole game, and the reference panel below the hand names the
       card in full. What is left is one line. */
    .szs-scoreboard .szs-bonuschip { display: none; }
    .szs-scoreboard { min-height: 1.8em; }
    .szs-progress-trick { margin-left: 8px; }

    /* --- the seat boards: ONE COLUMN, each board ONE STRIP --------------- */
    /* Three boards beside each other at this width leave no room for a shorts
       pile, and stacked-two-deep they spent 625px. Full width and two lines
       apiece spends about 180 for the same information. The positional reading
       (who sits where) is dropped here; the trick row below keeps seat order. */
    .szs-table {
        display: flex;
        flex-direction: column;
        /* STRETCH, overriding the grid layout's `align-items: end`. In a column
           flex container that property is the HORIZONTAL one, so `end` shrank
           every board to its content and pushed it against the right edge. */
        align-items: stretch;
        gap: 5px;
        margin-bottom: 8px;
    }
    .szs-seat.pos-up { margin-bottom: 0; }
    .szs-seat, .szs-mine { padding: 3px 7px 4px; }
    .szs-mine { margin-bottom: 6px; }
    /* Both rows lay out ACROSS the width. nowrap is what makes the height
       predictable: the strips inside scroll instead (see .cardrow.scroll). */
    .szs-seat-top,
    .szs-seat-bot {
        display: flex;
        align-items: center;
        flex-wrap: nowrap;
        gap: 6px;
        min-width: 0;
    }
    .szs-seat-top { margin-bottom: 1px; }
    /* Tight leading: at four boards on screen every 4px of line box is 16px of
       page, and none of these lines wraps. */
    .szs-seat-top, .szs-seat-bot { line-height: 1.15; }
    .szs-seat-top .szs-chips { flex-basis: auto; min-height: 0; flex-wrap: nowrap; }
    .szs-seat-name { font-size: 12px; }
    .szs-seat-total { font-size: 15px; }
    .szs-pairnum { font-size: 13px; }
    .szs-pairlabel { font-size: 10px; }
    .szs-pairbox { padding: 1px 6px; gap: 3px; }
    .szs-chip { font-size: 11px; }
    /* The shorts COUNT is already in the pair box beside it, so the label over
       the cards would be saying it twice in a strip that has no room. */
    .szs-shorts-label, .szs-lasttrick-label { display: none; }
    .szs-shorts { margin-top: 0; flex: 1 1 auto; min-width: 0; }
    .szs-shorts-cards .card.mini { --cw: 16px; }
    /* `.cardrow.scroll` pads 6px/2px for the lift a selected card makes; nothing
       in these strips is selectable, so the padding comes back down and the
       reservation with it. */
    .szs-shorts-cards {
        min-height: calc(16px * 1.39 + 6px);
        gap: 2px;
        padding: 2px 0;
    }
    .szs-shorts-empty { font-size: 10px; }
    .szs-lasttrick {
        margin-top: 0;
        padding-top: 0;
        border-top: 0;
        border-left: 1px dashed transparent;
        padding-left: 5px;
        flex: 0 1 auto;
        min-width: 0;
        min-height: calc(16px * 1.39 + 4px);
    }
    .szs-lasttrick-filled { border-left-color: var(--panel-border); }
    .szs-lasttrick .card.mini { --cw: 16px; }
    .szs-suits { margin-top: 0; gap: 5px; font-size: 11px; min-height: 0; }
    .szs-suit-glyph { font-size: 12px; }
    .szs-piles { gap: 7px; margin-bottom: 0; }
    .szs-pile { gap: 3px; }
    .szs-pile-label { font-size: 9px; letter-spacing: 0; }
    .szs-pile-count { font-size: 12px; }
    .szs-stack { width: 22px; height: 26px; }
    .szs-stack .card { --cw: 17px; }

    /* --- the trick: ONE ROW, in seat order ------------------------------ */
    /* The cross spends three rows of card height on a screen that has none to
       spare. One row keeps what the cross was for - a fixed slot per seat, so a
       card lands under the player who played it and never moves again - and the
       columns stay in seat order, so it is still read positionally. */
    .szs-trick {
        grid-template-columns: repeat(4, minmax(0, 1fr));
        grid-template-areas: 'left up right down';
        gap: 4px;
        padding: 5px;
        margin-bottom: 6px;
        min-height: calc(42px * 1.39 + 32px);
    }
    .szs-trick.seats-3 {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        grid-template-areas: 'left right down';
        min-height: calc(42px * 1.39 + 32px);
    }
    .szs-trick.seats-2 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        grid-template-areas: 'up down';
        min-height: calc(42px * 1.39 + 32px);
    }
    .szs-play .card { --cw: 42px; }
    .szs-play { gap: 2px; }
    .szs-play-name { font-size: 10px; max-width: 100%; }
    .szs-play-flag { font-size: 9px; letter-spacing: 0.03em; }

    /* --- the draw deck and the hand ------------------------------------- */
    .szs-deck { padding: 5px 8px 6px; margin-bottom: 6px; }
    .szs-deck-head { margin-bottom: 0; gap: 6px; }
    .szs-deck-title { font-size: 12px; }
    .szs-deck-note { font-size: 10px; }
    .szs-deck-cards .card.mini { --cw: 21px; }
    .szs-deck-cards { min-height: calc(21px * 1.39 + 6px); padding: 2px 0; }
    .szs-actions { min-height: 42px; margin-bottom: 6px; }
    /* The hand runs to 16 cards at three seats under Short Shorts. At 40px plus
       the 4px gap, eight fit a row here, so TWO rows are reserved and a hand that
       empties cannot re-flow the panel under the player's thumb. */
    /* 36px with a 4px gap fits EIGHT to a row in the 352px tray, so the longest
       hand in the game (16 cards, three seats under Short Shorts) takes two rows
       rather than three. At 38px and the shared 8px gap only seven fit, and the
       third row cost 67px of a screen that had none.
       No min-height here: src/shared/ui/handspace.ts measures the tallest hand
       at this width and pins it at runtime, which is strictly better than a
       figure written down per breakpoint. A CSS reservation on top of it only
       sets a floor the measurement cannot go under. */
    .szs-handpanel #hand-cards .card { --cw: 36px; }
    .szs-handpanel #hand-cards { gap: 4px; }
    .szs-handpanel { padding: 5px 8px 6px; }
    .szs-handpanel .panel-head { margin-bottom: 2px; }
    .szs-handpanel #hand-title { font-size: 12px; }
    .szs-ref-standing { min-height: 3.9em; }
}
body.motion-off .card.szs-picked { transform: none; }

/* =========================================================================
   Schadenfreude (schadenfreude)

   The board is built around the three facts a player cannot read off the cards,
   because the second-highest rule and the pay-the-winner-in-cards rule between
   them make almost everything on screen counter-intuitive:

     1. WHICH CARD IS WINNING. The trick panel rings the second-highest follower
        and separately marks the highest as too good to win, so the rule is
        stated where it is being applied rather than only in the rules block.
     2. WHAT WINNING COSTS. The note under the trick prices it.
     3. EVERY SEAT'S PROVISIONAL SCORE, since the piles hold negative cards and
        cancel pairs, so nobody should be adding them up by hand.

   SCOPING. Every rule here is scoped through a class this game owns, never a
   bare shared id: `.sfd-handpanel` for the hand (the page puts it there via
   handPanel()), `.sfd-status` and `.sfd-scoreboard` for the two panels the SHELL
   owns and the page therefore cannot class. Eight rules naming a shared id
   without a game class shipped across this stylesheet before that convention
   existed, and each one silently resized or re-laid-out all ~85 games.
   ========================================================================= */

/*
 * THE BOARD IS A GRID, AND THE HAND PANEL IS ONE OF ITS ITEMS.
 *
 * That is the only way to get both of the things this board needs, because they
 * disagree about ordering: on a desktop the score piles want a column on the
 * right, and on a phone THE HAND MUST SIT DIRECTLY BELOW THE TRICK. Stacked
 * flex-wrap put the side column between the trick and the hand at the narrow
 * breakpoint, which pushed the hand off the bottom of the screen. Grid areas
 * place the three blocks independently of DOM order, so the side column simply
 * moves below the hand at the breakpoint.
 *
 * `minmax(0, 1fr)` RATHER THAN `1fr` IS LOAD-BEARING, and so is the `min-width: 0`
 * on every box between a track and a card scroller. A grid track (like a flex
 * item) sizes to min-content by default, so the nowrap rows in the piles and the
 * discard list forced the whole board - and with it the page - wider than the
 * viewport. Measured with tools/schadenfreude/sfd-layout-probe.mjs, which fails on any
 * horizontal overflow.
 */
.sfd-board {
    display: grid;
    gap: 12px;
    margin-bottom: 12px;
    align-items: start;
    grid-template-columns: minmax(0, 1fr) 280px;
    grid-template-areas:
        'main side'
        'hand side';
}
.sfd-board > .sfd-main { grid-area: main; min-width: 0; }
.sfd-board > .sfd-side {
    grid-area: side;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
/* Every box between the side track and a nowrap card row, so the row scrolls
   rather than widening its ancestors. A column flex container stretches its
   items, but stretch will not take an item below its min-content width, which is
   the full length of an unwrapped card row. */
.sfd-board > .sfd-side > * { min-width: 0; }
/* Scoped through `.sfd-board`: #hand-panel is an id every page carries, so an
   unscoped rule on it would make every game's hand a grid item. */
.sfd-board > .sfd-handpanel { grid-area: hand; min-width: 0; margin-bottom: 0; }
.sfd-panelhead {
    font-size: 12px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
    margin-bottom: 6px;
}

/* Two reserved lines on the status line and the scoreboard, so the board and the
   hand never step up and down the screen as the wording changes. Both panels
   belong to the shell, so they are reached through the class the renderer sets. */
.status.sfd-status { min-height: calc(2 * 1.45em); }
.progress.sfd-scoreboard { min-height: 34px; }
/* A seat that has already passed 40 is not leading, it is out, so it must never
   wear the accent the leader wears. */
.progress-score.sfd-over { border-color: rgba(255, 120, 120, 0.55); }
.progress-score.sfd-over .sfd-scorenum { color: var(--red); }
.sfd-progress-tricks,
.sfd-progress-target { font-size: 12px; opacity: 0.65; white-space: nowrap; }

/* ---------- the seat strip: banked, pile, provisional ---------- */

.sfd-seats {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.sfd-seat {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 132px;
    padding: 6px 10px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.sfd-seat-mine { border-color: rgba(255, 255, 255, 0.35); }
.sfd-seat-turn { box-shadow: 0 0 0 2px var(--accent); }
/* Room left under 40, as a temperature. `close` is a warning, `over` is a
   verdict, and `out` is the finished match's record of it. */
.sfd-seat-close { border-color: rgba(255, 209, 102, 0.55); }
.sfd-seat-over { border-color: rgba(255, 120, 120, 0.6); }
.sfd-seat-out { opacity: 0.55; }
.sfd-seathead { display: flex; align-items: baseline; gap: 6px; }
.sfd-seatname { font-weight: 600; white-space: nowrap; }
.sfd-seatturn {
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--accent);
}
.sfd-seattotal { display: flex; align-items: baseline; gap: 7px; }
.sfd-seatlive {
    font-size: 24px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1.05;
}
.sfd-seat-over .sfd-seatlive { color: var(--red); }
.sfd-seatroom { font-size: 12px; opacity: 0.7; white-space: nowrap; }
.sfd-seat-over .sfd-seatroom { color: var(--red); opacity: 0.95; }
/* The arithmetic behind the provisional total, so it can be checked rather than
   trusted. Deliberately small: it is a receipt, not a headline. */
.sfd-seatsplit { font-size: 11px; opacity: 0.55; white-space: nowrap; }

/* ---------- the trick ---------- */

.sfd-trickrow {
    display: flex;
    gap: 8px;
    align-items: flex-start;
    flex-wrap: wrap;
    /* One card plus both captions and the stagger, reserved, so filling the
       trick does not walk the panels below it down the screen. */
    min-height: calc(var(--cw) * 1.39 + 40px);
}
/* Ownership reads two ways: the caption (needed with up to four opponents) and
   the vertical stagger, the viewer's slot a step lower. */
.sfd-trickcell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.sfd-trickcell.sfd-mine { padding-top: 16px; }
.sfd-trickcap {
    font-size: 11px;
    opacity: 0.7;
    white-space: nowrap;
    max-width: calc(var(--cw) + 12px);
    overflow: hidden;
    text-overflow: ellipsis;
}
.sfd-trickcell.sfd-next .card.empty { border-color: var(--accent); }
/* THE CARD TAKING THE TRICK: the second highest of the lead suit. */
.sfd-trickcell .card.sfd-winning {
    box-shadow: 0 0 0 3px var(--accent), 0 4px 14px rgba(0, 0, 0, 0.45);
}
/* THE CARD THAT IS TOO GOOD TO WIN: the highest of the lead suit. A dashed ring
   rather than a solid one, because it marks a card that is NOT doing the thing a
   ring usually means. */
.sfd-trickcell .card.sfd-toohigh {
    outline: 2px dashed rgba(255, 255, 255, 0.65);
    outline-offset: 2px;
}
/* Did not follow suit, so it goes to whoever wins. Tinted, since these are the
   cards the winner is being paid in and cannot refuse. */
.sfd-trickcell .card.sfd-offsuit { filter: saturate(0.7) brightness(0.88); }
.sfd-tricknote {
    margin-top: 8px;
    font-size: 13px;
    line-height: 1.5;
    /* Two lines reserved: this note is the longest sentence on the board and it
       changes on every single card. */
    min-height: 2.9em;
}
.sfd-suitchip { font-weight: 700; white-space: nowrap; }

/* ---------- card labels in prose, and value badges on faces ---------- */

.sfd-cardlabel { white-space: nowrap; }
.sfd-chipvalue {
    margin-left: 3px;
    padding: 0 4px;
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.14);
    font-size: 11px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    /* The value is a fact about the card, not part of its name, so it does not
       inherit the suit color the name is carrying. */
    color: var(--ink);
}

/* THE VALUE BADGE. On every card whose printed rank is not what it is worth:
   the courts (negative), the Ace (a 1) and both Jokers. The positioning class
   is scoped to this game so the shared `.card` rule stays untouched. */
.card.sfd-hasbadge { position: relative; }
.sfd-value {
    position: absolute;
    top: -6px;
    left: 50%;
    transform: translateX(-50%);
    background: #ffd166;
    color: #4a3600;
    font-size: 11px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    padding: 2px 5px;
    border-radius: 8px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
    pointer-events: none;
    z-index: 2;
}
/* A card that SUBTRACTS is the opposite kind of thing from one that adds, and at
   40 it is the card you want, so it is colored rather than merely signed. */
.sfd-value.sfd-neg { background: #7fd1a3; color: #123123; }
/* The colored Joker at 10: the biggest single jump in the pack. */
.sfd-value.sfd-big { background: #ff9c6b; color: #40190a; }

/* ---------- the score piles (public by rule) ---------- */

.sfd-pile {
    padding: 5px 0;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.sfd-pile:first-of-type { border-top: none; }
.sfd-pile-mine .sfd-pilename { color: var(--accent); }
.sfd-pilehead { display: flex; align-items: baseline; gap: 8px; }
.sfd-pilename { font-size: 13px; font-weight: 600; white-space: nowrap; }
.sfd-pilevalue {
    font-size: 15px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    margin-left: auto;
}
.sfd-pilevalue.sfd-neg { color: #7fd1a3; }
/* A pile grows all round, so it scrolls sideways rather than wrapping: a
   wrapping row would walk the panel below it down the screen a step at a time.
   One row reserved. */
.sfd-pilerow {
    display: flex;
    gap: 3px;
    align-items: center;
    flex-wrap: nowrap;
    overflow-x: auto;
    padding: 3px 0;
    min-height: calc(30px * 1.39 + 6px);
}
.sfd-pilerow .card { --cw: 30px; flex: 0 0 auto; }
.sfd-pilenote,
.sfd-outnote { font-size: 11px; opacity: 0.6; line-height: 1.45; margin-top: 4px; }
.sfd-empty { font-size: 12px; opacity: 0.45; }

/* ---------- out of play (a collapsed disclosure) ---------- */

/*
 * Shut, this is one summary line tall, and that is the point: open, it is a card
 * row that grows all round, and on a phone it was tall and wide enough to push
 * the player's hand off the bottom of the screen.
 *
 * THE BURIED BACKS LIVE IN THE SUMMARY, not the body, so they are on screen
 * whether it is open or shut. They are a fact about the whole round (two cards
 * nobody will ever see) rather than a list to scroll, and the tutorial teaches
 * them in its first beat, which a phone player would otherwise be shown nothing
 * for.
 */
.sfd-outpanel { padding: 10px 14px; }
.sfd-outsummary {
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    list-style: none;
    min-width: 0;
}
.sfd-outsummary::-webkit-details-marker { display: none; }
/* The disclosure triangle, drawn rather than inherited, so it turns with the
   panel instead of depending on the browser's default marker. Drawn as a
   border triangle rather than the U+25B8 glyph, whose ink sits low in its own
   box (see `details.loggroup > summary::before` in
   src/shared/styles/platform.css): the flex row centers the BOX, so only a
   shape that fills its box ends up centered on the title. */
.sfd-outsummary::before {
    content: '';
    width: 0.38em;
    height: 0.44em;
    background: currentColor;
    clip-path: polygon(0 0, 100% 50%, 0 100%);
    opacity: 0.6;
    flex: 0 0 auto;
    transition: transform 0.15s ease;
}
.sfd-outpanel[open] > .sfd-outsummary::before { transform: rotate(90deg); }
.sfd-outtitle {
    font-size: 12px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
    white-space: nowrap;
}
/* The counts ride in the summary, because that is the part worth glancing at
   while it is shut. */
.sfd-outcount { font-size: 12px; opacity: 0.75; margin-left: auto; white-space: nowrap; }
.sfd-outbody { margin-top: 8px; min-width: 0; }

.sfd-buriedrow {
    display: inline-flex;
    gap: 3px;
    align-items: center;
    flex: 0 0 auto;
}
.sfd-buriedrow .card { --cw: 26px; flex: 0 0 auto; }
.sfd-discardrow {
    display: flex;
    gap: 3px;
    align-items: center;
    flex-wrap: nowrap;
    overflow-x: auto;
    min-height: calc(26px * 1.39 + 6px);
}
.sfd-discardrow .card { --cw: 26px; flex: 0 0 auto; }

/* ---------- the hand ---------- */

/* Faded means "you cannot play this right now", so the legal cards are the
   bright ones. Scoped to this game's hand panel. */
.sfd-handpanel #hand-cards .card.sfd-faded { opacity: 0.4; }

/* ---------- the round summary and the final result ---------- */

.sfd-result { --sfd-cols: 4; }
.sfd-result .result-row {
    display: grid;
    grid-template-columns: minmax(88px, 1.2fr) repeat(var(--sfd-cols), 1fr);
    gap: 6px;
    align-items: baseline;
}
.sfd-result .result-cell {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.sfd-result .result-cell.sfd-won { color: var(--accent); font-weight: 700; }
.sfd-result .result-cell.sfd-bust { color: var(--red); }
.sfd-result .result-row.totals {
    border-top: 1px solid rgba(255, 255, 255, 0.18);
    padding-top: 4px;
    font-weight: 700;
}

/* ---------- the value table on the rules pages ---------- */

.sfd-valuetable { border-collapse: collapse; margin: 6px 0; font-size: 13px; }
.sfd-valuetable th,
.sfd-valuetable td { padding: 3px 12px 3px 0; text-align: left; }
.sfd-valuetable thead th { font-size: 11px; text-transform: uppercase; opacity: 0.6; }
.sfd-value-cell { text-align: right !important; font-variant-numeric: tabular-nums; }

/*
 * ONE COLUMN, AND THE HAND COMES SECOND: the score piles and the out-of-play
 * disclosure drop below it, so the cards a player is reaching for sit directly
 * under the trick they are playing into.
 *
 * The threshold is 1000px rather than the 720px the card sizes use, because the
 * two-column board needs room for a 280px side column beside a playable trick,
 * and it does not have it on a tablet: at 768 the board measured 15px wider than
 * the screen. Stacking is also simply the better layout at that width.
 */
@media (max-width: 1000px) {
    .sfd-board {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas:
            'main'
            'hand'
            'side';
    }
}

@media (max-width: 720px) {
    /* Driven through the game's own board class, not a rule on `.card`, so the
       reserved heights above recompute with it. */
    .sfd-trickcell .card { --cw: 50px; }
    .sfd-trickrow { min-height: calc(50px * 1.39 + 40px); }
    .sfd-seat { min-width: 118px; }
    .sfd-seatlive { font-size: 21px; }
    .sfd-tricknote { min-height: 4.3em; }
}
/* =========================================================================
   Lucy (lucy)
   A bidding trick taker whose one idea is INVERTED VISIBILITY: you hold your
   ten cards facing outwards, so the table reads them and you do not. Almost
   every decision in this stylesheet follows from that.
     every other seat's ten face up, in its panel    (.lucy-seat, .lucy-hand)
     your own ten as backs showing only their SUIT   (.lucy-back)
     a played card leaves its PLACE behind, captioned with what it was, which
       is how you narrow down the cards either side of it   (.lucy-hole)
     tricks won against tricks bet, on every seat    (.lucy-bidbox)
   THE TABLE AS A TABLE, as in Hearts: one grid (.lucy-board), the other seats
   West, North and East of the trick and this player South, and the trick a
   cross whose slots sit on the same sides as the panels. Under it, the action
   wanted from this device (.lucy-actions).
   EVERY HAND IS SPLIT INTO SUIT GROUPS (.lucy-suitgroup) with a little space
   between them. A group never breaks across a line, so a hand that has to wrap
   wraps between suits, and inside a deal it never re-flows at all: a played card
   leaves its place behind, so the places a row is laid out from never change.
   ANTI-JITTER. Nothing above the hand may change height as a deal runs. The
   action box empties when the betting ends, the status line changes line count
   per phase, and the scoreboard's reference chips wrap, so each is reserved
   rather than allowed to size itself. The trick cross is reserved at its full
   shape, empty slots included.
   Every shared class (.card, .cardrow, .progress-score, .status, .logline, .btn,
   .seg, .result-table, .winner-banner, .result-note) is reused as is; rules that
   name a SHARED id are scoped through a class this game owns (.lucy-status on
   the status line, .lucy-handpanel on the hand panel), because those ids are on
   every page.
   ========================================================================= */
/* --- the scoreboard ------------------------------------------------------ */
/* HIGH score wins here, so `.progress-score.leading` marks the MAXIMUM. */
.lucy-scorename { font-size: 12px; opacity: 0.8; margin-right: 5px; }
.lucy-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }
/* Two rows reserved: at four seats the score chips plus the three reference
   chips wrap, and this line sits above the whole board. */
.lucy-scoreboard { min-height: 2.6em; align-items: center; }
/* Which suit is trump, what this deal pays, and how much of the ten the table
   has claimed. Reference rather than news, so quiet - but always there, because
   all three change how every decision in the deal is priced. */
.lucy-refchip {
    font-size: 12px;
    padding: 1px 8px;
    border-radius: 999px;
    border: 1px dashed var(--panel-border);
    opacity: 0.85;
    white-space: nowrap;
}
.lucy-refchip.lucy-trumpchip { border-style: solid; font-weight: 700; }
/* The trump suit's glyph in its suit color (suitTextClass), the same ink the
   suit has on every card back and hole on the board. */
.lucy-trumpglyph.red { color: var(--red); }
.lucy-trumpglyph.red.diamond { color: var(--red-diamond); }
.lucy-trumpglyph.club { color: var(--blue-club); }
/* More tricks bet than there are to take, so at least one bet is already lost.
   Worth calling out: it changes who is fighting for what. */
.lucy-refchip.lucy-over {
    border-color: rgba(255, 138, 128, 0.8);
    color: #ff8a80;
    opacity: 1;
}
/* --- the status line ----------------------------------------------------- */
/* Reserved height, so naming the suit to follow, or explaining that you can see
   only your suits, cannot make the board jump. */
.status.lucy-status { min-height: 3.2em; }
/* LIT whenever this device may act. A sentence on its own is not enough of a
   signal - the seat chip, the panel ring, the action box and this share that job. */
.status.lucy-status.lucy-turn {
    color: var(--accent);
    font-weight: 700;
}
.lucy-hintnote { opacity: 0.85; font-weight: 400; }
/* --- the board: the table as a table ------------------------------------- */
/* ONE GRID for the whole table. #lucy-table is `display: contents`, so the other
   seats' panels are cells of it and take the areas their seat sits in. North
   and South span the full width, because a ten-card row is wider than the cross
   between West and East; the side columns are flexible and the middle one is
   the cross's own width, so West and East hug the trick. */
.lucy-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas:
        "up   up    up"
        "left trick right"
        "down down  down";
    align-items: center;
    /* Row gap and top padding both clear the turn tab that rides each panel's
       top edge (.lucy-chips), so it never covers the line above it. */
    gap: 16px 14px;
    padding-top: 10px;
    margin-bottom: 10px;
}
/* Three seats have no North: the two neighbors sit either side of the trick. */
.lucy-board.seats-3 {
    grid-template-areas:
        "left trick right"
        "down down  down";
}
.lucy-table { display: contents; }
.lucy-seat.pos-left { grid-area: left; justify-self: end; }
.lucy-seat.pos-right { grid-area: right; justify-self: start; }
.lucy-seat.pos-up { grid-area: up; justify-self: center; }
.lucy-seat.lucy-south { grid-area: down; justify-self: center; }
.lucy-trick { grid-area: trick; }
/* --- a seat's panel ------------------------------------------------------ */
/* The same panel for every seat, this one included: a head line, then the
   hand. Sized to its hand and capped at its column, so it never widens the grid
   and a narrow column makes the hand wrap between suits instead. */
.lucy-seat {
    box-sizing: border-box;
    max-width: 100%;
    min-width: 0;
    padding: 6px 10px 8px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default, so the accent ring never shifts the layout. */
    outline: 2px solid transparent;
}
/* The seat being waited on, to bet or to play. */
.lucy-seat.lucy-active { outline-color: var(--accent); }
.lucy-seat-head {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 5px;
}
.lucy-seat-name { font-weight: 700; font-size: 14px; white-space: nowrap; }
/* The match total, the number that decides the game. */
.lucy-seat-total {
    font-size: 18px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
/* --- the bet, and how it is going --------------------------------------- */
/* THE readout the game is about, on every seat including this one: tricks won
   against tricks bet. Colored three ways AND labeled in words, so a color-blind
   player and a grayscale screenshot both read it. */
/* A FIXED HEIGHT, so a box reading "still to bet" (small text) and one reading
   "2 of 3" (16px numerals) are the same size and the head line does not grow when
   the bet is placed. Measured at 2px of movement for the whole board below it. */
.lucy-bidbox {
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    height: 22px;
    gap: 4px;
    padding: 0 9px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    white-space: nowrap;
}
.lucy-bidnum {
    font-weight: 800;
    font-size: 16px;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.lucy-bidof { font-size: 11px; opacity: 0.6; }
/* The safety, as the "+1" it means: this bet may take one more and still make it. */
.lucy-bidsafety {
    font-size: 11px;
    font-weight: 800;
    padding: 0 4px;
    border-radius: 6px;
    background: rgba(143, 208, 255, 0.25);
    color: #cfe8ff;
}
.lucy-bidlabel { font-size: 11px; opacity: 0.7; }
/* Still short of the bet, so this seat is fighting for tricks. */
.lucy-bidbox.lucy-bid-short { border-color: var(--panel-border); }
/* On the bet (or inside its safety): this seat now wants no more tricks at all,
   which is what makes the rest of the deal interesting. */
.lucy-bidbox.lucy-bid-made {
    border-color: rgba(60, 200, 120, 0.75);
    background: rgba(60, 160, 90, 0.22);
}
/* Past it. The bet is gone and every further trick costs another point. */
.lucy-bidbox.lucy-bid-over {
    border-color: rgba(255, 138, 128, 0.8);
    background: rgba(180, 70, 60, 0.22);
    color: #ffd7d2;
}
.lucy-bidbox.lucy-nobet { border-style: dashed; opacity: 0.75; }
/* --- the chip a seat carries -------------------------------------------- */
/* A TAB ON THE PANEL'S TOP EDGE, out of the flow. In the head line it either
   shuffled the line along each time the turn moved, or, held open with an
   invisible stand-in, left a gap before every seat's total. On the edge it takes
   no space from anything, and it sits in the panel's top padding and the gap
   above the panel, both of which are there anyway. */
.lucy-seat { position: relative; }
.lucy-chips {
    position: absolute;
    top: -11px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    font-size: 12px;
    pointer-events: none;
}
.lucy-chips:empty { display: none; }
.lucy-chip {
    padding: 1px 6px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.07);
    white-space: nowrap;
    opacity: 0.85;
}
/* Who is being waited on. Accented and never abbreviated: the whole board is
   read off this one chip. */
.lucy-chip.lucy-toact {
    background: var(--accent);
    color: var(--accent-ink);
    font-weight: 700;
    opacity: 1;
}
/* --- a hand on screen --------------------------------------------------- */
/* Ten places in SUIT GROUPS, centered in the panel. The gap between groups is
   wider than the gap inside one, which is what separates the suits; a group
   never breaks across a line, so the row wraps between suits or not at all.
   Another seat's cards are small and simplified (`--lucy-oppcw`). */
.lucy-hand {
    --lucy-oppcw: 28px;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-end;
    gap: 4px 8px;
}
.lucy-suitgroup {
    display: flex;
    flex-wrap: nowrap;
    gap: 2px;
}
.lucy-hand .card { --cw: var(--lucy-oppcw); flex: 0 0 auto; }
/* --- the viewer's own panel, South -------------------------------------- */
/* The shared hand panel, placed South of the trick and carrying the same head
   line as every other seat, so the bet and the cards it is on sit together and
   the hand is on screen without scrolling. */
.lucy-seat.lucy-south { padding: 6px 12px 10px; }
.lucy-handpanel #hand-cards {
    align-items: center;
    gap: 6px 14px;
}
.lucy-handpanel .lucy-suitgroup { gap: 4px; }
.lucy-handpanel #hand-cards .card { --cw: 56px; }
/* "Low" and "High" at the two ends of the row, in the direction of the stored
   Hand_Order: inside every suit the cards run from one end towards the other.
   These replaced a number printed on each card, which read as its rank. */
.lucy-end {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    opacity: 0.6;
    white-space: nowrap;
}
/* --- the hole a played card leaves behind -------------------------------- */
/* THE GAME'S MEMORY, and the whole reason the places are fixed. A hand is dealt
   sorted, so a hole captioned "9" says every card below it in that suit is lower
   and every card above it is higher. Faded, dashed and captioned - it is a
   record, not a card, and it must not read as one.
   NOT a .card built by cardEl: the card itself is painted wherever it is now
   (the trick, while it is held), and a second element carrying the same
   data-card-id would give the FLIP animator two places to fly it between. */
/* TWO CLASSES on purpose. These elements carry `empty` as well (see holeEl -
   it is what keeps the atlas sweep off a card with no sprite behind it), and
   `.card.empty` sets its own background and border at two classes; a 1-class
   rule here would lose to it. */
.card.lucy-hole {
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.04);
    border: 1px dashed rgba(255, 255, 255, 0.22);
    box-shadow: none;
    opacity: 0.55;
}
.lucy-holelabel {
    font-size: 10px;
    font-weight: 700;
    white-space: nowrap;
}
/* SUIT COLOR, per suit, on both of the surfaces that carry it: the hole a played
   card left behind and the suit on the back of a card still held. This is not
   decoration here. Suits decide two rules at once - what you must follow, and
   which suit is trump - and the suit is the ONLY thing a player knows about a
   card they are holding, so an uncolored glyph would be the one surface in the
   game that withholds information it is showing.
   The classes come from suitTextClass(); the values follow the .cardlabel block,
   which is the platform's precedent. Spades gets no class and keeps --ink. */
.lucy-holelabel.red,
.lucy-back.red .lucy-backsuit { color: var(--red); }
.lucy-holelabel.red.diamond,
.lucy-back.red.diamond .lucy-backsuit { color: var(--red-diamond); }
.lucy-holelabel.club,
.lucy-back.club .lucy-backsuit { color: var(--blue-club); }
/* On the viewer's own row the holes are working notes rather than history, so
   they are the one faded thing on the board that stays legible. */
.card.lucy-hole.lucy-hole-mine {
    opacity: 0.8;
    border-color: rgba(255, 255, 255, 0.3);
}
.lucy-hole-mine .lucy-holelabel { font-size: 14px; }
/* --- the viewer's own cards, face down ---------------------------------- */
/* A suit and nothing else, which is EVERYTHING their holder is allowed to know.
   Deliberately not the sprite atlas and deliberately carrying no card id: an id
   encodes the card, so a face-down card built through cardEl() would hand its
   value to anybody who opened the inspector. */
.card.lucy-back {
    display: flex;
    align-items: center;
    justify-content: center;
    background:
        linear-gradient(160deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.04));
    border: 1px solid var(--panel-border);
}
.lucy-backsuit { font-size: 26px; line-height: 1; }
/* The trump suit, on the back of a card its holder cannot read. A ring rather
   than a color of its own: hearts already read red, and what a player needs at a
   glance is "this one beats another suit", which is a property of the card and
   not of the ink. */
.card.lucy-back.lucy-trumpback {
    border-color: rgba(255, 156, 156, 0.55);
    box-shadow: inset 0 0 0 1px rgba(255, 156, 156, 0.28),
        0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
/* Illegal on this turn: dimmed rather than removed, because knowing what you
   CANNOT play is half of knowing what the position is. */
.card.lucy-mute { opacity: 0.4; filter: saturate(0.5); }
/* --- the trick: a cross ------------------------------------------------- */
/* Each seat's slot on the side that seat sits, so a card is read by where it is
   and no slot needs a caption. The same size empty or full, so the board does
   not step up and down as the trick fills. */
/* A DIAMOND rather than a plus: West and East span both rows and sit at the
   middle height, so the cross is two card heights tall instead of three. Measured
   at 249px as a plus, which made the trick the tallest thing on the board and
   pushed the viewer's own hand down the page. The rows are FIXED at a card's
   height, so the cross is the same shape empty or full, and at three seats (no
   North) the empty top row keeps the viewer's slot below the other two. */
.lucy-trick {
    --lucy-playcw: 54px;
    display: grid;
    grid-template-columns: auto auto auto;
    grid-template-rows: repeat(2, calc(var(--lucy-playcw) * 1.39));
    grid-template-areas:
        "left up   right"
        "left down right";
    align-items: center;
    justify-items: center;
    gap: 6px 10px;
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.lucy-trick.seats-3 {
    grid-template-areas:
        "left .    right"
        "left down right";
}
.lucy-play.pos-up { grid-area: up; }
.lucy-play.pos-left { grid-area: left; }
.lucy-play.pos-right { grid-area: right; }
.lucy-play.pos-down { grid-area: down; }
.lucy-play .card { --cw: var(--lucy-playcw); }
/* A slot still owed a card: outlined, not blank, so it reads as "waiting". */
.lucy-play .card.empty { border-style: dashed; opacity: 0.35; }
/* The seat to play, so the eye knows where the next card is coming from. */
.lucy-play.onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
/* The card taking the trick, or that took the finished trick still on the
   table: ringed, and glowing so it is not a matter of color alone. */
.lucy-play .card.lucy-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
/* --- contextual actions: the bet ---------------------------------------- */
/* The one box that says what this device is being asked for. Controls only, so
   the primary button never moves between phases - and RESERVED, because trick
   play arms the cards themselves and puts no button here at all, which would
   otherwise collapse the box under the board every trick. */
.lucy-actions {
    display: flex;
    gap: 10px;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    min-height: 52px;
    margin-bottom: 8px;
    padding: 0 10px;
    border-radius: 10px;
    border: 1px solid transparent;
}
.lucy-actions.lucy-armed {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.08);
}
.lucy-bidpicker { display: flex; align-items: center; gap: 8px; min-width: 0; }
.lucy-bidpicker-label {
    font-size: 11px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
    white-space: nowrap;
}
/* Eleven choices is far more than `.segmented` is built for (it is an inline-flex
   with overflow hidden, so the later buttons are simply clipped on a phone), so
   this group drops the shared shell and becomes a wrapping row of chips. */
.lucy-bidnums { display: flex; flex-wrap: wrap; gap: 4px; }
.lucy-bidnums .seg.lucy-bidnum-seg {
    min-width: 30px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    font-variant-numeric: tabular-nums;
}
/* The picked number. The shared highlight is `.segmented .seg.selected`, and this
   group is deliberately not `.segmented` (see above), so without this rule the
   number a player had picked looked exactly like the other ten. */
.lucy-bidnums .seg.lucy-bidnum-seg.selected {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-ink);
    font-weight: 700;
}
/* The safety is a decision of its own, not a twelfth number, so it is a separate
   control whose label is what pressing it does: "Add the safety", or "Remove
   the safety" while it is on, when it is also lit. */
.lucy-safety.selected {
    border-color: rgba(143, 208, 255, 0.9);
    background: rgba(143, 208, 255, 0.18);
}
/* --- the deal summary and the final result ------------------------------ */
.lucy-summary .result-table.lucy-result { margin-top: 8px; }
/* An EXPLICIT track per seat: the shared .result-row auto-fits 180px columns,
   which at four seats drops the last seat onto a line of its own. */
.lucy-result.lucy-cols-3 .result-row { grid-template-columns: 128px repeat(3, 1fr); }
.lucy-result.lucy-cols-4 .result-row { grid-template-columns: 118px repeat(4, 1fr); }
.lucy-result .result-cell { font-variant-numeric: tabular-nums; }
/* Made the bet, which is the only thing the deal pays for. */
.lucy-result .result-cell.lucy-won { color: #7ce8a4; font-weight: 800; }
.lucy-result .result-row.lucy-dealrow {
    border-top: 1px solid var(--panel-border);
    font-weight: 700;
}
/* --- the log ------------------------------------------------------------ */
/* What the table has claimed of the ten tricks, which is the moment the deal's
   shape becomes clear. */
.lucy-log-bids { font-style: italic; opacity: 0.9; }
.lucy-log-win { font-weight: 700; color: var(--accent); }
/* --- the page's own reference table ------------------------------------- */
.lucy-scoretable { width: 100%; border-collapse: collapse; font-size: 13px; }
.lucy-scoretable th,
.lucy-scoretable td { padding: 3px 6px; text-align: left; vertical-align: top; }
.lucy-scoretable .lucy-score-value {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    font-weight: 700;
}
.lucy-scoretable .lucy-score-note { opacity: 0.7; }
/* --- phones ----------------------------------------------------------- */
/* Every reservation above is re-derived here rather than left at its desktop
   figure, which is the same bug with different numbers.
   The board restacks the way Fives does: the three other seats in a row ACROSS
   THE TOP, still West, North and East from left to right, the cross under them,
   and this player under that. Their hands wrap between suits at a smaller card,
   and with three seats the two neighbors take half the width each. */
@media (max-width: 720px) {
    .lucy-board {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        grid-template-areas:
            "left  up    right"
            "trick trick trick"
            "down  down  down";
        gap: 8px 6px;
        align-items: start;
    }
    .lucy-board.seats-3 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        grid-template-areas:
            "left  right"
            "trick trick"
            "down  down";
    }
    .lucy-seat.pos-left,
    .lucy-seat.pos-up,
    .lucy-seat.pos-right { justify-self: stretch; padding: 5px 5px 6px; }
    .lucy-seat.lucy-south { justify-self: stretch; padding: 6px 8px 8px; }
    .lucy-hand { --lucy-oppcw: 19px; gap: 3px 5px; }
    .lucy-suitgroup { gap: 1px; }
    /* A third of a phone is too narrow for the name, the bet and the total on one
       line, and letting them wrap freely made each panel's head a different
       shape that changed as bets went in. So the other seats' heads are always
       exactly two lines: the name, then the bet and the total. The bet box drops
       its trailing word ("tricks", "on the bet"): color and the numbers carry it,
       and the words are in its tooltip. */
    .lucy-seat:not(.lucy-south) .lucy-seat-head {
        flex-wrap: wrap;
        row-gap: 3px;
        min-height: 42px;
        align-content: flex-start;
    }
    .lucy-seat:not(.lucy-south) .lucy-seat-name { flex-basis: 100%; text-align: center; }
    .lucy-seat-head { gap: 4px 6px; }
    .lucy-seat-name { font-size: 12px; }
    .lucy-seat-total { font-size: 15px; }
    .lucy-bidbox { padding: 0 6px; gap: 3px; }
    .lucy-bidtail { display: none; }
    .lucy-bidnum { font-size: 14px; }
    .lucy-trick { justify-self: center; --lucy-playcw: 42px; padding: 8px 10px; }
    .lucy-handpanel #hand-cards { gap: 5px 10px; }
    .lucy-handpanel .lucy-suitgroup { gap: 3px; }
    .lucy-handpanel #hand-cards .card { --cw: 40px; }
    .lucy-backsuit { font-size: 20px; }
    .status.lucy-status { min-height: 4.6em; }
    /* Measured at 390px with tools/lucy-measure.mjs: the score chips plus the
       three reference chips take three rows here, 75px at the tallest. In px
       rather than em because the chips inside are a different size from the line
       itself, which is what left the em figure a row short. */
    .lucy-scoreboard { min-height: 76px; }
    .lucy-actions { min-height: 92px; }
}
/* =========================================================================
   Fives (fives)
   A trump trick taker whose one idea is that WINNING IS NOT THE POINT: you
   score the rank of the card each trick was won with, and you want the total
   to reach 25 without passing it. Almost every decision below follows from
   that.
     the table as a table: West, North and East around the trick, this player
       South, and the trick a cross with each card on its seat's side
       (.fiv-board, .fiv-trick, .pos-*)
     a small panel per seat carrying only what cannot be seen at a glance:
       name, match total, the sum toward 25, the pot marker, the won pile
       (.fiv-seat, .fiv-sumbox, .fiv-closest, .fiv-won)
     a finished trick held in the cross until the next lead, winner ringed
       (.fiv-winning)
     a card played FACE DOWN as a back badged with the 5 it counts as
       (.fiv-down, .fiv-downtag)
     the black Joker badged as the 0 of its suit   (.fiv-zerotag)
   ANTI-JITTER. Nothing above the hand may change height as a deal runs, and
   three things want to: the won pile grows a card every trick won, the pot
   marker comes and goes, and the action box swaps its control between phases.
   Each is reserved rather than allowed to size itself: the pile is two rows
   whatever it holds, the marker's line is always rendered, and the action box
   is one button high whether or not it has one. The cross is the same size
   empty or full, and the side panels sit beside it rather than above it, so
   they cannot push anything down.
   Every shared class (.card, .panel, .panel-head, .progress-score, .status,
   .logline, .btn, .seg, .result-table, .winner-banner, .result-note) is reused
   as is; rules that name a SHARED id are scoped through a class this game owns
   (.fiv-status on the status line, .fiv-handpanel on the hand panel), because
   those ids are on every page.
   ========================================================================= */
/* --- the scoreboard ----------------------------------------------------- */
/* HIGH score wins here, so `.progress-score.leading` marks the MAXIMUM. */
.fiv-scorename { font-size: 12px; opacity: 0.8; margin-right: 5px; }
.fiv-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }
/* One row reserved: four score chips, the trump chip, and a pot chip that comes
   and goes. The tablet and phone blocks below reserve what those wrap to. */
.progress.fiv-scoreboard { min-height: 2.6em; align-items: center; }
/* Which suit is trump, and what is in the pot. Reference rather than news, so
   quiet, but always there while it matters. */
.fiv-refchip {
    font-size: 12px;
    padding: 1px 8px;
    border-radius: 999px;
    border: 1px dashed var(--panel-border);
    opacity: 0.85;
    white-space: nowrap;
}
.fiv-refchip.fiv-trumpchip { border-style: solid; font-weight: 700; }
/* Points waiting for whoever finishes closest to 25. */
.fiv-refchip.fiv-pot {
    border-style: solid;
    border-color: var(--accent);
    opacity: 1;
    font-weight: 700;
}
/* --- suit glyphs in prose ---------------------------------------------- */
/* Colored everywhere they appear, because suits decide everything in this game
   and an uncolored glyph invites the player to read no distinction. `#log-entries
   .red` and its companions already cover the log; these cover the rest. */
.fiv-suited .red { color: var(--red); }
.fiv-suited .red.diamond { color: var(--red-diamond); }
.fiv-suited .club { color: var(--blue-club); }
/* --- the status line ---------------------------------------------------- */
/* Two lines reserved: the action wanted, and a hint when one is asked for. */
.status.fiv-status { min-height: 2.6em; }
/* LIT whenever this device may act. */
.status.fiv-status.fiv-turn { color: var(--accent); font-weight: 700; }
.fiv-hintnote { opacity: 0.85; font-weight: 400; }
/* --- the board: the table as a table ------------------------------------ */
/* ONE GRID for the whole table. #fiv-table is `display: contents`, so the other
   seats' panels are cells of it and take the areas their seat sits in; the cross
   in the middle puts each seat's card on that seat's side. The side columns are
   flexible and the middle one is the cross's own width, so the panels hug the
   trick rather than the page edges. */
.fiv-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas:
        ".    up    ."
        "left trick right"
        ".    down  .";
    align-items: center;
    gap: 10px 16px;
    margin-bottom: 10px;
}
/* Three seats have no North: the two neighbors sit either side of the trick. */
.fiv-board.seats-3 {
    grid-template-areas:
        "left trick right"
        ".    down  .";
}
.fiv-table { display: contents; }
.fiv-seat.pos-left { grid-area: left; justify-self: end; }
.fiv-seat.pos-right { grid-area: right; justify-self: start; }
.fiv-seat.pos-up { grid-area: up; justify-self: center; }
.fiv-seat.pos-down { grid-area: down; justify-self: center; }
.fiv-trick { grid-area: trick; }
/* --- a seat's panel ------------------------------------------------------ */
/* SMALL, and the same for every seat including this one. A FIXED width, so a
   long name or a growing pile cannot resize the grid, and capped at the column
   so it still fits where the board is narrowest. */
.fiv-seat {
    box-sizing: border-box;
    width: 176px;
    max-width: 100%;
    min-width: 0;
    padding: 6px 9px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default, so the accent ring never shifts the layout. */
    outline: 2px solid transparent;
}
/* The seat being waited on. */
.fiv-seat.fiv-active { outline-color: var(--accent); }
.fiv-seat-head {
    display: flex;
    align-items: baseline;
    gap: 5px;
    margin-bottom: 4px;
}
/* Whose turn it is, as a SHAPE as well as the ring, so it does not rest on color. */
.fiv-toact { color: var(--accent); font-size: 11px; }
.fiv-seat-name {
    flex: 1 1 auto;
    min-width: 0;
    font-weight: 700;
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* The match total, the number the game is won on. */
.fiv-seat-total {
    font-size: 17px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
/* --- the sum against 25 ------------------------------------------------- */
/* THE readout the game is about: the number and the target, nothing else. What
   it means is its color, plus a shape for the two that matter most (struck
   through when over, filled when on 25 exactly), and words in the tooltip. */
.fiv-sumbox {
    display: inline-flex;
    align-items: baseline;
    gap: 1px;
    padding: 1px 8px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    white-space: nowrap;
}
.fiv-sumnum {
    font-weight: 800;
    font-size: 16px;
    font-variant-numeric: tabular-nums;
    line-height: 1.2;
}
.fiv-sumof { font-size: 11px; opacity: 0.65; }
/* Plenty of room left: nothing to say. */
.fiv-sumbox.fiv-room { border-color: var(--panel-border); }
/* Inside a trick or two of the target: the interesting part of the deal. */
.fiv-sumbox.fiv-close {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}
/* ON 25 EXACTLY, which pays an extra point and is what everybody is aiming at. */
.fiv-sumbox.fiv-exact {
    border-color: #7fd18a;
    color: #7fd18a;
    background: rgba(127, 209, 138, 0.12);
    font-weight: 700;
}
/* Less room than the rest of the deal is likely to bring in. */
.fiv-sumbox.fiv-full { border-color: #ffd479; color: #ffd479; }
/* Past 25, so paying a point into the pot and placing nowhere. */
.fiv-sumbox.fiv-over {
    border-color: rgba(255, 138, 128, 0.9);
    color: #ff8a80;
    text-decoration: line-through;
}
/* The sum and the pot marker, on a line that is always rendered at its height so
   the marker can come and go without the panel changing size. */
.fiv-seat-line {
    display: flex;
    align-items: center;
    gap: 6px;
    min-height: 24px;
    white-space: nowrap;
}
.fiv-chip {
    font-size: 11px;
    padding: 1px 7px;
    border-radius: 999px;
    white-space: nowrap;
}
/* The pot marker: closest to 25 with nobody tied, so the pot is theirs as it
   stands. */
.fiv-chip.fiv-closest {
    border: 1px solid var(--accent);
    color: var(--accent);
    font-weight: 700;
}
/* --- the pile that scores ----------------------------------------------- */
/* The cards this seat won tricks WITH, small and simplified, and nothing else.
   It WRAPS inside TWO RESERVED ROWS rather than scrolling: at this size two rows
   hold ten or more, far past what a seat heading for 25 can afford to win, so the
   reservation is never outgrown in practice and there is no scrollbar to show. */
.fiv-won {
    --fiv-woncw: 22px;
    display: flex;
    flex-wrap: wrap;
    align-content: flex-start;
    gap: 3px;
    margin-top: 5px;
    min-height: calc(2 * var(--fiv-woncw) * 1.39 + 3px);
}
.fiv-won .card { --cw: var(--fiv-woncw); }
/* --- the trick: a cross ------------------------------------------------- */
/* Each seat's slot on the side that seat sits, so a card is read by where it is
   and no slot needs a caption. Same size empty or full. */
.fiv-trick {
    --fiv-playcw: 56px;
    display: grid;
    grid-template-columns: auto auto auto;
    grid-template-areas:
        ".    up    ."
        "left .     right"
        ".    down  .";
    align-items: center;
    justify-items: center;
    gap: 2px 8px;
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.fiv-trick.seats-3 {
    grid-template-areas:
        "left .    right"
        ".    down .";
}
.fiv-play.pos-up { grid-area: up; }
.fiv-play.pos-left { grid-area: left; }
.fiv-play.pos-right { grid-area: right; }
.fiv-play.pos-down { grid-area: down; }
.fiv-play .card { --cw: var(--fiv-playcw); }
/* A slot still owed a card: outlined, not blank, so it reads as "waiting". */
.fiv-play .card.empty { border-style: dashed; opacity: 0.35; }
/* The seat to play, so the eye knows where the next card is coming from. */
.fiv-play.onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
/* The card taking the trick, or that took the finished trick still on the
   table: ringed, and glowing so it is not a matter of color alone. */
.fiv-play .card.fiv-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
/* --- a card played face down -------------------------------------------- */
/* The back, badged with the 5 of the five suit it counts as, which is what the
   printed cards have on them. */
.fiv-down { position: relative; }
.fiv-downtag,
.fiv-zerotag {
    position: absolute;
    top: 2px;
    left: 2px;
    font-size: 10px;
    font-weight: 800;
    line-height: 1;
    padding: 1px 3px;
    border-radius: 4px;
    background: #f7f3e8;
    border: 1px solid rgba(0, 0, 0, 0.35);
    /* A DARK ink, not the site's suit palette: the badge sits on a cream chip,
       and `--blue-club` is a light color picked to read against the dark table.
       Near-black because the five suit is a black one. */
    color: #17212b;
    white-space: nowrap;
}
/* The black Joker standing in as the 0 of its suit. Bottom right, so it does
   not sit on top of the badge a face-down card wears. */
.fiv-zerotag { top: auto; bottom: 2px; left: auto; right: 2px; }
/* On the won pile's small cards the badge would cover the whole face, so it
   shrinks with them. */
.fiv-won .fiv-downtag { font-size: 8px; padding: 0 2px; top: 1px; left: 1px; }
/* --- the action box ---------------------------------------------------- */
/* One button high whether or not it has a button in it, so the hand below it
   never moves between phases. */
.fiv-actions {
    display: block;
    min-height: 38px;
    margin-bottom: 10px;
}
.fiv-actionrow {
    display: flex;
    justify-content: center;
    gap: 8px;
    min-height: 38px;
}
/* --- the hand --------------------------------------------------------- */
/* A card that cannot be played right now: dimmed rather than removed, because
   knowing what you cannot play is half of knowing what the position is. */
.fiv-mute { opacity: 0.4; }
/* Armed in the turn-over mode, where tapping plays the card as the 5 rather
   than as itself. Marked differently from an ordinary armed card, because the
   two do very different things to the same tap. */
.fiv-turnable { box-shadow: 0 0 0 2px #ffd479; border-radius: 6px; }
/* --- the summary ------------------------------------------------------ */
/* COMPACT: a short label column and one narrow track per seat, at a smaller
   size than the shared table, centered as a block. The rows are only the ones
   that carry a number for somebody (see scoreTable), so this is usually five. */
.fiv-result {
    max-width: 440px;
    margin: 6px auto 0;
    gap: 2px;
    font-size: 13px;
}
.fiv-result .result-row { gap: 6px; }
.fiv-result .result-label { font-size: 12px; }
.fiv-result.fiv-cols-3 .result-row { grid-template-columns: 56px repeat(3, minmax(0, 1fr)); }
.fiv-result.fiv-cols-4 .result-row { grid-template-columns: 56px repeat(4, minmax(0, 1fr)); }
.fiv-result .result-cell {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-variant-numeric: tabular-nums;
}
.fiv-result .result-row.header .result-cell { font-size: 12px; }
/* The seat closest to 25 with nobody tied: the one that took the pot. */
.fiv-result .result-cell.fiv-top { color: var(--accent); }
/* The line the rows above it add up to, and the running total under it. */
.fiv-result .result-row.fiv-dealrow { font-weight: 700; }
.fiv-result .result-row.total .result-cell { font-size: 17px; font-weight: 800; }
.fiv-log-win { color: var(--accent); font-weight: 700; }
/* --- the scoring table in the Cheat_Sheet ------------------------------ */
/* Keyed on the table's own class, not the rules wrapper, so it styles the
   Rules_Summary and the Rules_Page alike. */
.fiv-scoretable { width: 100%; border-collapse: collapse; margin: 6px 0 10px; }
.fiv-scoretable th,
.fiv-scoretable td {
    text-align: left;
    padding: 3px 8px 3px 0;
    border-bottom: 1px solid var(--panel-border);
    font-size: 13px;
}
.fiv-scoretable .fiv-score-value {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.fiv-scoretable .fiv-score-note { opacity: 0.7; font-size: 12px; }
/* --- tablets ---------------------------------------------------------- */
/* The board column narrows while the 300px log column stays beside it, so the
   scoreboard's chips can wrap to a second row as the pot chip comes and goes. */
@media (max-width: 1000px) {
    .progress.fiv-scoreboard { min-height: 66px; align-items: flex-start; }
}
/* --- phones ----------------------------------------------------------- */
/* Every reservation above is re-derived here rather than left at its desktop
   figure, which is the same bug with different numbers.
   The board restacks: the three other seats in a row ACROSS THE TOP, still West,
   North and East from left to right, the cross under them, this player under
   that. Two 176px panels beside a cross do not fit a phone. */
@media (max-width: 720px) {
    .fiv-board {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        grid-template-areas:
            "left  up    right"
            "trick trick trick"
            "down  down  down";
        gap: 8px 6px;
        align-items: start;
    }
    .fiv-board.seats-3 {
        grid-template-areas:
            "left  .     right"
            "trick trick trick"
            "down  down  down";
    }
    .fiv-seat.pos-left,
    .fiv-seat.pos-up,
    .fiv-seat.pos-right { width: auto; justify-self: stretch; padding: 5px 6px 6px; }
    .fiv-trick { justify-self: center; --fiv-playcw: 44px; padding: 8px 10px; }
    .fiv-won { --fiv-woncw: 19px; }
    /* A third of a phone is too narrow for the sum AND the pot marker on one
       line, so the line wraps, and it is reserved at the two lines it wraps to. */
    .fiv-seat:not(.pos-down) .fiv-seat-line {
        flex-wrap: wrap;
        row-gap: 3px;
        min-height: 48px;
        align-content: flex-start;
    }
    .fiv-seat-total { font-size: 15px; }
    .fiv-sumnum { font-size: 15px; }
    .progress.fiv-scoreboard { min-height: 106px; align-items: flex-start; }
}
/* =========================================================================
   American Bookshop (bookshop)

   SUITS, NOT COLORS. The printed game is four colors of books, and the class
   names here used to say so. Everything a player reads says suit now, because a
   player is looking at clubs, diamonds, spades and hearts.

   THE BOARD IS READ POSITIONALLY. The trick is a cross in the middle of the
   table and every other player sits where they sit, west, north and east, so a
   card in the trick is under the player who played it and that player's own
   panel is on the same side of the screen. Before this it was a flat strip of
   seat chips beside a separate stack of winnings piles, which listed every
   seat's name twice and made whose-card-is-whose a caption-reading exercise.

   The three numbers a table says out loud, and this game gives a player no way
   to work out from the cards alone:

     1. THE RUNNING SUM, with the limit and the ROOM under it, directly above the
        cross. Figures only: it used to carry a paragraph that said what the
        status line a few lines above it was already saying.
     2. THE SUIT RACE. Scoring is a majority per suit and a TIE PAYS NOBODY, so a
        pile of six clubs is worth +6 or -6 depending on a number sitting in
        somebody else's winnings. Every seat's count is on screen, the outright
        leader accented, a tie called out, and how many are STILL OUT beside it.
     3. EVERY SEAT'S WINNINGS GROUPED BY SUIT with the count over each group,
        which is what the rules ask players to do with the cards they take. It
        lives in that seat's own panel, not in a second list.

   SCOPING. Every rule here is scoped through a class this game owns, never a
   bare shared id: `.bks-handpanel` for the hand (the page puts it there via
   handPanel()), `.bks-status` and `.bks-scoreboard` for the two panels the
   SHELL owns and the page therefore cannot class.
   ========================================================================= */

.bks-board {
    display: flex;
    gap: 12px;
    align-items: flex-start;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.bks-board .bks-main { flex: 1 1 520px; min-width: 320px; }
.bks-board .bks-side {
    flex: 0 1 300px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.bks-panelhead {
    font-size: 12px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
    margin-bottom: 6px;
}
.bks-dim { opacity: 0.5; }
.bks-good { color: #7fd1a3; }
.bks-bad { color: var(--red); }

/* ---------- suit glyphs, colored ---------- */

/* `suitTextClass` hands back `red`, `red diamond` or `club`, and NONE of those
   color anything on their own: every `.red` rule in this stylesheet is scoped to
   the surface that opted in (`#log-entries .red`, `.cardlabel.red`, ...). So a
   glyph painted with the class and no rule behind it came out the inherited ink,
   which is the bug this block fixes. Suits decide the whole of the scoring here,
   so they are colored everywhere the game names one. */
.bks-suit { font-weight: 700; }
.bks-suit.red,
.bks-label.red { color: var(--red); }
.bks-suit.red.diamond,
.bks-label.red.diamond { color: var(--red-diamond); }
.bks-suit.club,
.bks-label.club { color: var(--blue-club); }
/* A suit named in prose keeps the glyph colored and the WORD in the inherited
   ink: a colored word reads as emphasis rather than as a suit. */
.bks-suitrun { white-space: nowrap; }

/* ---------- the two shell-owned lines ---------- */

/* Reserved heights, so the board and the hand never step up and down the screen
   as the wording changes. Both are reached through the class the renderer sets,
   since the page cannot class them.
   Two lines: the longest wording runs to two, and so does any wording with a
   hint appended to it. The padding is unconditional, so the accent bar below can
   appear without shifting the text sideways. */
.status.bks-status { min-height: calc(2 * 1.45em); padding-left: 11px; }
.progress.bks-scoreboard { min-height: 34px; }
/* Lit whenever this device may act. A LEFT BAR rather than a box outline: the
   line reserves two lines of height and most of its wordings are one, so an
   outline drew an accented box around empty space and read as a broken input
   field. The bar marks the text without claiming the reservation. */
.bks-status.bks-turn {
    color: var(--ink);
    box-shadow: inset 3px 0 0 var(--accent);
}
.bks-hintnote { opacity: 0.85; }
.bks-progress-tricks { font-size: 12px; opacity: 0.65; white-space: nowrap; }
.bks-scorename { opacity: 0.75; margin-right: 5px; }
.bks-scorenum { font-variant-numeric: tabular-nums; font-weight: 700; }

/* ---------- the table: seats above their own cards ---------- */

/* ONE GRID FOR THE WHOLE TABLE, and one column per side, so a card in the trick is
   in the SAME COLUMN as the panel of the player who played it. That is the point of
   the layout and it took three tries to get right: a cross in a narrow middle column
   put every card within 200px of the center while the panels sat 400px apart, so the
   positions were technically correct and told the eye nothing.

   Row 1 is the opponents' panels, west to east. Row 2 onward is the trick, spanning
   every column: the opponents' cards under their own panels, then the viewer's card
   below. Five seats add a fourth column, because four opponents sit above the trick
   and turn order has to keep reading left to right. */
.bks-table {
    --bks-trickcw: 62px;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px 10px;
    align-items: start;
    margin-bottom: 10px;
}
.bks-table.bks-seats5 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* Every panel can be narrower than its winnings row, which scrolls. */
.bks-table .bks-seat { min-width: 0; grid-row: 1; }
.bks-seat.pos-left { grid-column: 1; }
.bks-seat.pos-up { grid-column: 2; }
.bks-seat.pos-right { grid-column: 3; }
.bks-seats5 .bks-seat.pos-upleft { grid-column: 2; }
.bks-seats5 .bks-seat.pos-upright { grid-column: 3; }
.bks-seats5 .bks-seat.pos-right { grid-column: 4; }

/* ---------- the trick ---------- */

/* The trick is a grid of the SAME columns as the table it spans, which is what
   lines a card up under its player. Repeated rather than inherited, so the two
   cannot drift. */
.bks-trick {
    grid-column: 1 / -1;
    grid-row: 2;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    row-gap: 2px;
    column-gap: 10px;
    justify-items: center;
}
.bks-trick.bks-wide { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.bks-trickhead {
    grid-column: 1 / -1;
    grid-row: 1;
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 12px;
    flex-wrap: nowrap;
    font-size: 12px;
    line-height: 1.45;
    /* ONE line. The sentence explaining WHICH of the two ways the trick was won
       used to live here, wrapped to a third line in a narrow column, and grew the
       whole trick by 17px every time a trick settled - which walked the hand down
       the screen. It is on the status line now, which is full width and already
       reserves two lines. */
    min-height: 1.6em;
}
.bks-tricknum {
    opacity: 0.75;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.bks-trickcell { grid-row: 2; }
.bks-trickcell.pos-left { grid-column: 1; }
.bks-trickcell.pos-up { grid-column: 2; }
.bks-trickcell.pos-right { grid-column: 3; }
.bks-trickcell.pos-down { grid-column: 2; grid-row: 3; }
.bks-wide .bks-trickcell.pos-upleft { grid-column: 2; }
.bks-wide .bks-trickcell.pos-upright { grid-column: 3; }
.bks-wide .bks-trickcell.pos-right { grid-column: 4; }
/* Four columns and one viewer, so the viewer's card centers across the middle
   two rather than sitting under one opponent. */
.bks-wide .bks-trickcell.pos-down { grid-column: 2 / 4; grid-row: 3; }
.bks-trickcell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.bks-trickcell .card { --cw: var(--bks-trickcw); }
.bks-trickcap {
    font-size: 11px;
    opacity: 0.7;
    /* A long name wrapping to a second line would make the cell taller than what
       the row reserved for it. */
    white-space: nowrap;
    max-width: calc(var(--bks-trickcw) + 24px);
    overflow: hidden;
    text-overflow: ellipsis;
}
.bks-tricksum {
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.bks-trickcell.bks-tookit .bks-tricksum { color: var(--accent); }
.bks-trickcell .card.empty.bks-next { border-color: var(--accent); }
/* Cut off by the limit before it ever reached this seat: a dashed empty slot,
   because "played nothing" and "has not played yet" have to look different, and
   this game produces the first constantly. */
.bks-trickcell .card.empty.bks-cutoff {
    border-style: dashed;
    opacity: 0.45;
}
/* THE CARD TAKING THE TRICK: the highest of the led suit, or whoever reached the
   limit. */
.bks-trickcell .card.bks-winning {
    box-shadow: 0 0 0 3px var(--accent), 0 4px 14px rgba(0, 0, 0, 0.45);
}

/* ---------- the running sum, in the trick's heading ---------- */

.bks-sum { display: flex; align-items: baseline; gap: 6px; white-space: nowrap; }
.bks-sumfigure { display: flex; align-items: baseline; gap: 5px; }
.bks-sumnow {
    font-size: 20px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
/* At or past the limit the sum is not a running total any more, it is a verdict. */
.bks-sumnow.bks-sumover { color: var(--accent); }
.bks-sumof { font-size: 12px; opacity: 0.65; }
.bks-sumtag,
.bks-sumidle { font-size: 12px; opacity: 0.7; letter-spacing: 0.03em; }

/* ---------- the suit race ---------- */

.bks-suittable { --bks-cols: 4; font-size: 13px; }
.bks-suitrow {
    display: grid;
    grid-template-columns: 28px repeat(var(--bks-cols), 1fr) 30px;
    gap: 4px;
    align-items: baseline;
    padding: 2px 0;
}
.bks-suitrow.bks-suithead {
    font-size: 11px;
    text-transform: uppercase;
    opacity: 0.55;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    padding-bottom: 3px;
    margin-bottom: 2px;
}
.bks-suitname { font-weight: 700; }
.bks-suitcell,
.bks-suitleft {
    text-align: right;
    font-variant-numeric: tabular-nums;
    /* Every cell in this table is a name or a count; a wrap would make the row
       taller than its neighbors and break the column read. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.bks-suitcell.bks-suitmine { color: var(--accent); }
/* Holds the suit outright, so those cards pay +1 each. */
.bks-suitcell.bks-suitlead {
    font-weight: 800;
    color: #7fd1a3;
}
/* Tied for the most, which pays NOBODY - the one state where a big pile is worth
   nothing, so it is marked rather than left to be inferred. */
.bks-suitcell.bks-suittied {
    color: var(--red);
    text-decoration: underline dotted;
}
.bks-suitleft { opacity: 0.55; }
.bks-suitnote { font-size: 11px; opacity: 0.6; line-height: 1.45; margin-top: 6px; }

/* ---------- a seat's panel ---------- */

.bks-seat {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 6px 8px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* The tallest ordinary shape: head, totals, receipt and one row of winnings.
       Reserved so a seat taking its first trick does not grow and shove the
       cross sideways. */
    min-height: 118px;
}
.bks-seat-mine { border-color: rgba(255, 255, 255, 0.32); }
.bks-seat-mine .bks-seatname { color: var(--accent); }
.bks-seat-turn { box-shadow: 0 0 0 2px var(--accent); }
.bks-seathead { display: flex; align-items: baseline; gap: 6px; }
.bks-seatname {
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.bks-seatrole,
.bks-seatturn {
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    white-space: nowrap;
}
.bks-seatrole { opacity: 0.5; }
.bks-seatturn { color: var(--accent); margin-left: auto; }
.bks-seattotals { display: flex; align-items: baseline; gap: 7px; }
.bks-seatround {
    font-size: 21px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1.05;
}
.bks-seatmatch { font-size: 12px; opacity: 0.7; white-space: nowrap; }
/* The counts behind the two figures, so they can be checked rather than trusted.
   Deliberately small: it is a receipt, not a headline. */
.bks-seatsplit { font-size: 11px; opacity: 0.55; white-space: nowrap; }
/* The viewer's own panel sits with the hand, so it is a full-width panel rather
   than a grid cell and loses the border the shared `.panel` already draws. */
.bks-mine { padding: 0; margin-bottom: 10px; }
.bks-mine .bks-seat {
    background: none;
    border: 0;
    border-radius: 0;
    min-height: 0;
}

/* ---------- the winnings, inside the seat panel ---------- */

/* ONE ROW A SEAT, with the suits as groups inside it. Four separately reserved
   card rows a seat measured 888px of panel at 1280px wide, nearly all of it empty
   early in a round. It grows all round, so it scrolls sideways rather than
   wrapping: a wrapping row would walk the panels below it down the screen a step
   at a time. One row reserved, at the mini card size.
   NOT auto-scrolled to its end, unlike the sibling games' growing strips: the
   cards are SORTED, so a new one lands inside a group rather than at the end, and
   the useful thing to keep in view is the start. */
.bks-pilerow {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: nowrap;
    overflow-x: auto;
    padding-top: 2px;
    min-height: calc(22px * 1.39 + 8px);
}
.bks-pilecount { font-size: 11px; opacity: 0.55; white-space: nowrap; flex: 0 0 auto; }
.bks-pilegroup { display: flex; align-items: center; gap: 3px; flex: 0 0 auto; }
/* A suit nobody has taken any of still shows its count, so the four sit in the
   same order in every pile and are read by position. */
.bks-pilegroup-empty { opacity: 0.4; }
.bks-pilesuit {
    display: flex;
    align-items: baseline;
    gap: 2px;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
/* The suit this seat holds outright: its cards pay +1 each, so the group's glyph is
   marked rather than left to be worked out from the table on the side. A PILL
   behind the glyph, not a recolor: the glyph keeps its suit ink, and a green
   heart would read as a fifth suit. */
.bks-pilegroup-lead .bks-pilesuit {
    background: rgba(127, 209, 163, 0.22);
    box-shadow: 0 0 0 1px rgba(127, 209, 163, 0.6);
    border-radius: 4px;
    padding: 0 3px;
}
.bks-pilefan { display: flex; gap: 2px; align-items: center; }
.bks-pilerow .card { --cw: 22px; flex: 0 0 auto; }
/* The viewer's own pile gets the room to be bigger, since it is the one a player
   actually counts. */
.bks-mine .bks-pilerow { min-height: calc(28px * 1.39 + 8px); }
.bks-mine .bks-pilerow .card { --cw: 28px; }

/* ---------- the value badge ---------- */

/* On every card whose printed rank is not the number the rules count: the two
   ZEROS (Queen and King), the Ace as a 1 and the Jack as an 11. The positioning
   class is scoped to this game so the shared `.card` rule stays untouched. */
.card.bks-hasbadge { position: relative; }
.bks-value {
    position: absolute;
    top: -6px;
    left: 50%;
    transform: translateX(-50%);
    background: #ffd166;
    color: #4a3600;
    font-size: 11px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    padding: 2px 5px;
    border-radius: 8px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
    pointer-events: none;
    z-index: 2;
}
/* A ZERO cannot push anybody past the limit, which makes it a different kind of
   card from every other one in the pack, so it is colored rather than merely
   numbered. */
.bks-value.bks-zero { background: #7fd1a3; color: #123123; }

/* ---------- card labels in prose ---------- */

.bks-cardlabel { white-space: nowrap; }
.bks-label { font-weight: 700; }
.bks-chipvalue {
    margin-left: 3px;
    padding: 0 4px;
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.14);
    font-size: 11px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    /* What a card counts is a fact about it, not part of its name, so it does not
       inherit the color the name is carrying. */
    color: var(--ink);
}

/* ---------- the leftover choice and the next dealer ---------- */

.bks-actions {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-bottom: 10px;
    /* Reserved even when empty, and CONTROLS ABOVE PROSE in their own boxes, so a
       phase that adds a line of wording cannot push the buttons onto a second row
       or walk the hand down the screen. 84px is the MEASURED tallest ordinary
       shape (tools/bookshop/bks-measure.mjs): the leftover step's two buttons over its
       three-line note. Reserved at 78 it grew 5px there and took the hand with
       it. */
    min-height: 84px;
}
.bks-controlrow {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    align-items: center;
    min-height: 34px;
}
.bks-actionnote {
    font-size: 13px;
    line-height: 1.5;
    opacity: 0.85;
    min-height: 2.9em;
}
.bks-preview {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* ---------- the hand ---------- */

/* Dimmed means "you cannot play this right now", so the legal cards are the
   bright ones. Scoped to this game's hand panel. */
.bks-handpanel #hand-cards .card.bks-mute { opacity: 0.4; }
/* Picked out to add to your winnings. The lift is on top of the shared `.sel`
   outline, so the choice reads without relying on color alone. */
.bks-handpanel #hand-cards .card.bks-picked { transform: translateY(-8px); }
body.motion-off .bks-handpanel #hand-cards .card.bks-picked {
    transform: none;
}

/* ---------- the round summary and the final result ---------- */

.bks-result { --bks-cols: 4; }
.bks-result .result-row {
    display: grid;
    grid-template-columns: minmax(96px, 1.1fr) repeat(var(--bks-cols), 1fr);
    gap: 6px;
    align-items: baseline;
}
.bks-result .result-cell {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.bks-result .result-cell.bks-won { color: var(--accent); font-weight: 700; }
.bks-resultsuit { white-space: nowrap; }
.bks-result .result-row.bks-roundrow {
    border-top: 1px solid rgba(255, 255, 255, 0.12);
    padding-top: 3px;
}
.bks-result .result-row.total {
    border-top: 1px solid rgba(255, 255, 255, 0.18);
    padding-top: 4px;
    font-weight: 700;
}
.bks-log-section { font-weight: 600; }
.bks-log-out { color: var(--accent); }
.bks-log-win { font-weight: 700; }

/* ---------- the reference tables on the rules pages ---------- */

.bks-valuetable { border-collapse: collapse; margin: 6px 0; font-size: 13px; }
.bks-valuetable th,
.bks-valuetable td { padding: 3px 12px 3px 0; text-align: left; }
.bks-valuetable thead th { font-size: 11px; text-transform: uppercase; opacity: 0.6; }
.bks-value-cell { text-align: right !important; font-variant-numeric: tabular-nums; }

@media (max-width: 860px) {
    /* Below this a panel per column is too narrow to read, so the panels stack in
       turn order and the trick keeps its own columns underneath. Turn order then
       reads top to bottom, which is the next best thing to reading round, and the
       cards still line up with each other. */
    .bks-table,
    .bks-table.bks-seats5 { grid-template-columns: minmax(0, 1fr); }
    /* The five-seat selector is REQUIRED, not belt and braces. The desktop
       placements for five seats are three classes deep
       (`.bks-seats5 .bks-seat.pos-upleft`), so a two-class reset here lost to them
       and left the opponents in columns 2 to 4 as IMPLICIT columns: the trick's
       `1 / -1` then spanned only the one explicit column, crushed into a sliver,
       and two panels shared a cell. Four seats never showed it, because its
       placements are two classes deep and this later rule wins the tie. */
    .bks-table .bks-seat,
    .bks-table.bks-seats5 .bks-seat { grid-column: 1; grid-row: auto; }
    .bks-trick { grid-row: auto; }
    .bks-seat { min-height: 0; }
}

@media (max-width: 720px) {
    .bks-board .bks-side { flex: 1 1 100%; }
    /* One property, so every reservation keyed to the card size recomputes. */
    .bks-table { --bks-trickcw: 46px; }
    .bks-sumnow { font-size: 23px; }
    .bks-seatround { font-size: 18px; }
    /* Every reservation re-derived at this width rather than inherited from the
       desktop figures, which is the same bug with different numbers. */
    .bks-trickhead { min-height: 3.2em; flex-wrap: wrap; }
    .bks-actionnote { min-height: 4.3em; }
    /* Reserved for TRICK PLAY and the leftover step, which measure 0px of
       movement at this width; the round summary's dealer buttons wrap to a second
       row and take it further, and that is left unreserved on purpose - the hand
       is hidden by then, so the only thing below it to push is the summary panel
       the growth is part of. */
    .bks-actions { min-height: 132px; }
    .status.bks-status { min-height: 4.4em; }
    /* The score chips wrap to two rows at five players, and the "Trick n" chip
       only exists during play, so the row count changes by one between phases
       unless it is reserved. */
    .progress.bks-scoreboard { min-height: 68px; align-items: flex-start; }
    /* The hand's heading runs under the absolutely placed Sort button at this
       width ("Your hand: follow \2663 if you can" is 240px of a 340px panel), so
       it keeps clear of the button on the right and wraps instead. Wrapping
       changes the line count by phase, and #hand-cards sits directly below, so
       the heading reserves the two lines its longest wording takes, the Crisps
       arrangement. */
    .bks-handpanel .panel-head { min-height: calc(2 * 1.3em + 10px); }
    .bks-handpanel #hand-title { padding-right: 84px; line-height: 1.3; }
    /* NO min-height ON #hand-cards. A fixed three rows was here, for thirteen
       cards at four seats, and it left a dead row under the ten-card hand at five.
       The tray is reserved at runtime by src/shared/ui/handspace.ts, per width and
       per deal, and that measures both tables correctly on its own. */
    .bks-suitrow { grid-template-columns: 24px repeat(var(--bks-cols), 1fr) 26px; }
}
/* ==========================================================================
 * Khmer (khmer.html) - Team SAIEN's Elements on a standard deck. Opponent on
 * top (hidden hand as backs), the shared display and its limit in the middle,
 * your taken cards and hand below, and the action column inside the hand
 * panel so a decision never moves the cards (the Liar!/Le Truc recipe).
 * Everything here is scoped through a `.khmer-` class, never a bare shared id.
 * ========================================================================== */
.status.khmer-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.khmer-status { min-height: 4.4em; }
}
/* --- the progress line ------------------------------------------------------ */
.progress.khmer-progress {
    opacity: 1;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
}
.khmer-chip {
    font-size: 13px;
    padding: 4px 10px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.25);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.khmer-tokens { display: inline-flex; gap: 8px; align-items: baseline; }
.khmer-tokens.mine { border-color: rgba(255, 209, 102, 0.5); }
.khmer-tokname { font-size: 13px; opacity: 0.8; }
.khmer-toknum { font-size: 14px; font-weight: 700; color: var(--accent); }
/* --- the board -------------------------------------------------------------- */
.khmer-board {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 10px 0 14px;
}
.khmer-board .card { --cw: 60px; }
.khmer-board .card.mini { --cw: 42px; }
/* Each side is one centered column: the strip, the opponent's last move and
   hand, and the taken cards all sit on the board's center line. */
.khmer-side {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 8px 10px;
    border-radius: 12px;
    border: 1px solid transparent;
}
/* The acting side lights up so whose-turn is unmistakable. */
.khmer-side.khmer-active {
    border-color: rgba(255, 209, 102, 0.5);
    background: rgba(255, 209, 102, 0.06);
}
/* Once a round ends, the side that won it takes the same frame. */
.khmer-side.khmer-won {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.1);
}
.khmer-seathead {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px;
}
/* The viewer's sum against the limit: signed, and colored by whether a knock
   is allowed (at or under the limit) or not (over it). */
.khmer-delta { font-weight: 700; }
.khmer-delta.khmer-under { color: #7ee2a8; border-color: rgba(126, 226, 168, 0.5); }
.khmer-delta.khmer-over { color: #ff7b72; border-color: rgba(255, 123, 114, 0.55); }
/* The opponent's last move, on its own reserved line so a long one ("discarded
   a 6, out of the round") never wraps the strip above it. Lit when it is the
   move just made, which is what makes a discard visible at all. */
.khmer-lastmove {
    min-height: 1.5em;
    line-height: 1.5em;
    font-size: 13px;
    opacity: 0.7;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
    padding: 0 10px;
    border-radius: 999px;
}
.khmer-lastmove.khmer-fresh {
    opacity: 1;
    color: var(--accent);
    background: rgba(255, 209, 102, 0.12);
    font-weight: 700;
}
.khmer-winmark { font-weight: 700; }
.khmer-seatname { font-weight: 700; font-size: 15px; }
.khmer-toact {
    font-size: 11px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--accent);
}
/* The marker keeps its slot when idle so the head row never jumps. */
.khmer-toact.off { visibility: hidden; }
.khmer-sum { border-color: rgba(255, 209, 102, 0.5); font-weight: 700; }
/* The opponent's hidden hand, and the taken rows: one mini-card row tall,
   reserved so a card arriving never reflows the side. */
.khmer-backs, .khmer-taken {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    min-height: calc(42px * 89 / 64);
}
.khmer-backs .card, .khmer-taken .card { --cw: 42px; }
.khmer-none { font-size: 13px; opacity: 0.55; font-style: italic; }
/* --- the central display and the limit -------------------------------------- */
/* Three columns: an empty one, the Limit box with the display (centered on the
   board), and the discard pile. The two outer columns are equal, so the middle
   one sits on the center line whatever the pile holds. */
.khmer-mid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: center;
    gap: 14px;
}
.khmer-mid > .khmer-center { grid-column: 2; }
.khmer-discards {
    grid-column: 3;
    justify-self: start;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    min-width: 0;
}
.khmer-pilelabel { font-size: 11px; text-transform: uppercase; opacity: 0.7; }
/* Discarded 6s, fanned tight: at most six ever leave a round. */
.khmer-discardfan {
    display: flex;
    align-items: center;
    min-height: calc(36px * 89 / 64);
}
.khmer-discardfan .card.mini { --cw: 36px; }
.khmer-discardfan .card + .card { margin-left: -22px; }
.khmer-discardfan .card.khmer-fresh {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    border-radius: 5px;
}
.khmer-center {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 10px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.3);
    border: 1px solid rgba(255, 209, 102, 0.3);
    /* One card row tall, reserved, so playing/taking never changes its height. */
    min-height: calc(60px * 89 / 64 + 20px);
}
.khmer-limit {
    display: flex;
    flex-direction: column;
    align-items: center;
    flex: 0 0 auto;
    padding: 4px 12px;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.35);
}
.khmer-limit-label { font-size: 11px; text-transform: uppercase; opacity: 0.7; }
.khmer-limit-num {
    font-size: 26px;
    font-weight: 800;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}
/* The display, fanned: each card overlaps the one before, so its rank corner
   stays readable and up to a dozen cards fit without a scrollbar. The top
   card (the one a Take removes) is fully shown and highlighted. */
.khmer-display { display: flex; align-items: center; }
.khmer-display .card { --cw: 60px; }
.khmer-display .card + .card { margin-left: -34px; }
.khmer-display .card.khmer-top {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    border-radius: 6px;
}
.khmer-display .card.clickable:hover { transform: translateY(-4px); }
/* --- the hand panel + action bar (the Le Truc recipe) ----------------------- */
.khmer-handrow {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    min-height: calc(var(--cw) * 89 / 64);
}
.khmer-handrow #hand-cards { flex: 1 1 auto; }
.card.khmer-picked {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}
.khmer-actions {
    /* Fixed column, present even when empty, with a reserved height covering
       the tallest state (the turn menu with the multi-line knock note, or a
       selected 6 with its play and discard projections) so it never pulses
       as turns pass. */
    flex: 0 0 250px;
    min-height: 250px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 0;
    border-radius: 12px;
    border: 1px solid transparent;
}
.khmer-actions.filled {
    padding: 9px 11px;
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 209, 102, 0.35);
}
.khmer-actions .btn { width: 100%; }
.khmer-note { font-size: 12px; opacity: 0.8; line-height: 1.35; }
@media (max-width: 760px) {
    .khmer-handrow { flex-wrap: wrap; }
    .khmer-actions { flex: 1 1 100%; }
}
/* --- the result panel ------------------------------------------------------- */
.panel.khmer-summary {
    max-width: 560px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
.panel.khmer-summary .result-actions { justify-content: center; flex-wrap: wrap; }
.panel.khmer-summary .result-note { text-align: center; }
.khmer-revealblock {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    margin: 8px auto;
    padding: 8px 10px;
    border-radius: 10px;
    border: 1px solid var(--panel-border);
    max-width: 460px;
}
.khmer-revealblock.khmer-won {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.08);
}
.khmer-revealname { font-weight: 700; }
.khmer-revealrow {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
}
.khmer-reveallabel { font-size: 12px; opacity: 0.7; min-width: 46px; text-align: right; }
.khmer-revealblock .card.mini { --cw: 40px; }
@media (max-width: 620px) {
    .khmer-board .card { --cw: 48px; }
    .khmer-display .card { --cw: 48px; }
    .khmer-display .card + .card { margin-left: -28px; }
    .khmer-center { min-height: calc(48px * 89 / 64 + 20px); }
    /* No room beside the display on a phone: the pile goes under it, centered,
       with its one row reserved. */
    .khmer-mid { grid-template-columns: minmax(0, 1fr); gap: 8px; }
    .khmer-midspacer { display: none; }
    .khmer-mid > .khmer-center { grid-column: 1; justify-self: center; }
    .khmer-discards {
        grid-column: 1;
        justify-self: center;
        flex-direction: row;
        align-items: center;
        gap: 8px;
    }
}

/* =========================================================================
   Cribbage

   A counting game, so the layout is built around two numbers: the COUNT,
   which rides at the right-hand end of the scoreboard line, and the SCORES,
   the shared .progress-score widget above (Cribbage opts in with
   .cribbage-scoreboard). The board is THREE STACKED ROWS - opponent strip,
   center (starter | live count | crib), viewer strip. Every layout rule
   below is load-bearing: without the flex declarations the strips and the
   center collapse to a single card's width and the board becomes one tall
   vertical line of cards.
   ========================================================================= */

/* --- a status line that does not resize ----------------------------------- */
.status.cribbage-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.cribbage-status { min-height: 4.4em; }
}

/* --- the count, against the cards it counts ------------------------------- */
/*
 * It used to ride at the right-hand end of the scoreboard line, two rows above
 * the pile and next to the race to 121 - the one other number it could be
 * confused with. It now sits immediately left of the played cards, so the card
 * and the number it brought the count to are read in one movement.
 *
 * FIXED WIDTH, and painted in every phase (`.idle`, invisible): the center
 * strip must not change width or height when the play starts, when it ends, or
 * when the count goes from 9 to 31.
 */
.cribbage-playbox {
    flex: 0 1 auto;
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}
.cribbage-countchip {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    width: 62px;
    padding: 5px 0;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.28);
    border: 1px solid var(--panel-border);
}
.cribbage-countchip.idle { visibility: hidden; }
.cribbage-count-label {
    font-size: 10px;
    opacity: 0.7;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
.cribbage-count-value {
    font-size: 23px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.cribbage-count-max { font-size: 11px; opacity: 0.5; }

/* --- the board: three strips STACKED, each spanning the full width -------- */
/* display:flex + column is what stacks the three strips as ROWS; without it
   they - and the cards inside them - collapse into a single vertical line.
   align-items:stretch (the default, stated for emphasis) makes each strip
   span the playarea rather than shrink-wrap to one card. */
.cribbage-table {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    padding: 12px 14px;
    margin-bottom: 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}

/* A seat's strip: name gutter on the left, then the seat's cards in a ROW. */
.cribbage-seatrow {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: calc(58px * 89 / 64);
}
.cribbage-seatrow.active .cribbage-seatname { color: var(--accent); opacity: 1; }
.cribbage-seatname {
    flex: 0 0 92px;
    text-align: right;
    font-size: 13px;
    font-weight: 600;
    opacity: 0.7;
    white-space: nowrap;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
}
.cribbage-dealertag {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 1px 7px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.25);
    opacity: 0.9;
}
.cribbage-seatcards {
    display: flex;
    align-items: center;
    gap: 4px;
    flex-wrap: wrap;
    flex: 1 1 auto;
    min-width: 0;
}
/* Backs and closed-round cards are context, not the thing being played to. */
.cribbage-seatcards .card { --cw: 44px; flex: none; }
/* "go", on the seat that said it: the one badge that means "this player is out
   of the round", so it is colored rather than quiet like the dealer tag. */
.cribbage-gotag {
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 1px 7px;
    border-radius: 999px;
    color: var(--felt-dark);
    background: var(--accent);
}

/* The center strip: starter | the live round | the crib, in a horizontal ROW.
   flex-wrap lets the three sections drop under one another only on a phone;
   on any real width they sit side by side. */
.cribbage-center {
    display: flex;
    align-items: flex-start;
    gap: 18px;
    justify-content: center;
    flex-wrap: wrap;
    padding: 6px 0;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
/* Starter and crib: a card (or backs) over a small label. */
.cribbage-pilechip {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.cribbage-pilechip .card { --cw: 58px; flex: none; }
.cribbage-pile-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.6;
    white-space: nowrap;
}
/*
 * The crib: FOUR FACE-DOWN BACKS IN BARELY MORE THAN ONE CARD'S WIDTH.
 *
 * It is the least informative thing on the board - four cards nobody may look
 * at until the show - and it was taking a quarter of the center strip at the
 * starter's size with a loose overlap. Smaller cards and a tight stack say the
 * same two facts (there are four, they are face down) in a third of the room,
 * which is what the live count needed back.
 */
.cribbage-cribcards { display: flex; }
.cribbage-cribcards .card { --cw: 38px; }
.cribbage-cribcards .card + .card { margin-left: -30px; }
.card.cribbage-starter { outline: 2px solid rgba(255, 255, 255, 0.45); }

/* The round in progress: full-size cards, lightly overlapped in play order.
   The vertical padding is the room a peg badge hangs into, above the card or
   below it; it is there in every phase, so nothing moves when one appears. */
.cribbage-pilerow {
    flex: 0 1 auto;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    padding: 12px 0;
    min-height: calc(76px * 89 / 64);
}
.cribbage-pilerow .card { --cw: 76px; flex: none; }
.cribbage-pilerow .card + .card { margin-left: -30px; }
.cribbage-pilerow .card.empty { box-shadow: none; }

/* The card the count is standing on, which is what the next play answers. The
   slot wrapper takes the overlap rule in the card's place, so a scoring card
   sits in the row exactly where a bare one would. */
.cribbage-pileslot { position: relative; display: flex; flex: none; }
.cribbage-pilerow > :last-child .card,
.cribbage-pilerow > .card:last-child {
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.45);
}
.cribbage-pilerow .cribbage-pileslot + .card,
.cribbage-pilerow .card + .cribbage-pileslot,
.cribbage-pilerow .cribbage-pileslot + .cribbage-pileslot { margin-left: -30px; }

/*
 * WHAT THE LAST CARD PEGGED, on the card itself.
 *
 * The score used to arrive only as a log line and a score that ticked up, which
 * is easy to miss entirely in the middle of a hand - and pegging is half the
 * game. The badge sits on the card that earned it, reads "+2 fifteen", and
 * stays until the next card is played, because until then the count is still
 * standing on it. Gold on dark, like every other points figure in the family.
 *
 * ON THE SIDE OF THE CARD ITS PLAYER SITS: under the viewer's own card, over
 * the opponent's. The board puts the opponent's strip above the center and the
 * viewer's below it, so the badge now points at whoever earned it instead of
 * hanging below every card whatever its owner.
 */
.cribbage-pegbadge {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: baseline;
    gap: 5px;
    padding: 2px 8px;
    border-radius: 999px;
    white-space: nowrap;
    background: var(--accent);
    color: var(--felt-dark);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
    /* Above the neighboring cards, which overlap this one by 30px. */
    z-index: 2;
}
.cribbage-pileslot.mine .cribbage-pegbadge { bottom: -9px; }
.cribbage-pileslot.theirs .cribbage-pegbadge { top: -9px; }
.cribbage-pegbadge-pts { font-size: 15px; font-weight: 800; line-height: 1; }
.cribbage-pegbadge-why {
    font-size: 11px;
    font-weight: 600;
    text-transform: lowercase;
}
/* The scoring card is ringed in gold rather than white: it is both the card the
   count stands on AND the card that just paid. */
.cribbage-pileslot.scored .card {
    box-shadow: 0 0 0 2px var(--accent);
}
/* The pop comes FROM the card, so it runs the other way for a badge that sits
   above one. Both keyframes carry the centering translate, which the animation
   would otherwise replace for its duration. */
.cribbage-pileslot.mine .cribbage-pegbadge {
    animation: cribbage-pegpop 0.28s ease-out;
}
.cribbage-pileslot.theirs .cribbage-pegbadge {
    animation: cribbage-pegpop-up 0.28s ease-out;
}
@keyframes cribbage-pegpop {
    from { transform: translateX(-50%) translateY(4px) scale(0.85); opacity: 0; }
    to { transform: translateX(-50%) translateY(0) scale(1); opacity: 1; }
}
@keyframes cribbage-pegpop-up {
    from { transform: translateX(-50%) translateY(-4px) scale(0.85); opacity: 0; }
    to { transform: translateX(-50%) translateY(0) scale(1); opacity: 1; }
}
/* The badge still says "+2 fifteen" standing still - the pop only draws the
   eye to it. body.motion-off is "Animations: Off" (shared/ui/motion.ts). */
body.motion-off .cribbage-pegbadge { animation: none; }

/* --- the hand, and the controls beside it --------------------------------- */
/* Cards and the action column are a flex row aligned to the TOP: the action
   column's contents changing height never nudges the hand, and picking a card
   lifts it with a transform (layout-neutral), so the row does not reflow. */
.cribbage-handrow {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    min-height: calc(var(--cw) * 89 / 64 + 8px);
    padding-top: 8px;
}
.cribbage-handrow #hand-cards { flex: 1 1 auto; min-width: 0; }
.cribbage-actions {
    flex: 0 0 232px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 0;
    border-radius: 12px;
    border: 1px solid transparent;
    min-height: 78px;
}
.cribbage-actions.filled {
    padding: 9px 11px;
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 209, 102, 0.35);
}
.cribbage-actions .btn { width: 100%; }

/*
 * WHAT THE FOUR YOU ARE KEEPING ARE WORTH, while you are still choosing them.
 *
 * Two figures: the four as they lie, and their mean over every unseen starter.
 * The second is the one a keep should be chosen on and the one no player can
 * work out at the table. Draft Cribbage has had the same panel since it
 * shipped; this is the question in its own game.
 */
.cribbage-keepvalue {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding-bottom: 2px;
}
.cribbage-keeplabel {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.7;
}
.cribbage-keepnote { font-size: 12px; opacity: 0.8; line-height: 1.35; }
.cribbage-keeprow { display: flex; gap: 14px; }
.cribbage-keepcell {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
}
.cribbage-keeppts {
    font-size: 20px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
    color: var(--accent);
}
/* The average is a mean over 46 counts rather than a count, so it opens
   nothing: it is a number, not a button. */
.cribbage-keepavg { color: var(--ink); opacity: 0.9; }
.cribbage-keepcaption { font-size: 11px; opacity: 0.7; }
.cribbage-actions-note { font-size: 12px; opacity: 0.8; line-height: 1.35; }
@media (max-width: 760px) {
    .cribbage-handrow { flex-wrap: wrap; }
    .cribbage-actions { flex: 1 1 100%; }
}
.cribbage-actions .btn.hinted {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}

/* A card picked for the crib: lifted and ringed, transform is layout-neutral. */
.card.cribbage-picked {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    transform: translateY(-8px);
}
.card.cribbage-toohigh { opacity: 0.45; filter: saturate(0.6); }

/* --- the show (hand summary) ---------------------------------------------- */
.panel.cribbage-summary {
    max-width: 560px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
.panel.cribbage-summary .result-actions { justify-content: center; flex-wrap: wrap; }
.panel.cribbage-summary .result-note { text-align: center; }
/*
 * ONE LINE PER COUNTED HAND, so the whole summary fits a desktop viewport
 * without scrolling: the shell lifts this panel into a centered dialog, and a
 * scroll inside a modal is the worst place to keep the arithmetic the hand just
 * produced. Each row was three lines (a head, a row of 52px cards, a detail
 * line) and the starter had a fourth row of its own above them.
 *
 * A GRID OF FIXED TRACKS, so the names, the cards, the reasons and the totals
 * each line up down the panel (`## Score tables line up by column`): every row
 * holds exactly four cards and the starter, so the card track's width is a
 * constant rather than something each row sizes for itself.
 */
/*
 * The row is ONE BUTTON over its whole face (it carries .scorebd-chip), so the
 * breakdown is opened by pressing the cards, the reasons or the total and not
 * only the number on the end. A button centers its own text and sizes to its
 * content, so the grid, the full width and the left alignment are all stated
 * here; the shared chip rule supplies the hover border.
 */
.cribbage-showrow {
    display: grid;
    grid-template-columns: 78px 224px minmax(0, 1fr) 42px;
    align-items: center;
    gap: 0 8px;
    width: 100%;
    padding: 5px 4px;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 0;
    text-align: left;
}
/* The hover border is what says the row is pressable, but a full border would
   redraw the row's own top rule; light the row instead. */
.cribbage-showrow.scorebd-chip:hover:not(:disabled) {
    border-color: transparent;
    background: rgba(255, 255, 255, 0.05);
}
.cribbage-showrow.scorebd-chip:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}
.cribbage-showname { font-size: 13px; font-weight: 600; line-height: 1.25; }
/* The total FILLS its track and right-aligns its digits, rather than being a
   shrink-to-fit box parked at the end: a 14 is wider than an 8, so an end-
   justified chip starts at a different x in every row and the column stops
   reading down the panel. Stretched, the boxes line up and tabular figures
   line the digits up inside them. It also gives the chip a bigger tap target. */
.cribbage-showpts {
    font-size: 17px;
    font-weight: 800;
    color: var(--accent);
    justify-self: stretch;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.cribbage-showcards { display: flex; align-items: center; gap: 3px; }
/* 40px is inside the band where the full art's pips collapse and the flat
   rank-over-suit art stays legible, and these are a read-only exhibit, so they
   are drawn from the simplified atlas (`## Small cards use the simplified
   sprite`). */
.cribbage-showcards .card { --cw: 40px; flex: none; }
/* The starter on the end of each row, set apart by the '+': it is the one card
   in the row that is not the player's. */
.cribbage-showplus { opacity: 0.5; font-weight: 700; margin: 0 1px; }
.card.cribbage-showstarter { outline: 2px solid rgba(255, 255, 255, 0.35); }
.cribbage-showdetail { font-size: 12px; opacity: 0.8; line-height: 1.3; }

/* The race to 121, one line, each total carrying its own seat name so there is
   no header row for it to line up with. */
.cribbage-totline {
    display: flex;
    justify-content: center;
    gap: 18px;
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px solid var(--panel-border);
}
.cribbage-totcell { display: inline-flex; align-items: baseline; gap: 5px; }
.cribbage-totname { font-size: 13px; opacity: 0.75; }
.cribbage-totnum {
    font-size: 21px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.cribbage-tottarget { font-size: 12px; opacity: 0.5; }
.cribbage-totcell.winner .cribbage-totnum { color: var(--accent); }

@media (max-width: 620px) {
    /* No room for four columns: the name heads the row, the cards and the
       reasons go under it, and the total stays on the name's line. */
    .cribbage-showrow {
        grid-template-columns: 1fr 42px;
        gap: 2px 8px;
    }
    .cribbage-showcards,
    .cribbage-showdetail { grid-column: 1 / -1; }
    .cribbage-showcards .card { --cw: 34px; }
}

/* --- log ------------------------------------------------------------------ */
.cribbage-logshow { color: var(--accent); }

/*
 * THE PEGGING, DRAWN: one strip per round, the opponent's cards along the top
 * and the viewer's along the bottom, in play order.
 *
 * It replaces eight text lines a hand ("Bot plays 8♠, 15: fifteen for 2"). The
 * play is a sequence of cards against a climbing count, and the alternation is
 * the thing being read, so two rows of small cards carry it at a glance in a
 * fraction of the height.
 *
 * The strip reserves the room for BOTH rows plus the badges above and below, so
 * a round that happens to be all one player's cards does not sit at a different
 * height from the one before it.
 */
.cribbage-pegline {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}
.cribbage-pegstrip {
    display: flex;
    align-items: flex-start;
    gap: 2px;
    /* Two card rows, offset, plus the badge room: the card is 28px wide, so
       28 * 89/64 is its height, and the offset and badges are the rest. */
    min-height: calc(28px * 89 / 64 + 26px);
    padding: 7px 0;
}
.cribbage-pegslot { position: relative; display: flex; flex: none; }
/* The offset that says whose card it is. The opponent's sit at the top of the
   strip, the viewer's a card's-worth lower down. */
.cribbage-pegslot.mine { margin-top: 13px; }
.cribbage-pegstrip .card { --cw: 28px; flex: none; }
/* What the card pegged, on its player's own side of it - the same rule the
   board's badge follows. Absolute, so it never moves the row. */
.cribbage-pegchip {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    padding: 0 4px;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 800;
    line-height: 1.4;
    white-space: nowrap;
    background: var(--accent);
    color: var(--felt-dark);
    z-index: 2;
}
.cribbage-pegslot.mine .cribbage-pegchip { bottom: -7px; }
.cribbage-pegslot.theirs .cribbage-pegchip { top: -7px; }
/* A seat that could not play: a gap in its row where a card would have gone. */
.cribbage-peggo {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: calc(28px * 89 / 64);
    border-radius: 4px;
    border: 1px dashed rgba(255, 255, 255, 0.3);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.75;
}
/*
 * What the round closed on, and the point for its last card.
 *
 * THE TWO BREAK ONTO SEPARATE LINES RATHER THAN SHARING A CRAMPED ONE. Read
 * together on one line they run into each other ("11 You peg 1 for last
 * card."), and the count then reads as the first word of the sentence instead
 * of as the number the round finished on. Wrapping moves the note as a WHOLE
 * item to its own line, which is what flex-wrap does and what text wrapping
 * inside the note would not: `min-width` is the width below which it stops
 * trying to share.
 */
.cribbage-pegtail {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 2px 7px;
    min-width: 0;
}
.cribbage-pegcount {
    flex: none;
    font-size: 15px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    opacity: 0.9;
}
.cribbage-pegnote {
    flex: 1 1 auto;
    min-width: 11em;
    font-size: 12px;
    opacity: 0.8;
}

/*
 * A counted hand in the log, collapsed to its one line until asked about.
 *
 * This is where a hand stays viewable after the next deal has replaced the
 * summary panel, so it holds the five cards and the arithmetic - but a log is
 * read for the story of the game, so it opens on demand rather than filling the
 * column with card faces.
 */
.cribbage-logfold > summary {
    cursor: pointer;
    /* The default marker sits badly against the log's tight line height, and
       the accent-colored line already reads as a heading. */
    list-style: none;
}
.cribbage-logfold > summary::-webkit-details-marker { display: none; }
/* A caret that turns, so a collapsed count looks openable. */
/* Drawn, not the U+25B8 glyph, which sits low against capitals: the same
   triangle as the shared log caret (see `details.loggroup > summary::before`
   in src/shared/styles/platform.css). */
.cribbage-logfold > summary::before {
    content: '';
    display: inline-block;
    width: 0.38em;
    height: 0.44em;
    background: currentColor;
    clip-path: polygon(0 0, 100% 50%, 0 100%);
    vertical-align: 0.13em;
    margin: 0 0.56em 0 0.06em;
    opacity: 0.6;
    transition: transform 0.12s ease-out;
}
.cribbage-logfold[open] > summary::before { transform: rotate(90deg); }
.cribbage-logcards {
    display: flex;
    align-items: center;
    gap: 3px;
    flex-wrap: wrap;
    margin: 4px 0 2px 1em;
}
.cribbage-logcards .card { --cw: 28px; flex: none; }
.cribbage-logfold-detail {
    margin-left: 1em;
    font-size: 12px;
    opacity: 0.8;
}
body.motion-off .cribbage-logfold > summary::before {
    transition: none;
}

/* --- phones: the opponent's strip on ONE row ------------------------------ */
/*
 * The strip holds up to six backs (the deal, before the throw) plus the name
 * gutter, and at 44px a card that is ~360px - wider than the column on a phone,
 * so it wrapped to two rows and the board grew by a card's height.
 *
 * Both halves give way: the gutter loses its fixed width and its second line
 * (the dealer tag moves up beside the name), and the cards come down to 30px.
 * Six at 30px plus five 3px gaps is 195px, which clears even a 320px screen
 * with the gutter beside it.
 */
@media (max-width: 620px) {
    .cribbage-seatrow { gap: 7px; min-height: calc(40px * 89 / 64); }
    .cribbage-seatname {
        flex: 0 1 auto;
        flex-direction: row;
        align-items: center;
        gap: 4px;
        font-size: 12px;
        /* Long names give way before the cards do. */
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        /* The clip box reaches 6px past the name, so a tutorial's .coach-mark
           ring on the dealer tag is drawn whole. The negative margin hands
           the padding back, so nothing moves. */
        padding: 6px;
        margin: -6px;
    }
    .cribbage-dealertag,
    .cribbage-gotag { padding: 0 5px; font-size: 9px; }
    .cribbage-seatcards { gap: 3px; flex-wrap: nowrap; }
    .cribbage-seatcards .card { --cw: 30px; }
    /* The center strip is the one that may still wrap: it is the live count and
       it is worth the room. The crib shrinks again rather than pushing it. */
    .cribbage-center { gap: 10px; }
    .cribbage-pilechip .card { --cw: 46px; }
    .cribbage-cribcards .card { --cw: 32px; }
    .cribbage-cribcards .card + .card { margin-left: -26px; }
    .cribbage-pilerow { min-height: calc(60px * 89 / 64); }
    .cribbage-pilerow .card { --cw: 60px; }
    .cribbage-pilerow .card + .card,
    .cribbage-pilerow .cribbage-pileslot + .card,
    .cribbage-pilerow .card + .cribbage-pileslot,
    .cribbage-pilerow .cribbage-pileslot + .cribbage-pileslot {
        margin-left: -24px;
    }
    .cribbage-pegbadge-pts { font-size: 13px; }
    .cribbage-pegbadge-why { font-size: 10px; }
    /* The count keeps its place beside the cards, one size smaller. */
    .cribbage-playbox { gap: 7px; }
    .cribbage-countchip { width: 52px; padding: 4px 0; }
    .cribbage-count-value { font-size: 19px; }
    /* The action column is full width here, so the two keep figures have the
       room to sit side by side with their captions. */
    .cribbage-keeprow { gap: 20px; }
}

/* =========================================================================
   Jacks' Dream (src/jacksdream/, pages/jacksdream.mjs)

   A calm solitaire. Shared classes (.card, .card.simple.back, .card.empty,
   .panel, .btn, .logline, .winner-banner, .controls) are reused as-is; only
   this game's own layout lives here, all of it prefixed `jd-`. The tableau is
   five columns, each a downward cascade so the movable top card sits fully
   visible at the foot, with the moves it has listed under it; the Jacks still
   in hand sit centered under the board.
   ========================================================================= */

/* --- the five columns ---------------------------------------------------- */

.jd-board {
    display: flex;
    justify-content: center;
    gap: 14px;
    margin: 4px 0 12px;
    /* Card size and the cascade's strip, set once here so every reservation
       below follows them at each breakpoint. */
    --jd-cw: 76px;
    --jd-strip: 34px;
    /* A column holds 9 units at the full strip and tightens after that (see
       .jd-column), so it keeps ONE height through the game and the hand under
       the board never moves. Expert solves peak at 11 or 12 units. */
    --jd-room: calc(var(--jd-strip) * 8);
    --jd-ch: calc(var(--jd-cw) * 89 / 64);
}
.jd-colwrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
/* The column's number: the moves under the board name columns by it. */
.jd-colnum {
    font-size: 12px;
    font-weight: 700;
    line-height: 14px;
    opacity: 0.55;
}
.jd-column {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 8px 8px 10px;
    border: 2px solid transparent;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    cursor: pointer;
    /* One card plus the room, plus the padding and border (border-box). */
    min-height: calc(var(--jd-ch) + var(--jd-room) + 22px);
    min-width: calc(var(--jd-cw) + 20px);
    /* The strip each buried unit shows: the full strip up to 9 units, then
       shared out so the cascade fits the same height, never under 18px (the
       rank corner). --jd-gaps is the unit count less one, set by def.ts. */
    --jd-step: clamp(18px, calc(var(--jd-room) / var(--jd-gaps, 1)), var(--jd-strip));
}
/* Every unit is position:relative so the cascade paints in DOM ORDER. A
   pillow needs position:relative for its rank badge; if the number cards
   stayed static, CSS would paint the positioned pillows on top of them
   regardless of order, and a pillow would cover the cards below it instead of
   splaying. Making them all positioned keeps later-in-column = on-top. */
.jd-column > .card { --cw: var(--jd-cw); position: relative; }
/* The downward cascade: each unit overlaps the one above it, so a rank corner
   stays readable and the top (movable) unit shows in full at the foot. */
.jd-column > *:not(:first-child) { margin-top: calc(var(--jd-step) - var(--jd-ch)); }
/* A card on a card of its own rank sits closer, so a stack (which moves as
   one) reads as one. 0.75 of a strip is SAME_RANK_GAP in def.ts, which counts
   these gaps at that size when it shares the room out; keep the two equal. */
.jd-column > .card.jd-samerank {
    margin-top: calc(var(--jd-step) * 0.75 - var(--jd-ch));
}

.jd-column.lit { border-color: var(--accent); background: rgba(46, 125, 70, 0.22); }
.jd-column.sel { border-color: #d9b24a; background: rgba(217, 178, 74, 0.14); }
.jd-column.hint { border-color: #7fb0ff; animation: jd-flash 0.7s ease-in-out 3; }
.jd-column:hover { background: rgba(255, 255, 255, 0.06); }

/* Pre-selection affordance: which columns (and the hand) can move something
   right now, before you click. A soft blue dashed edge, distinct from the
   green destination highlight (.lit) and the gold pickup selection (.sel). */
.jd-column.jd-source {
    border-color: rgba(127, 176, 255, 0.55);
    border-style: dashed;
}
.jd-handpile.jd-source { outline: 2px dashed rgba(127, 176, 255, 0.7); outline-offset: 3px; }

@keyframes jd-flash {
    0%, 100% { box-shadow: 0 0 0 0 rgba(127, 176, 255, 0); }
    50% { box-shadow: 0 0 0 4px rgba(127, 176, 255, 0.5); }
}

/* The empty-column placeholder is a slot, not a card face (never the Ace of
   Clubs - see refreshCardArt): .card.empty already handles that. */
.jd-column > .jd-empty { --cw: var(--jd-cw); opacity: 0.5; }

/* The picked-up run: the whole top same-rank stack (or the top unit). */
.card.jd-moving { outline: 3px solid #d9b24a; outline-offset: 1px; z-index: 2; }

/* Pillows: the shared face-down back, with a small rank badge so progress is
   legible even though the card itself is out of play. The badge sits TOP-LEFT
   so it stays visible in the strip a splayed pillow shows (the foot of the
   card is covered by the unit below it in the cascade). */
.jd-pillow-badge {
    position: absolute;
    left: 4px;
    top: 3px;
    min-width: 16px;
    padding: 0 4px;
    border-radius: 8px;
    font-size: 12px;
    font-weight: 700;
    line-height: 15px;
    text-align: center;
    color: #17324a;
    background: rgba(238, 243, 255, 0.92);
}

/* --- the moves under each column ----------------------------------------- */

/* Two rows reserved: a column has at most four moves (one to each other
   column), two to a row, so the board's height never depends on how many
   moves are open. */
.jd-foot {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-content: flex-start;
    gap: 3px;
    width: calc(var(--jd-cw) + 20px);
    min-height: 41px;
}
/* One move from this column: where it goes, and how many moves it leaves.
   Two of the widest ("→1 10") fit a desktop column with room to spare, so the
   reserved two rows hold in every browser's font. */
.jd-move, .jd-then {
    display: inline-flex;
    align-items: baseline;
    gap: 2px;
    padding: 1px 4px;
    border-radius: 9px;
    font-size: 12px;
    line-height: 17px;
    white-space: nowrap;
    background: rgba(255, 255, 255, 0.12);
    font-variant-numeric: tabular-nums;
}
.jd-move-to { opacity: 0.7; }
.jd-move-n { font-weight: 700; }
.jd-then { font-weight: 700; }
/* One move left after it: look before you take it. */
.jd-move.low, .jd-then.low { background: rgba(217, 178, 74, 0.32); }
/* No moves after it: this move loses the game. */
.jd-move.zero, .jd-then.zero { background: rgba(212, 80, 80, 0.55); color: #fff; }
/* It solves the deal. */
.jd-move.win, .jd-then.win { background: rgba(88, 176, 110, 0.6); color: #fff; }

/* --- rank focus: hold the pointer on a card to see its whole rank -------- */

.jd-column > .card { transition: filter 140ms ease; }
.jd-column > .card.jd-dim { filter: brightness(0.5) saturate(0.6); }
/* The ring is an overlay, not a box-shadow: a buried card's shadow would run
   down both sides of the cards stacked on it, and those cards would look lit
   too. So a buried card rings only the strip it shows, and the card on top
   rings the whole card. */
.card.jd-focus::after {
    content: '';
    position: absolute;
    inset: -3px;
    border: 3px solid #ffe07a;
    border-radius: 9px;
    box-shadow: 0 0 10px 2px rgba(255, 224, 122, 0.6);
    pointer-events: none;
}
.jd-column > .card.jd-focus:not(:last-child)::after {
    bottom: auto;
    height: calc(var(--jd-step) + 3px);
    border-bottom: 0;
    border-radius: 9px 9px 0 0;
    box-shadow: 0 -4px 10px 1px rgba(255, 224, 122, 0.55);
}
/* A hand Jack is not in a column, so it has no strip variable; it is never
   buried under another card either, only overlapped sideways. */
.jd-handpile > .card.jd-focus::after { inset: -2px; border-width: 2px; }
body.motion-off .jd-column > .card { transition: none; }

/* --- the Jacks in hand, under the board ---------------------------------- */

.jd-hand {
    /* Centered on the board's axis, as wide as what it holds. */
    width: fit-content;
    margin-inline: auto;
    padding: 10px 16px;
    /* The fold below animates grid rows, which (unlike height) can reach 0
       from an auto-sized content height. */
    display: grid;
    grid-template-rows: 1fr;
    transition: grid-template-rows 700ms ease, opacity 700ms ease,
        padding 700ms ease, margin 700ms ease, border-width 700ms ease;
}
.jd-hand-inner {
    min-height: 0;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
}
/* The last Jack is placed: hold the note a moment, then fade and fold away.
   The delay sits on this rule only, so an undo that brings a Jack back opens
   the box at once. */
.jd-hand.jd-hand-gone {
    grid-template-rows: 0fr;
    opacity: 0;
    padding-block: 0;
    margin-block: 0;
    border-width: 0;
    transition-delay: 900ms;
}
body.motion-off .jd-hand { transition: none; }

/* Both halves have fixed widths, so the box keeps one size and one position
   while the pile shrinks and the moves under the title come and go. */
.jd-hand-slot {
    display: flex;
    /* Right-aligned: the top Jack keeps one spot beside the title, and the
       pile shrinks away from it as Jacks are placed. */
    justify-content: flex-end;
    /* Four fanned Jacks: one card plus three strips, plus the pile's padding. */
    width: calc(var(--jd-hcw) + 3 * (var(--jd-hcw) - var(--jd-hover)) + 12px);
}
.jd-hand-side {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    /* Three of the widest moves to a row, so five fit in the two reserved. */
    width: 150px;
}
.panel .jd-hand-title { margin: 0; font-size: 15px; }
.jd-hand-note { font-size: 12.5px; opacity: 0.8; }
/* A Jack from hand can go to any of the five columns: room for five moves,
   two rows of them, which is still shorter than the pile beside it. */
.jd-hand-foot { width: 100%; min-height: 41px; }

/* The hand is one pickup control: the remaining Jacks fanned a little. */
.jd-handpile {
    position: relative;
    display: inline-flex;
    align-items: flex-start;
    padding: 4px 6px;
    border: 0;
    background: transparent;
    cursor: pointer;
    border-radius: 12px;
}
/* --jd-hcw is a hand card's width, --jd-hover how much of the card under it
   each next Jack covers. */
.jd-hand { --jd-hcw: 60px; --jd-hover: 40px; }
.jd-handpile > .jd-hand-card { --cw: var(--jd-hcw); position: relative; }
.jd-handpile > .jd-hand-card:not(:first-child) { margin-left: calc(-1 * var(--jd-hover)); }
.jd-handpile.sel { outline: 3px solid var(--accent); outline-offset: 2px; }
.jd-handpile:hover { background: rgba(255, 255, 255, 0.06); }

@media (max-width: 720px) {
    /* The status line wraps to two lines on a phone in most states, and a
       line count that changes per state walks the whole board up and down. */
    .status.jd-status { min-height: 50px; }
    /* The progress line wraps too once Sleep Harder is named on it. */
    .progress.jd-progress { min-height: 39px; }
    /* Nine strips of room, not eight: at this card size eight left a
       twelve-unit column (common in a solved deal) 6px over its reservation. */
    .jd-board { gap: 6px; --jd-cw: 52px; --jd-strip: 24px; --jd-room: calc(var(--jd-strip) * 9); }
    .jd-column { padding: 6px 5px 8px; min-height: calc(var(--jd-ch) + var(--jd-room) + 18px);
        min-width: calc(var(--jd-cw) + 14px); }
    .jd-colnum { font-size: 11px; line-height: 12px; }
    /* One chip to a row in a column this narrow: four rows reserved. */
    .jd-foot { width: calc(var(--jd-cw) + 14px); min-height: 75px; gap: 2px; }
    .jd-move, .jd-then { font-size: 11px; line-height: 15px; padding: 1px 4px; gap: 2px; }
    .jd-pillow-badge { font-size: 11px; line-height: 13px; top: 2px; left: 3px; }
    .jd-hand { --jd-hcw: 46px; --jd-hover: 30px; padding: 8px 12px; }
    .jd-hand-side { width: 140px; }
    .jd-hand-foot { min-height: 36px; }
}
/* =========================================================================
   Conic
   Board LEFT, everything you act with RIGHT. The board is a 4x4 grid whose
   SCORING LINES ARE THE DIAGONALS, which is the one thing the layout has to
   make readable on a square grid: hence a tint per diagonal (heavier the more
   the diagonal pays), a point badge in every space's corner, a pressable chip
   at the head of each diagonal that lights its band, and one fixed line under
   the board describing whichever is lit.
   EVERY selector here is scoped by a cnc- class. #hand-cards, #hand-panel,
   #status-line and #progress-line are on all ~90 pages, so a rule naming one
   of them without a game class reaches every game - which is how six games
   once resized every other game's hand.
   ========================================================================= */
/* --- layout ---------------------------------------------------------------- */
.cnc-layout {
    display: flex;
    align-items: flex-start;
    gap: 18px;
    flex-wrap: wrap;
}
.cnc-side {
    flex: 1 1 460px;
    min-width: 280px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
/* A status line that does not resize between phases: the longest of them is a
   two-line sentence about sending the Queen on. */
.status.cnc-status { min-height: 2.9em; }
.status.cnc-status.cnc-turn { color: var(--accent); }
@media (max-width: 620px) {
    .status.cnc-status { min-height: 4.4em; }
}
/* --- the board ------------------------------------------------------------- */
.cnc-board {
    flex: 0 0 auto;
    padding: 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
/* Five columns and five rows: four of spaces, plus the diagonal chips along the
   top edge and down the right edge, which is where each diagonal begins. */
.cnc-grid {
    --cnc-cw: 62px;
    /* LOAD-BEARING. Without `display: grid` the sixteen spaces stack into one
       tall column of cards - which nothing in src/tests can see, because the DOM
       fakes have no geometry. tools/conic-probe.mjs is what catches it. */
    display: grid;
    /* The label column is a FIXED width, not `auto`. With `auto` the empty
       spacers in it (which have to exist, because a grid is filled in order)
       were free to claim as much as the cell columns, and the board came out
       604px wide instead of 380 - wide enough that the side column wrapped
       underneath it at 1280px and the whole layout stopped being two columns. */
    grid-template-columns: repeat(4, calc(var(--cnc-cw) + 10px)) 44px;
    gap: 4px;
    justify-content: start;
}
.cnc-cell {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 5px;
    border-radius: 9px;
    border: 1px solid transparent;
}
.cnc-cell .card { --cw: var(--cnc-cw); flex: none; }
/* Tinted by what the diagonal pays, so the four-point line through the middle
   is the most prominent thing on the board. */
.cnc-cell.cnc-d0,
.cnc-cell.cnc-d6 { background: rgba(255, 209, 102, 0.035); }
.cnc-cell.cnc-d1,
.cnc-cell.cnc-d5 { background: rgba(255, 209, 102, 0.065); }
.cnc-cell.cnc-d2,
.cnc-cell.cnc-d4 { background: rgba(255, 209, 102, 0.1); }
.cnc-cell.cnc-d3 { background: rgba(255, 209, 102, 0.16); }
/* The lit band, from pressing a diagonal's chip. */
.cnc-cell.cnc-lit { border-color: rgba(255, 209, 102, 0.85); }
/* Somebody's largest group, which pays 1 a card. Inset rather than an outline so
   it cannot be confused with the cone's ring or a legal target. */
.cnc-cell.cnc-group { box-shadow: inset 0 0 0 2px rgba(120, 220, 160, 0.85); }
/* The space being fought over right now. */
.cnc-cell.cnc-conecell {
    border-color: var(--accent);
    box-shadow: 0 0 10px rgba(255, 209, 102, 0.3);
}
.cnc-cell.legal { cursor: pointer; border-color: rgba(255, 255, 255, 0.4); }
.cnc-cell.legal:hover,
.cnc-cell.legal:focus-visible {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.22);
    outline: none;
}
.cnc-cell.hinted {
    border-color: var(--accent);
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}
/* What this space's diagonal pays, in every space, because a diagonal on a square
   grid cannot be traced by eye at a glance. Absolutely positioned, so it paints
   over the card rather than under it, and transparent to the pointer, because the
   SPACE is the click target while the cone is being sent on. High contrast in the
   accent color: over card art a dim chip is indistinguishable from the rotated
   index printed in the same corner. */
.cnc-dbadge {
    position: absolute;
    right: 1px;
    bottom: 1px;
    min-width: 15px;
    padding: 0 3px;
    border-radius: 5px;
    background: rgba(0, 0, 0, 0.82);
    color: var(--accent);
    font-size: 10px;
    font-weight: 800;
    line-height: 15px;
    text-align: center;
    pointer-events: none;
}
/* The face-down marker a passed-out battle leaves: it counts for nobody. */
.card.cnc-neutral {
    --cw: var(--cnc-cw);
    position: relative;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    filter: saturate(0.3) brightness(0.7);
}
.cnc-nobody {
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 1px 4px 2px;
    background: rgba(0, 0, 0, 0.6);
    border-radius: 4px;
}
.card.cnc-conemark { box-shadow: 0 0 0 2px var(--accent); }
/* --- the diagonal chips --------------------------------------------------- */
.cnc-dhead {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1px;
    padding: 3px 4px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.3);
    border: 1px solid var(--panel-border);
    color: inherit;
    font: inherit;
    cursor: pointer;
}
.cnc-dhead:hover { border-color: rgba(255, 255, 255, 0.4); }
.cnc-dhead.cnc-lit { border-color: var(--accent); background: rgba(255, 209, 102, 0.16); }
.cnc-dpts {
    font-size: 17px;
    font-weight: 800;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}
.cnc-dlead {
    display: inline-flex;
    align-items: baseline;
    gap: 1px;
    font-size: 11px;
    font-weight: 700;
    white-space: nowrap;
}
.cnc-dnone { opacity: 0.45; }
/* A lead held on the sum of the numbers rather than on cards: the count beside the
   suit is level, so without this the chip reads as a tie. */
.cnc-onsum { border-bottom: 1px dotted currentColor; }
/* One line, always present, so lighting a diagonal does not move the board.
   `width: 0` with `min-width: 100%` is the point of it: the board shrink-wraps to
   its content, and a sentence's max-content width is the sentence on ONE line -
   which made the board 604px instead of 372 and pushed the side column onto a
   second row at 1280px. Contributing nothing to the intrinsic width and then
   filling whatever the grid settled on is what keeps the two-column layout. */
.cnc-dline {
    width: 0;
    min-width: 100%;
    margin-top: 8px;
    font-size: 12.5px;
    line-height: 1.35;
    opacity: 0.8;
    min-height: 3.4em;
}
/* --- the battle ----------------------------------------------------------- */
.panel.cnc-battle { padding: 10px 12px; }
.cnc-battle-head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin-bottom: 8px;
}
.cnc-battle-title { font-size: 15px; font-weight: 700; }
.cnc-battle-pts { font-size: 12px; opacity: 0.7; }
.cnc-slots {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    flex-wrap: wrap;
}
.cnc-slot {
    flex: 1 1 96px;
    min-width: 92px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 6px 4px;
    border-radius: 10px;
    border: 1px solid transparent;
    background: rgba(0, 0, 0, 0.2);
}
.cnc-slot.cnc-mine { border-color: rgba(255, 255, 255, 0.28); }
/* The seats that still owe a card, so whose turn it is never has to be read. */
.cnc-slot.cnc-acting { border-color: var(--accent); }
.cnc-slot.cnc-won { background: rgba(255, 209, 102, 0.16); border-color: var(--accent); }
.cnc-slot-name {
    display: inline-flex;
    align-items: baseline;
    gap: 3px;
    font-size: 12.5px;
    font-weight: 600;
    white-space: nowrap;
}
/* One row of cards per seat, and a reserved height so the panel does not grow as
   a battle goes to a scout step and then a playoff. */
.cnc-slot-cards {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 3px;
    min-height: calc(42px * 89 / 64);
}
.cnc-slot-cards .card { --cw: 42px; flex: none; }
.cnc-slot-state {
    font-size: 11px;
    opacity: 0.75;
    text-align: center;
    min-height: 1.3em;
    white-space: nowrap;
}
.cnc-battle-note {
    margin-top: 8px;
    font-size: 12px;
    line-height: 1.35;
    opacity: 0.75;
    min-height: 2.7em;
}
/* --- the controls --------------------------------------------------------- */
.controls.cnc-actions {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    min-height: 52px;
}
.cnc-prompt { font-size: 13px; opacity: 0.85; line-height: 1.35; }
.cnc-hint {
    font-size: 12.5px;
    line-height: 1.35;
    color: var(--accent);
    padding: 6px 9px;
    border-radius: 8px;
    background: rgba(255, 209, 102, 0.1);
}
/* --- the hand ------------------------------------------------------------- */
/* The badges hang off a card corner, so a hand card has to be a containing
   block - scoped to this game's panel, because `#hand-cards .card { position:
   relative }` unscoped makes every card on the site a containing block for its
   own overlays. */
.cnc-handpanel #hand-cards .card {
    --cw: 62px;
    position: relative;
}
@media (max-width: 720px) {
    .cnc-handpanel #hand-cards .card { --cw: 52px; }
    .cnc-grid { --cnc-cw: 50px; }
}
@media (max-width: 430px) {
    .cnc-handpanel #hand-cards .card { --cw: 46px; }
    .cnc-grid { --cnc-cw: 40px; }
}
/* A card that can no longer take the space, once cards are face up. A fact, not
   advice: the King and the Jack never get it. */
.cnc-handpanel #hand-cards .card.cnc-beaten { opacity: 0.45; filter: saturate(0.55); }
/* --- the two cards that are not numbers ----------------------------------- */
/* Drawn in CSS rather than set as a glyph or an emoji: an emoji renders as a
   blank box in headless Chromium and on plenty of Linux setups, and there is no
   "scout" character that is safe in every font. */
.cnc-badge {
    position: absolute;
    right: 2px;
    bottom: 2px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #1a1a1a;
    border: 1.5px solid #f6efe4;
    font-size: 10px;
    font-weight: 800;
    line-height: 13px;
    text-align: center;
    color: #f6efe4;
    pointer-events: none;
}
/* The King fights as 0, which is the useful thing to say about it. */
.cnc-badge-pass { background: #2c6fb0; }
/* The Jack is the scout: an eye, drawn as a lens with a pupil. */
.cnc-badge-scout { background: #1f7a4d; }
.cnc-badge-scout::before,
.cnc-badge-scout::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    border-radius: 50%;
}
.cnc-badge-scout::before {
    width: 11px;
    height: 7px;
    border-radius: 50% / 60%;
    background: #f6efe4;
}
.cnc-badge-scout::after {
    width: 4px;
    height: 4px;
    background: #1f7a4d;
}
/* --- the other players' hands, which are open ----------------------------- */
.cnc-seats { display: flex; flex-direction: column; gap: 6px; }
.cnc-seat {
    padding: 6px 9px;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.18);
    border: 1px solid transparent;
}
.cnc-seat.cnc-mine { border-color: rgba(255, 255, 255, 0.22); }
.cnc-seat-head {
    display: flex;
    align-items: baseline;
    gap: 6px;
    margin-bottom: 4px;
}
.cnc-seat-name { font-size: 13px; font-weight: 600; }
.cnc-seat-count { font-size: 11px; opacity: 0.65; margin-left: auto; white-space: nowrap; }
/* One row that scrolls inside itself rather than wrapping: a wrapping strip is
   the biggest single cause of panels walking down the screen as a game runs. */
.cnc-seat-cards {
    display: flex;
    align-items: center;
    gap: 3px;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: thin;
    min-height: calc(34px * 89 / 64);
}
.cnc-seat-cards .card { --cw: 34px; flex: none; position: relative; }
/* The badges are drawn for a 62px card; at 34px they only need to be smaller. */
.cnc-seat-cards .cnc-badge {
    width: 11px;
    height: 11px;
    right: 1px;
    bottom: 1px;
    border-width: 1px;
    font-size: 8px;
    line-height: 9px;
}
.cnc-seat-cards .cnc-badge-scout::before { width: 7px; height: 5px; }
.cnc-seat-cards .cnc-badge-scout::after { width: 2.5px; height: 2.5px; }
/* --- the score as it stands ------------------------------------------------ */
.panel.cnc-scores { padding: 9px 12px; }
.cnc-scores-title { margin: 0 0 6px; font-size: 14px; }
.cnc-table { border-collapse: collapse; width: 100%; }
.cnc-table th,
.cnc-table td { padding: 2px 6px; font-size: 13px; }
.cnc-table th.cnc-num,
.cnc-table td.cnc-num {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.cnc-table th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.6; }
.cnc-table .cnc-th-name { text-align: left; }
.cnc-trow.cnc-focus { background: rgba(120, 220, 160, 0.1); }
.cnc-total { font-size: 17px; font-weight: 800; }
.cnc-seatbtn {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    padding: 1px 4px;
    border-radius: 6px;
    border: 1px solid transparent;
    background: none;
    color: inherit;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
}
.cnc-seatbtn:hover { border-color: rgba(255, 255, 255, 0.3); }
.cnc-trow.cnc-focus .cnc-seatbtn { border-color: rgba(120, 220, 160, 0.6); }
.cnc-scores-note { margin-top: 6px; font-size: 11.5px; line-height: 1.35; opacity: 0.65; }
/* --- suit glyphs ---------------------------------------------------------- */
/* A seat plays one whole suit, so its glyph is WHO a card belongs to, not
   decoration - which is why it is colored everywhere it appears rather than only
   in the log. The modifier names come from suitTextClass and the colors are the
   shared variables, per the .cardlabel block; the per-suit refinement has to come
   after the family rule, since the two tie on specificity. */
.cnc-suit { font-size: 13px; font-weight: 700; }
.cnc-suit.red { color: var(--red); }
.cnc-suit.red.diamond { color: var(--red-diamond); }
.cnc-suit.club { color: var(--blue-club); }

/* =========================================================================
   Royal Heist (src/royalheist/, pages/royalheist.mjs)

   Diamant on a standard pack. Nobody holds a hand, so there is no hand panel
   and no sort control; the whole board is four blocks, all of them public:
   the danger readout, the run of rooms, the thieves, and the two buttons.
   Everything here is prefixed `rh-`; shared classes (.card, .panel, .btn,
   .controls, .logline, .winner-banner, .result-note, .progress-score) are
   reused as they are.

   THE ANTI-JITTER WORK IS THE POINT OF HALF THESE RULES. A heist gets longer
   one room at a time, and the buttons sit under the run, so anything that
   grows pushes the control the player is reaching for down the screen. The
   run strip therefore never wraps (it scrolls, with one row of height
   reserved), and the status line, the chip row, the controls row and the note
   row all reserve their tallest ordinary shape rather than sizing to their
   current contents. Heights are expressed in terms of the card width and the
   card aspect (89/64 = 1.39) so they follow the card scaling, and every one
   of them is re-derived at the phone breakpoint.
   ========================================================================= */

/* Two lines of narration at any width, so the board below never steps. */
.status.rh-status { min-height: 2.6em; }
.status.rh-turn { color: var(--accent); }

/* --- the danger readout ---------------------------------------------------- */

/* Reserved at the tallest this row gets over a whole game, which is with the
   retired-cards chip on the end: measured at 64px here and 97px on the phone,
   so the reservations are those figures rounded up. Anything less and the row
   grows when the fourth chip appears, which walks the whole board down. */
.rh-danger {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: 66px;
    margin: 0 auto 10px;
    max-width: 900px;
}
.rh-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    padding: 4px 11px;
    border-radius: 999px;
    font-size: 13px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* A wrapping label would make the row taller than the reservation. */
    white-space: nowrap;
}
.rh-chip-key { font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; opacity: 0.6; }
.rh-chip-val { font-weight: 700; font-variant-numeric: tabular-nums; }
/* The two chips that describe how close the heist is to ending light up only
   once there is something to light up for. */
.rh-chip.rh-risk.live { border-color: rgba(255, 140, 120, 0.75); }
.rh-chip.rh-risk.live .rh-chip-val { color: #ff9c86; }
.rh-chip.rh-armed.live { border-color: rgba(255, 200, 90, 0.6); }
.rh-chip.rh-floor .rh-chip-val { color: #7fe3d4; }
.rh-chip.rh-retired { opacity: 0.7; }

/* --- the run -------------------------------------------------------------- */

.rh-run {
    max-width: 900px;
    margin: 0 auto 12px;
    padding: 8px 10px 10px;
    border-radius: 10px;
    background: #006400;
    border: 1px solid rgba(0, 0, 0, 0.35);
    --rh-cw: 58px;
}
.rh-run-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 6px;
}
.rh-run-title { font-size: 14px; font-weight: 700; letter-spacing: 0.03em; }
.rh-run-note { font-size: 12px; opacity: 0.75; white-space: nowrap; }

/* NEVER WRAPS. A wrapping strip is the single biggest source of movement on a
   board like this, because its height tracks how far the heist has got. It
   scrolls sideways instead, and the renderer pins it to the deep end. */
.rh-strip {
    display: flex;
    flex-wrap: nowrap;
    align-items: flex-start;
    gap: 8px;
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 4px;
    /* One row: the card, the bonus sitting lower than it, and the foot. */
    min-height: calc(var(--rh-cw) * 1.39 + 44px);
}
.rh-room {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
/* The stack is as wide as a card plus the part of the bonus that sticks out. */
.rh-roomcards {
    position: relative;
    width: calc(var(--rh-cw) * 1.42);
    height: calc(var(--rh-cw) * 1.39 + var(--rh-cw) * 0.2);
}
.rh-roomcard { --cw: var(--rh-cw); position: absolute; top: 0; left: 0; }
/* Laid ACROSS the diamond, the way it is dealt on a table: down and to the
   right, so both ranks stay readable. */
.rh-roomcard.rh-bonus {
    left: calc(var(--rh-cw) * 0.42);
    top: calc(var(--rh-cw) * 0.2);
    box-shadow: -2px -2px 5px rgba(0, 0, 0, 0.35);
}
.rh-roomfoot {
    display: flex;
    align-items: baseline;
    gap: 5px;
    font-size: 12px;
    white-space: nowrap;
}
.rh-roomvalue {
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: #ffe9a8;
}
/* Jewels still lying in this room, in the floor color the chip above uses. */
.rh-roomleft {
    padding: 0 5px;
    border-radius: 999px;
    background: rgba(127, 227, 212, 0.18);
    color: #7fe3d4;
    font-weight: 700;
}
.rh-roomdanger { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.75; }
.rh-room.rh-danger-room .rh-roomcard { filter: saturate(0.75); }
/* The card that sprang the trap, ringed, because it is the one thing on the
   board a player will go back and look for. */
.rh-room.rh-sprung .rh-roomcard {
    outline: 3px solid #ff6b4a;
    outline-offset: 1px;
    border-radius: 5px;
}
.rh-room.rh-sprung .rh-roomdanger { color: #ff9c86; opacity: 1; }

/* --- the thieves ---------------------------------------------------------- */

.rh-seats {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    max-width: 900px;
    margin: 0 auto 12px;
}
.rh-seat {
    flex: 0 1 150px;
    display: flex;
    flex-direction: column;
    gap: 5px;
    padding: 7px 10px 8px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.rh-seat.rh-mine { border-color: rgba(255, 255, 255, 0.4); }
/* A thief who is out of the palace is out of the decision, so the cell recedes
   rather than disappearing (a cell that vanished would reflow the row). */
.rh-seat.rh-outside { opacity: 0.55; }
.rh-seat-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
}
.rh-seat-name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rh-seat-stash { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1; }
/* Reserved at TWO rows of chips, not one. A thief inside carries two chips (what
   it holds, and whether its fist is closed) which wrap to two lines in a cell
   this narrow, and a thief who is out carries one - so a one-row reservation
   makes the cell change height the moment somebody walks, and the seats row
   change height with it. That is the 59px the decision buttons were moving. */
.rh-seat-body {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    align-items: center;
    align-content: flex-start;
    /* Two chip rows measured at 51px, at both breakpoints. Reserving 47 - which
       looked like "two rows" by arithmetic - left the box 4px short, so it still
       shrank by exactly that when a thief walked out and dropped to one chip. */
    min-height: 52px;
}
.rh-seat .rh-chip { font-size: 12px; padding: 2px 8px; }
.rh-chip.rh-satchel .rh-chip-val { color: #ffe9a8; }
/* A closed fist. What it HOLDS is the one secret in the game, so this only ever
   says that a choice was made - the renderer is what decides whether the
   viewer's own choice is spelled out. */
.rh-chip.rh-fist { opacity: 0.7; }
.rh-chip.rh-fist.ready { opacity: 1; border-color: rgba(255, 255, 255, 0.3); }
.rh-chip.rh-out.caught { color: #ff9c86; border-color: rgba(255, 140, 120, 0.5); }

/* --- press on or get out --------------------------------------------------- */

.rh-actions {
    max-width: 640px;
    margin: 0 auto 12px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    text-align: center;
}
/* Controls and prose in SEPARATE rows, each with its own reservation: a note
   that needs a second line must not push the buttons down, and an empty
   controls row (between heists, when the button is in the lifted summary) must
   not let everything below it jump up. */
.rh-controls {
    display: flex;
    justify-content: center;
    gap: 10px;
    flex-wrap: wrap;
    min-height: 48px;
    align-items: center;
}
.rh-note { font-size: 13px; opacity: 0.8; min-height: 2.6em; }
/* `rh-leave` is the button; `rh-chip.rh-out` below is the badge on a thief who
   is already out of the palace. Two different things, so two names. */
.rh-press { min-width: 150px; }
.rh-leave { min-width: 190px; }
.rh-hint {
    font-size: 13px;
    line-height: 1.45;
    padding: 8px 10px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid var(--panel-border);
    text-align: left;
}
.rh-hintline + .rh-hintline { margin-top: 4px; opacity: 0.85; }

/* --- the between-heists summary and the finish ----------------------------- */

.rh-endbanner { font-size: 18px; }
.rh-summary { margin: 8px auto 0; max-width: 420px; }
.rh-summary-rows { display: flex; flex-direction: column; gap: 3px; }
.rh-summary-row {
    display: grid;
    grid-template-columns: 1fr auto 3.2em;
    align-items: baseline;
    gap: 10px;
    padding: 3px 8px;
    border-radius: 7px;
    font-size: 13px;
    text-align: left;
}
.rh-summary-row.rh-mine { background: rgba(255, 255, 255, 0.08); }
.rh-summary-name { font-weight: 600; }
.rh-summary-took { opacity: 0.85; }
.rh-summary-took.caught { color: #ff9c86; }
.rh-summary-total {
    text-align: right;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

/* --- the phone ------------------------------------------------------------ */

@media (max-width: 620px) {
    /* Every reservation re-derived: the same rules with the desktop figures
       left in place would be the same bug with different numbers. */
    /* Four lines measured at 76px in this column; 4em is 72, which is the sort
       of near-miss that leaves the board stepping by a few pixels a turn. */
    .status.rh-status { min-height: 78px; }
    .rh-danger { min-height: 100px; gap: 6px; }
    .rh-run { --rh-cw: 46px; padding: 6px 7px 8px; }
    .rh-run-note { font-size: 11px; }
    .rh-seat { flex: 0 1 132px; padding: 6px 8px 7px; }
    .rh-seat-stash { font-size: 17px; }
    .rh-note { min-height: 3.9em; }
    /* The two buttons go full width and stack, so the row is twice as tall. */
    .rh-controls { min-height: 104px; }
    .rh-press,
    .rh-leave { min-width: 0; flex: 1 1 100%; }
    /* Name and total on one line, what happened underneath: three columns of
       prose in a narrow phone column would wrap and defeat the alignment. */
    .rh-summary-row { grid-template-columns: 1fr 3.2em; }
    .rh-summary-took { grid-column: 1 / -1; grid-row: 2; font-size: 12px; }
}
/* =========================================================================
   He Will Never Stop (src/hewillneverstop/, pages/hewillneverstop.mjs)
   Horror-movie solitaire played in the hand. Shared classes (.card,
   .card.simple.back, .card.empty, .card.hinted, .panel, .btn, .logline,
   .winner-banner) are reused as-is; everything below is this game's own and
   prefixed `hwns-`.

   The board is the rules sheet's diagram, left to right: the ground He has
   crossed, the hand that is the distance between you, and the terrain ahead.
   The hand carries four separate readings at once, so each has its own visual
   channel and no two share one:
       red ring, sunk down    He takes this card next (cascade included)
       blue bar underneath    part of a descending sequence
       faint dashed edge      may be the card you move (nothing lifted yet)
       gold ring, lifted up   the card you have picked up
   The gold ring is the shared accent's established meaning ("selected"), and
   the live drop gaps are gold too - they are a thin bar rather than a card, so
   the two cannot be confused.
   ========================================================================= */
.hwns-board { max-width: 1000px; margin: 0 auto 10px; }
/* The three zones. The hand is the only one that grows, so it takes the slack
   and the piles keep their intrinsic width. */
.hwns-lane {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
}
.hwns-pile {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    min-width: 76px;
}
/* The pile itself stays a PASSIVE wrapper - cards fly to and from it, and an
   animation anchor should not also be a control - and holds one button. */
.hwns-pilebtn {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 6px 8px;
    border: 1px solid transparent;
    border-radius: 10px;
    background: transparent;
    color: var(--ink);
    font: inherit;
    cursor: pointer;
}
.hwns-pilebtn:hover { background: rgba(255, 255, 255, 0.06); border-color: var(--panel-border); }
.hwns-pilebtn.selected { border-color: var(--accent); background: rgba(255, 209, 102, 0.1); }
.hwns-pile-title {
    margin: 0;
    font-size: 11.5px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.65;
    white-space: nowrap;
}
.hwns-pile-more { font-size: 11px; opacity: 0.6; white-space: nowrap; }
.hwns-pilebtn:hover .hwns-pile-more, .hwns-pilebtn.selected .hwns-pile-more { opacity: 0.95; }
/* --- what is left, and what is gone ------------------------------------- */
.hwns-pile-view { max-width: 1000px; margin: 0 auto 12px; }
.hwns-view-title { margin: 0 0 4px; font-size: 15px; }
.hwns-view-note { font-size: 12.5px; opacity: 0.75; margin-bottom: 8px; }
.hwns-view-cards {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-bottom: 10px;
}
.hwns-view-empty { font-size: 12.5px; opacity: 0.7; }
.hwns-view-cards .card { --cw: 34px; }
/* A count per rank: the table that answers "can I still build this". Nine
   columns, the label plus the eight ranks. */
.hwns-tally {
    display: grid;
    grid-template-columns: auto repeat(8, minmax(0, 1fr));
    gap: 2px 4px;
    max-width: 560px;
    margin-bottom: 8px;
    font-variant-numeric: tabular-nums;
    font-size: 12.5px;
}
.hwns-tally-label { opacity: 0.65; padding-right: 6px; white-space: nowrap; }
.hwns-tally-cell { text-align: center; }
.hwns-tally-cell.head, .hwns-tally-label.head { font-weight: 700; opacity: 0.9; }
.hwns-tally-cell.zero { opacity: 0.35; }
/* A rank with nothing left to hold or draw is a wall: struck through, once,
   across the whole column. */
.hwns-tally-cell.spent { opacity: 0.4; text-decoration: line-through; }
.hwns-view-facts { display: flex; flex-direction: column; gap: 3px; }
.hwns-view-fact { font-size: 12.5px; }
.hwns-view-dead { opacity: 0.8; }
.hwns-pile-cards { display: flex; align-items: center; }
.hwns-pile .card { --cw: 40px; }
.hwns-pile-card:not(:first-child) { margin-left: -26px; }
.hwns-pile-empty { opacity: 0.4; }
.hwns-pile-count { font-size: 11.5px; opacity: 0.7; white-space: nowrap; }
/* min-width: 0 is what lets the strip scroll instead of stretching the lane:
   a flex item's default min-width is its content, so a long hand would push
   the draw pile off the page. */
.hwns-handzone { flex: 1 1 auto; min-width: 0; }
/* The felt tray the hand sits on. IT WRAPS ONTO MORE ROWS, and every row after
   the first is STEPPED IN, which is the hanging-indent trick: the tray is inline
   content with a positive padding-left and an equal negative text-indent, so
   line one starts flush and every line after it begins one indent further along.
   CSS has no selector for "the first card of a wrapped row", and this needs none.

   It used to scroll sideways instead, on the grounds that a wrapping strip walks
   everything below it down the screen. It does, and that is the trade: the hand
   is the whole game here, and a hand that has GROWN is exactly what a player
   needs to read, which scrolling hid. The cost is paid down instead - two rows
   of height are reserved, which holds eighteen cards at this size, so the
   controls under the tray stay put across the ordinary range. */
.hwns-hand {
    display: block;
    text-indent: calc(-1 * var(--hwns-indent));
    padding: 12px 8px 14px;
    padding-left: calc(8px + var(--hwns-indent));
    border-radius: 10px;
    background: #006400;
    border: 1px solid rgba(0, 0, 0, 0.35);
    /* Two rows, plus room for the lifted and the sunk states. */
    min-height: calc(2 * var(--hwns-row) + 22px);
    --hwns-cw: 64px;
    --hwns-row: calc(var(--hwns-cw) * 89 / 64 + 10px);
    /* About half a card, which is what makes the step unmistakable whichever
       way a row happens to break: a row that begins with a gap reads as one
       full indent, one that begins with a card as an indent less a gap. */
    --hwns-indent: 34px;
    line-height: var(--hwns-row);
}
/* Cards and gaps are inline-block, so they form LINES for the indent to act on.
   No whitespace ever reaches the tray - the renderer only appends elements - so
   there is no inter-word space to fight. */
.hwns-hand > * { vertical-align: top; }
/* The size travels on the CARD's own class rather than on the strip, so an
   animation ghost - a clone appended to the body, outside the strip - flies at
   the size it left at instead of falling back to the sheet's base card. */
.card.hwns-card { --cw: var(--hwns-cw, 64px); display: inline-block; }
.hwns-handcaption {
    margin-top: 5px;
    text-align: center;
    font-size: 11.5px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.55;
}
.hwns-handgone { padding: 8px 4px; font-size: 14px; opacity: 0.8; }
/* The key to the three marks. Painted every turn whether or not each mark is
   on screen, so it never arrives mid-game and pushes the board down. */
.hwns-legend {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px 14px;
    margin-top: 5px;
    min-height: 18px;
    font-size: 11.5px;
    opacity: 0.75;
}
.hwns-key { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.hwns-key-mark { width: 14px; height: 9px; border-radius: 2px; }
/* The same left edge the card carries, so the key matches the board. */
.hwns-key-doomed .hwns-key-mark {
    box-shadow: inset 4px 0 0 0 rgba(199, 62, 52, 0.95), 0 0 0 1px rgba(199, 62, 52, 0.5);
}
/* The sequence key is the same thin line the block carries under it. */
.hwns-key-seq .hwns-key-mark {
    height: 2px;
    border-radius: 2px;
    background: rgba(127, 176, 255, 0.75);
}
.hwns-key-liftable .hwns-key-mark { border: 1px dashed rgba(255, 255, 255, 0.6); }
/* Dead ground: the same drained, hatched face the card wears. */
.hwns-key-dead .hwns-key-mark {
    border: 1px solid rgba(255, 255, 255, 0.35);
    background: repeating-linear-gradient(135deg,
        rgba(255, 255, 255, 0.16) 0 2px, transparent 2px 5px);
}
/* --- the drop gaps -------------------------------------------------------
   Painted at every place ALWAYS, at a fixed width, and only live once a card
   is lifted. Adding them on pickup would widen the strip by a gap per card at
   the moment of the click, sliding every card out from under the pointer. */
.hwns-gap {
    display: inline-flex;
    width: 15px;
    height: calc(var(--hwns-cw) * 89 / 64);
    padding: 0;
    border: 0;
    background: transparent;
    align-items: center;
    justify-content: center;
    cursor: default;
}
.hwns-gap-mark {
    width: 3px;
    height: 60%;
    border-radius: 2px;
    background: transparent;
}
.hwns-gap.lit { cursor: pointer; }
.hwns-gap.lit .hwns-gap-mark { background: rgba(255, 209, 102, 0.8); }
.hwns-gap.lit:hover .hwns-gap-mark { background: var(--accent); width: 5px; }
/* A gap the sequence rule refuses: barred rather than blank, so the rule shows
   on the board instead of only in the rules. */
.hwns-gap.blocked { cursor: not-allowed; }
.hwns-gap.blocked .hwns-gap-mark {
    width: 11px;
    height: 3px;
    border-radius: 2px;
    background: rgba(199, 62, 52, 0.85);
}
.hwns-gap.hinted .hwns-gap-mark {
    background: var(--accent);
    width: 5px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}
/* --- the four readings on a hand card -------------------------------------
   EVERY rule below is scoped INSIDE the strip, and that is not tidiness. An
   animation ghost is a clone of the card appended to the BODY, so it carries
   these classes with it: an unscoped `.hwns-card { position: relative }` is one
   class, exactly what `.card-fly { position: fixed }` is, and being later in
   the sheet it would win - which drops every flying clone into document flow
   at the foot of the page and empties the strip while it flies. Scoping to the
   strip means a ghost matches none of it. Same trap as the note at
   .ctl-badged/.ctl-known further up. */
.hwns-hand .hwns-card { position: relative; cursor: pointer; }
/* His reach: sunk toward the discard, and marked down its LEFT edge.
   The edge matters rather than a ring, because the cards of a sequence overlap
   (see below) and each one covers its neighbor's RIGHT side - so a ring on an
   overlapped card is half hidden, while its left edge and rank corner are always
   in view. It also reads the right way round: He comes from the left. */
.hwns-hand .card.hwns-doomed {
    transform: translateY(8px);
    box-shadow: inset 6px 0 0 0 rgba(199, 62, 52, 0.95),
        0 0 0 2px rgba(199, 62, 52, 0.5),
        0 6px 12px rgba(0, 0, 0, 0.4);
}
/* A SEQUENCE IS DRAWN BY PUSHING ITS CARDS TOGETHER, not by decorating them.
   The cards of a run overlap into one tight block and the gaps between them
   close up, which says "these are bound to each other" with the layout instead
   of with a badge - and a badge is what this was, banded top and bottom, which
   read as clutter over the card art. A thin soft line under the block is the
   only ink left, enough to bound the group where two runs sit side by side.

   The overlap is 22px of a 64px card, so every rank corner stays readable. */
.hwns-hand .card.hwns-seq-mid,
.hwns-hand .card.hwns-seq-end { margin-left: calc(var(--hwns-cw) * -0.34); }
.hwns-hand .card.hwns-seq::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: -6px;
    height: 2px;
    background: rgba(127, 176, 255, 0.75);
}
.hwns-hand .card.hwns-seq-start::after { left: 2px; border-radius: 2px 0 0 2px; }
.hwns-hand .card.hwns-seq-end::after { right: 2px; border-radius: 0 2px 2px 0; }
/* Later in the run paints on top, so the overlap reads left to right, the way
   the sequence descends. */
.hwns-hand .card.hwns-seq-mid { z-index: 1; }
.hwns-hand .card.hwns-seq-end { z-index: 1; }
/* Locked: in a sequence, so it is not yours to move. Still clickable, because
   clicking it is how a player finds out why (the status line answers). */
.hwns-hand .card.hwns-seq { cursor: not-allowed; }

/* DEAD GROUND (the hard variant): a card you can never move and He never has to
   pay for. Drained of color and hatched over, so it reads as terrain rather
   than as a card - which is what it is. The hatching is an overlay rather than a
   filter on the face, because `filter` on a card creates a containing block and
   the animation ghosts fly out of one. It sits UNDER the sequence underline and
   over the art, and it never covers the rank corner, since a King and a Joker
   have to be told apart. */
.hwns-hand .card.hwns-dead { cursor: not-allowed; }
.hwns-hand .card.hwns-dead::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: repeating-linear-gradient(135deg,
        rgba(20, 24, 32, 0.55) 0 3px, rgba(20, 24, 32, 0.22) 3px 7px);
    pointer-events: none;
}
/* Sunk a little, like His reach, because it is ground rather than head start:
   it is in the hand without doing the hand's one job.

   The three rules below are ordered, not accidental: they have equal
   specificity, so the last one that matches wins, and a card can be all three.
   Inside a run the block reads LEVEL - a run spans a dead card (7-K-6-5 is a run
   of three over four places), and stepping that one card down would break apart
   a group the rules hold together. His reach wins over both, because what He
   takes next is the thing you most need to see. */
.hwns-hand .card.hwns-dead { transform: translateY(4px); opacity: 0.82; }
.hwns-hand .card.hwns-dead.hwns-seq { transform: none; }
.hwns-hand .card.hwns-dead.hwns-doomed { transform: translateY(8px); }
/* The gaps inside a run collapse with it: there is nowhere to drop a card in
   there, so a barred gap would only be pushing the block apart again. */
.hwns-gap.tight { width: 0; overflow: hidden; }

/* What you could pick up, before you click anything. */
.hwns-hand .card.hwns-liftable {
    outline: 2px dashed rgba(255, 255, 255, 0.4);
    outline-offset: 2px;
}
.hwns-hand .card.hwns-liftable:hover { outline-color: rgba(255, 255, 255, 0.75); }
/* And what you have. */
.hwns-hand .card.hwns-lifted {
    transform: translateY(-10px);
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    z-index: 2;
}
/* --- the search controls -------------------------------------------------- */
.hwns-actions {
    max-width: 1000px;
    margin: 0 auto 12px;
    text-align: center;
}
.hwns-searchlabel {
    font-size: 11.5px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.65;
    margin-bottom: 6px;
}
.hwns-searchrow {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    /* One row of chips is reserved, so gaining or losing a rank cannot move the
       note under it. Eight ranks fit one row well inside the page cap. */
    min-height: 42px;
}
.hwns-rankchip {
    display: inline-flex;
    align-items: baseline;
    gap: 5px;
    padding: 6px 11px;
    border-radius: 9px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
    color: var(--ink);
    font: inherit;
    cursor: pointer;
}
.hwns-rankchip:hover { border-color: var(--accent); }
.hwns-rankchip.hinted {
    border-color: var(--accent);
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}
.hwns-chip-rank { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }
.hwns-chip-count { font-size: 11.5px; opacity: 0.7; }
.hwns-actionnote { margin-top: 6px; font-size: 12.5px; opacity: 0.75; }
/* --- the two lines above the board ---------------------------------------
   Both are shared ids, so both are scoped by a class this game's renderer puts
   on them. Two lines are reserved for the status line because the refusal and
   hint sentences are longer than the prompt, and a third of the width of the
   page is prose either way. */
.status.hwns-status { min-height: 52px; }
.progress.hwns-progress { min-height: 20px; }
/* A pursuit that cascaded, and the trap, are the two log lines worth finding
   again afterwards. */
.hwns-logchase { color: #ff9c86; }
.hwns-logtrap { color: #7fe3d4; font-weight: 600; }
@media (max-width: 720px) {
    .hwns-lane { gap: 6px; }
    .hwns-pile { min-width: 58px; }
    .hwns-pile .card { --cw: 32px; }
    .hwns-pile-card:not(:first-child) { margin-left: -21px; }
    .hwns-hand {
        --hwns-cw: 46px;
        --hwns-indent: 24px;
        padding: 10px 6px 12px;
        padding-left: calc(6px + var(--hwns-indent));
    }
    .hwns-gap { width: 12px; }
    /* The progress line is four facts long, so on a phone it takes two lines
       and both of them have to be reserved. */
    .progress.hwns-progress { min-height: 38px; }
    .status.hwns-status { min-height: 66px; }
}
@media (max-width: 560px) {
    /* The piles go under their titles rather than beside the hand: at this
       width three zones in a row leave the hand no room to be read. */
    .hwns-lane { flex-wrap: wrap; }
    .hwns-handzone { flex: 1 1 100%; order: -1; }
    .hwns-hand { --hwns-cw: 42px; }
    .hwns-pile { flex: 1 1 auto; }
}

/* =========================================================================
   Cover Story (coverstory)
   ONE COLUMN, and the seat chips are the clue board. A clue is a PUBLIC promise,
   so every seat's clues, the tricks buried under them and its plot holes sit in
   that seat's own chip, in front of their owner the way they would on a table.
   They are drawn from the SIMPLIFIED card atlas (`.card.simple`), which is flat
   rank-over-suit art built to stay legible at this size, so a whole plan fits in
   a chip. The trick panel below stays small: only one trick is ever on the table
   and the only things it has to say are which card is taking it and what that
   would bury.
   WHAT THIS REPLACED, because the sizes below are the point of the change. The
   clue board used to be a second wide column, one labeled row per seat, and it
   ate more of the screen than the game it described - mostly on repeating names
   and counting what the cards already say. The counts are gone: a buried clue
   WEARS the trick that buried it, so "2 of 3 buried" is redundant, and the score
   is in #progress-line already.
   THE MARKS ARE THE RULE. `cs-eligible` rings a clue this trick would be allowed
   to bury, which is the one rule a player cannot check by eye: it has three
   routes and one of them (the trick's LEAD suit) is not visible in the winning
   card at all. `cs-winning` rings the card taking the trick, because an Alibi
   never wins however high it looks and a low heart beats an Ace of anything else.
   SCOPING. Every rule here is scoped through a class this game owns, never a
   bare shared id: `.cs-handpanel` for the hand (the page puts it there via
   handPanel()), `.cs-status` and `.cs-scoreboard` for the two panels the SHELL
   owns and the page therefore cannot class. Eight rules naming a shared id
   without a game class shipped across this stylesheet before that convention
   existed, and each one silently resized or re-laid-out all ~85 games.
   ========================================================================= */
.cs-board { margin-bottom: 12px; }
.cs-panelhead {
    font-size: 12px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.6;
    margin-bottom: 6px;
}
/* Two reserved lines on the status line and the scoreboard, so the board and the
   hand never step up and down the screen as the wording changes. Both panels
   belong to the shell, so they are reached through the class the renderer sets. */
.status.cs-status { min-height: calc(2 * 1.45em); }
.progress.cs-scoreboard { min-height: 34px; }
.cs-scorename { opacity: 0.8; margin-right: 5px; }
.cs-scorenum { font-weight: 700; font-variant-numeric: tabular-nums; }
.cs-progress-tricks { font-size: 12px; opacity: 0.65; white-space: nowrap; }

/* ---------- the seat chips, which ARE the clue board ---------- */
.cs-seats {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.cs-seat {
    flex: 1 1 150px;
    min-width: 132px;
    padding: 5px 7px 6px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    background: var(--panel);
}
.cs-seat-mine { border-color: rgba(255, 209, 102, 0.45); }
.cs-seat-turn { box-shadow: 0 0 0 2px var(--accent) inset; }
/* A snitch is the one seat everybody else should be watching: while they are
   still clean they are about to take 10 points and cancel the whole table's
   bonus. It is the one state that cannot be drawn with cards - it IS the absence
   of cards - so it keeps a word. */
.cs-seat-snitch { border-color: rgba(127, 227, 212, 0.5); }
.cs-seat-hole { border-left: 3px solid rgba(255, 156, 134, 0.7); }
.cs-seathead {
    display: flex;
    gap: 6px;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 3px;
}
.cs-seatname { font-size: 12.5px; font-weight: 600; }
.cs-seattag {
    padding: 0 5px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.12);
    font-size: 10.5px;
    white-space: nowrap;
}
.cs-seattag.cs-clean { background: rgba(127, 227, 212, 0.25); color: #bdf5ec; }
/* The cards. One reserved row so a chip does not grow and shrink as clues are
   laid and tricks land, which would walk the trick panel down the screen. */
.cs-seatclues {
    display: flex;
    gap: 5px;
    flex-wrap: wrap;
    align-items: flex-start;
    min-height: calc(34px * 89 / 64 + 6px);
}
.cs-seatclues .card { --cw: 34px; }
.cs-clueslot { position: relative; display: flex; }
/* A buried clue keeps its face - the promise is still on the table, it is just
   settled - and the trick that buried it sits face down BEHIND it, offset up and
   left so both read as one settled unit. That overlap is what replaces a count. */
.card.cs-buried { opacity: 0.6; }
.cs-clueslot .cs-covercard { position: absolute; left: -4px; top: -4px; }
/* THE RULE, stated where it is applied: this clue is one the trick on the table
   would be allowed to bury. */
.card.cs-eligible {
    outline: 2px dashed var(--accent);
    outline-offset: 2px;
    border-radius: 6%;
}
.card.cs-eligible.clickable:hover { outline-style: solid; }
/* A plot hole: the card that took a trick this seat could not bury. Ringed in the
   warning color, and set apart from the clues by a margin, because it is not
   part of the plan - it is what went wrong with it. */
.card.cs-holecard {
    margin-left: 6px;
    opacity: 0.85;
    outline: 2px solid rgba(255, 156, 134, 0.85);
    outline-offset: 1px;
    border-radius: 6%;
}
.card.cs-holecard + .card.cs-holecard { margin-left: 4px; }

/* ---------- the trick ---------- */
.cs-trickpanel { margin-bottom: 10px; }
.cs-trickrow { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-end; }
.cs-trickcell { display: flex; flex-direction: column; align-items: center; gap: 3px; }
/* The viewer's own slot sits a step lower and captions underneath, so "which of
   these is mine" needs no reading. */
.cs-trickcell.cs-mine { margin-top: 14px; }
.cs-trickcap { font-size: 11px; opacity: 0.7; white-space: nowrap; }
.cs-trickcell .card { --cw: 58px; }
.cs-trickcell.cs-next .card.empty { border: 1px dashed rgba(255, 209, 102, 0.6); }
.card.cs-winning {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    border-radius: 6%;
}
/* An Alibi can never win, so it is dimmed wherever it lies in a trick: the
   sprite gives no hint of it, and a Joker looks like the biggest card there. */
.card.cs-alibi-played { opacity: 0.72; }
.cs-tricknote {
    margin-top: 8px;
    font-size: 12.5px;
    opacity: 0.85;
    min-height: calc(2 * 1.4em);
}

/* ---------- the actions: pass, recant, the cover prompt ---------- */
.cs-actions {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
    min-height: 34px;
}
.cs-actionnote { font-size: 12px; opacity: 0.75; }
.cs-prompt { opacity: 1; color: var(--accent); font-weight: 600; }
/* Recant is irreversible and gives up a plan the player has already built, so it
   is deliberately the quieter of the two buttons. */
.cs-recant { opacity: 0.85; }

/* ---------- the hand ---------- */
/* Scoped to this game's own panel class: `#hand-cards` is on every page, so an
   unscoped card size here would resize all ~85 games' hands. */
.cs-handpanel #hand-cards { min-height: calc(var(--cw) * 89 / 64); }
.cs-handpanel .card { --cw: 62px; }
.cs-faded { opacity: 0.42; }

/* ---------- the round summary ---------- */
.cs-result .result-row {
    display: grid;
    grid-template-columns: 108px repeat(var(--cs-cols, 4), minmax(48px, 1fr));
    gap: 4px;
}
.cs-result .result-cell { text-align: center; font-variant-numeric: tabular-nums; }
.cs-result .result-label { font-size: 12.5px; opacity: 0.7; }
.cs-result .totals { font-weight: 700; border-top: 1px solid var(--panel-border); }
.cs-result .cs-won { color: var(--accent); font-weight: 700; }
.cs-snitchnote { color: #bdf5ec; }

/* ---------- prose ---------- */
.cs-suit { font-weight: 700; }
.cs-suitchip { font-weight: 600; }
.cs-cardlabel { font-weight: 600; }
.cs-counttable { border-collapse: collapse; font-size: 13px; }
.cs-counttable th,
.cs-counttable td { padding: 2px 10px 2px 0; text-align: left; }
.cs-counttable .cs-num { text-align: right; font-variant-numeric: tabular-nums; }

@media (max-width: 620px) {
    .cs-trickcell .card { --cw: 46px; }
    .cs-handpanel .card { --cw: 48px; }
    /* The chips go two-up rather than four-across, and the clue cards shrink with
       them - at 28px the simplified art is still a readable rank and pip. */
    .cs-seat { flex: 1 1 calc(50% - 8px); min-width: 0; }
    .cs-seatclues { min-height: calc(28px * 89 / 64 + 6px); }
    .cs-seatclues .card { --cw: 28px; }
    /* Three facts on the status line and a four-seat scoreboard both take two
       lines on a phone, and both have to be reserved. */
    .status.cs-status { min-height: calc(3 * 1.45em); }
    .progress.cs-scoreboard { min-height: 52px; }
    .cs-result .result-row { grid-template-columns: 86px repeat(var(--cs-cols, 4), 1fr); }
    .cs-result .result-label { font-size: 11.5px; }
}
/* ---------- joker jailbreak (solitaire) ---------- */

/*
 * THE BOARD IS THE RULES SHEET'S DIAGRAM: a 3x3 grid, the Joker dead center,
 * walls on the sides, corners on the diagonals.
 *
 * Every track is FIXED, so the fit-content box is a constant size and the
 * centered board never re-centers mid-game (the Cuttle treatment). Card size is
 * driven by --cw on the board itself rather than by a rule on .card, so the
 * reserved stack height below recomputes with it at every breakpoint - and so
 * that nothing here can reach another game's cards.
 */
.jj-board {
    --cw: 84px;
    /* One face-down edge peeking out from under the open card... */
    --jj-edge: 3px;
    /* ...and the deepest of the three, which is what the stack box reserves. */
    --jj-lift: calc(var(--jj-edge) * 3);
    --jj-cardh: calc(var(--cw) * 89 / 64);
    display: grid;
    grid-template-columns: repeat(3, calc(var(--cw) + var(--jj-lift)));
    gap: 12px;
    width: fit-content;
    margin: 10px auto 14px;
}

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

/*
 * A pile, at a FIXED size whatever its depth. The open card sits at the origin
 * and up to three face-down edges fan down and to the right behind it; the
 * badge carries the real count. A stack that grew with its cards would resize
 * the grid row every time a pile shrank, which is the one thing a board the
 * player is aiming at must not do.
 */
.jj-stack {
    position: relative;
    width: calc(var(--cw) + var(--jj-lift));
    height: calc(var(--jj-cardh) + var(--jj-lift));
}
.jj-stack .card { position: absolute; top: 0; left: 0; }
.jj-back {
    top: calc(var(--jj-depth) * var(--jj-edge));
    left: calc(var(--jj-depth) * var(--jj-edge));
}
.jj-open, .jj-empty, .jj-jokercard { z-index: 2; }

/* Cards left in the pile, on the open card's own bottom-right corner. */
.jj-count {
    position: absolute;
    z-index: 3;
    bottom: calc(var(--jj-lift) + 3px);
    right: calc(var(--jj-lift) + 3px);
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    border-radius: 9px;
    background: rgba(0, 0, 0, 0.72);
    color: #fff;
    font-size: 11px;
    line-height: 17px;
    text-align: center;
    font-weight: 600;
}

/* Captions are one short word on purpose: the box is one card wide, and a
   label that wrapped would make the cell taller than what was reserved. The
   whole sentence is the cell's title instead. */
.jj-cap {
    height: 15px;
    line-height: 15px;
    font-size: 11px;
    white-space: nowrap;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.6;
}
/* Walls are the way out and corners are not, which is the single most
   important thing to read off this board, so the two captions differ in
   weight and color rather than only in wording. */
.jj-wall .jj-cap { opacity: 0.9; font-weight: 700; }

/* A cleared wall: the Joker leaves this way as soon as his back is free. */
.jj-openwall .jj-cap { color: var(--accent); opacity: 1; }
.jj-openwall .jj-empty {
    border: 2px solid var(--accent);
    box-shadow: 0 0 12px 2px rgba(255, 209, 102, 0.45);
}

/* The center of the jail, ringed so the eye lands there first. */
.jj-jokercell .jj-stack::before {
    content: '';
    position: absolute;
    inset: -6px;
    border-radius: 12px;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid var(--panel-border);
}
.jj-jokercell .jj-cap { opacity: 0.85; }
/* Cards on his back, counted in the accent that means "this is in your way". */
.jj-burden {
    top: -7px;
    right: calc(var(--jj-lift) - 7px);
    bottom: auto;
    background: var(--accent);
    color: var(--accent-ink);
}

/*
 * THE FADE. Anything that cannot be part of a play right now, given what is
 * already picked, and the Remainder when it cannot feed the Joker. Applied to
 * the whole CELL rather than to the card, because what is unusable is the area:
 * its caption and count are no help either.
 *
 * OPACITY ONLY, NEVER saturation. A faded card still has to read as red or
 * black, because that is the one thing the rules turn on: a player scanning for
 * a red partner is looking at the whole board, including the piles that cannot
 * help this turn, to work out what to set up next. `filter: saturate(0.3)`
 * shipped here first and turned every faded heart gray, so the board lost the
 * red/black distinction exactly where it was being read.
 *
 * The shadow goes instead, which is what makes a faded pile read as recessed
 * rather than merely dim.
 */
.jj-dim { opacity: 0.4; }
.jj-dim .card, .card.jj-dim { box-shadow: none; }

/* Picked cards. .card.sel already rings them; the glow is so a selection stays
   obvious against the faded rest of the board. */
.jj-board .card.sel, .jj-feedrow .card.sel {
    box-shadow: 0 0 12px 3px rgba(255, 209, 102, 0.5);
}

/* ---- his back: the Remainder, and what sits on him ---- */

.jj-feed { width: fit-content; margin: 0 auto 10px; }
.jj-feedrow {
    --cw: 58px;
    --jj-cardh: calc(var(--cw) * 89 / 64);
    display: flex;
    gap: 22px;
    align-items: flex-start;
    justify-content: center;
}
.jj-zone { display: flex; flex-direction: column; align-items: center; gap: 5px; }
.jj-slots { display: flex; gap: 5px; min-height: var(--jj-cardh); align-items: flex-start; }

/*
 * The stack on his back: a box sized for a full stack of three, so it is one
 * constant size whatever is on him and the controls below cannot move. Each
 * card sits at place 0, 1 or 2 from the left, a fan step apart; the TOP card is
 * always at place 2, so the one card in play never moves under the pointer.
 *
 * The fan step shows a covered card's corner index (rank and suit), which is
 * enough to read it: they are face up, so what is under the top is public.
 */
.jj-backstack {
    --jj-fan: calc(var(--cw) * 0.3);
    position: relative;
    width: calc(var(--cw) + 2 * var(--jj-fan));
    height: var(--jj-cardh);
}
.jj-backstack .card {
    position: absolute;
    top: 0;
    left: calc(var(--jj-place) * var(--jj-fan));
}
/*
 * A covered card is out of play, so it is faded like every other unusable
 * card, but by BRIGHTNESS rather than opacity: these overlap, and a
 * translucent card would show the one under it through its face. brightness
 * keeps the hue, so a covered heart still reads red (the saturation lesson in
 * the THE FADE block above). The shadow stays, because it is what separates
 * one overlapped card from the next.
 */
.jj-backstack .card.jj-dim {
    opacity: 1;
    filter: brightness(0.62);
    box-shadow: 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.jj-zonecap {
    height: 15px;
    line-height: 15px;
    font-size: 11px;
    white-space: nowrap;
    opacity: 0.7;
}
.jj-slot { opacity: 0.5; }

/* ---- controls and the running arithmetic ---- */

.jj-tray { width: fit-content; max-width: 100%; margin: 0 auto 12px; }
/*
 * Controls FIRST and in a box of their own, with the note under them. A
 * full-width sentence emitted before a button pushes that button onto a second
 * row, so the primary control would move between phases as the sentence above
 * it changed length. Both boxes reserve their tallest ordinary height.
 */
/* One row of three short buttons, at every width down to 320px: that is what
   the one-word labels bought (see renderTray). Reserved at a button's own
   height, so the box is exact rather than padded. */
.jj-controls {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    min-height: 38px;
}
.jj-note {
    min-height: 40px;
    margin-top: 7px;
    max-width: 400px;
    font-size: 14px;
    line-height: 20px;
    text-align: center;
    opacity: 0.9;
}

/*
 * Two lines of status and one of progress: both change length on every play and
 * both sit directly above the board, so a line gained moves it. Scoped through
 * the classes the renderer adds, because #status-line and #progress-line are on
 * all ~90 pages and an unscoped rule on either would reach every one of them.
 *
 * These are MEASURED, not chosen. tools/jokerjailbreak/jj-measure.mjs drives a whole game in a
 * real browser and reports each box's natural height against its reservation, at
 * every width below. From 400px up the tallest ordinary shape is 50px of status,
 * one line of progress and two lines of note, which is what these reserve.
 */
.jj-status { min-height: 52px; }
.jj-progress { min-height: 20px; }
.jj-good { color: var(--accent); }
.jj-bad { opacity: 0.85; }
.jj-hintnote { color: var(--accent); }

@media (max-width: 560px) {
    /* Card sizes only. The text reservations hold down to 400px, so they are
       NOT re-stated here: this breakpoint and theirs are different widths, and
       lumping them together is how a reservation ends up guessed. */
    .jj-board {
        --cw: 56px;
        --jj-edge: 2px;
        gap: 8px;
    }
    .jj-feedrow { --cw: 46px; gap: 14px; }
    .jj-slots { gap: 4px; }
}

/*
 * Below 400px every one of the three text boxes takes an extra line: the
 * progress line wraps to two, and on the narrowest phones the status recap
 * reaches three and the prompt does too. Measured at 320, 330, 350, 360 and
 * 390px; the crossover for the progress line is between 390 and 400.
 *
 * The slack this leaves at 360 to 399px is the price of the board never moving,
 * and it is the right way round: reserving the typical shape instead would move
 * it on exactly the turns a player is reading the line.
 */
@media (max-width: 399px) {
    .jj-status { min-height: 78px; }
    .jj-progress { min-height: 40px; }
    .jj-note { min-height: 60px; }
}
/* ---------- turning points (solitaire) ---------- */
/*
 * THE TIMELINE IS NINE FIXED COLUMNS, and every one of them paints all four of
 * its boxes on every render (see src/turningpoints/def.ts), so the grid is a
 * constant size from the first turn to the readout. That matters more here than
 * usual: this game has no buttons at all, so every action is a tap on a card, and
 * a board that resized would move the target under the pointer.
 *
 * Card size is driven by --cw on the board itself rather than by a rule on
 * `.card`, so the reserved heights below recompute with it at every breakpoint,
 * and so that nothing here can reach another game's cards.
 *
 * NINE COLUMNS IS WHY THE BOARD CARDS ARE SMALL, and why they use the simplified
 * atlas unconditionally. Nine of anything across a phone is about 34px a column,
 * and the full art is a smear at that size whatever resolution it comes from; so
 * rather than switch atlas at a breakpoint (which a class on the element cannot
 * do), the board is sized to sit inside the simplified band - 46px down to 32px -
 * at every width. The HAND is the thing you pick up, so it keeps the full art and
 * the platform's own fluid size. See .kiro/steering/ui-conventions.md.
 */
.tp-board {
    --cw: 46px;
    --tp-cardh: calc(var(--cw) * 89 / 64);
    display: grid;
    grid-template-columns: repeat(9, var(--cw));
    gap: 8px;
    width: fit-content;
    /* Room for the caption to spill. A caption is centered and `nowrap`, and
       "Unresolved" is wider than a 46px column, so it overhangs its track into
       the gaps; without this the overhang would also stick out of the
       fit-content box at the two ends and widen the board past its own tracks. */
    padding-inline: 8px;
    margin: 10px auto 8px;
}
.tp-col {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
/* The moment number, which is also the column's heading for the tie rule. */
.tp-num {
    font-size: 11px;
    font-weight: 700;
    opacity: 0.55;
    line-height: 14px;
    min-height: 14px;
}
.tp-holdbox, .tp-slotbox {
    width: var(--cw);
    height: var(--tp-cardh);
}
.tp-card { width: var(--cw); height: var(--tp-cardh); }
/* The hold and the commitment read as one column, so the gap between them is
   smaller than the gap between columns. */
.tp-slotbox { margin-top: 1px; }
/* An empty slot is a dashed outline rather than a card back: nothing is there
   yet, and a back would read as a card you cannot see. */
.tp-slot, .tp-slotbtn {
    border: 1px dashed rgba(255, 255, 255, 0.28);
    background: rgba(255, 255, 255, 0.03);
}
.tp-slotbtn {
    border-color: var(--accent);
    border-style: solid;
    box-shadow: 0 0 0 1px var(--accent) inset;
}
.tp-coverbtn { box-shadow: 0 0 0 2px var(--accent); }
/* A column with something to tap in it. */
.tp-live .tp-num { opacity: 1; color: var(--accent); }
.tp-hinted .tp-num { opacity: 1; }
.tp-hinted .tp-card { outline: 3px solid var(--accent); outline-offset: 1px; }

/*
 * THE CAPTION IS RESERVED AT TWO LINES, ALWAYS. It fills in with a column's
 * verdict as soon as both its cards are known and gains the tie chain at the end,
 * so its content changes on most turns; reserving both lines from the first render
 * is what stops the hand walking down the screen as the board fills in.
 */
.tp-cap {
    min-height: 28px;
    /* Exactly the column, so the grid track is what decides the board's width.
       A longer word overhangs into the gap rather than widening the column. */
    width: var(--cw);
    text-align: center;
}
.tp-verdict {
    font-size: 10px;
    font-weight: 700;
    line-height: 13px;
    /* A long word wrapping would make the box taller than what is reserved. */
    white-space: nowrap;
}
.tp-chain {
    font-size: 9px;
    line-height: 13px;
    opacity: 0.65;
    white-space: nowrap;
}
.tp-blank { opacity: 0.25; font-weight: 400; }
.tp-good { color: var(--accent); }
/* --red doubles as the platform's warning color, which is what a hold standing
   is: the one column that ends the game. */
.tp-bad { color: var(--red); }
.tp-tie { color: #d9b25c; }

/*
 * A MOMENT BEING TURNED OVER FADES IN PLACE, and does not arrive.
 *
 * The shared `.card-enter` pop translates a new card 8px down and scales it from
 * 0.9, which is right for a card that came from somewhere. An uncovered moment
 * came from nowhere: it was lying in that exact slot the whole time, face down.
 * def.ts turns the shared entrance off (`enterAnimation: 'none'`) and adds this
 * class instead, so the only thing that changes is which side of the card shows.
 *
 * Short on purpose. It is feedback that the tap landed, not an effect.
 */
.tp-reveal { animation: tp-reveal 170ms ease-out; }
@keyframes tp-reveal {
    from { opacity: 0; }
    to { opacity: 1; }
}
/* Mirrors the platform's own idiom for every other card animation. def.ts also
   checks the STORED preference in JS, which this query cannot see. */
body.motion-off .tp-reveal { animation: none; }

/*
 * The cycle, which is the one rule that is not printed on the cards. Content
 * sized, so it needs nothing to center.
 */
.tp-cycle {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    font-size: 13px;
    min-height: 24px;
    margin-bottom: 10px;
}
.tp-kind {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    border: 1px solid rgba(255, 255, 255, 0.16);
    border-radius: 999px;
    white-space: nowrap;
}
.tp-glyph { font-size: 15px; line-height: 1; }
.tp-kindname { font-weight: 600; }
.tp-arrow { opacity: 0.5; font-size: 15px; }
/* The chip that closes the cycle carries only the glyph it wraps back to. */
.tp-wrap { opacity: 0.55; }

/*
 * The draw pile beside the step note. TWO BOXES, not one flow: the note is the
 * only prose above the hand, and a sentence wrapping to a second line inside a
 * shared row would move the pile with it.
 */
.tp-tray {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: 16px;
    margin: 0 auto 10px;
    max-width: 680px;
}
.tp-stock { display: flex; flex-direction: column; align-items: center; gap: 4px; }
/* A FIXED single edge behind the top card, with the caption carrying the real
   count, so the pile is the same size at eight cards as at one. */
.tp-stockstack { position: relative; width: 58px; height: 78px; }
.tp-stockstack .card { position: absolute; top: 0; left: 0; --cw: 52px; }
.tp-stockedge { top: 4px; left: 5px; }
.tp-stockcard { z-index: 2; }
.tp-stockcap { font-size: 11px; opacity: 0.7; white-space: nowrap; }
.tp-spent .tp-stockcap { opacity: 0.45; }
/* Reserved at three lines, which is the tallest the note gets at this width. */
.tp-note {
    flex: 1 1 auto;
    min-height: 54px;
    font-size: 14px;
    opacity: 0.85;
    align-self: center;
}

/*
 * There is no controls row and nothing on the board is ever armed for selection.
 * Both existed for the Final swap ending, which carried a "Resolve as it stands"
 * button and let you tap two committed cards to trade them. That ending is gone,
 * replaced by No chaining, which changes only how far a tie may look and so needs
 * no control of its own. See Rules in src/turningpoints/engine.ts.
 */

/* The hand: the one place on this board that keeps the full card art. */
.tp-handpanel #hand-cards { gap: 10px; }
.tp-hand.sel { outline: 3px solid var(--accent); outline-offset: 2px; }
.tp-idle { opacity: 0.75; }

/*
 * What has not been seen yet: public knowledge, and the game's card counting made
 * visible (the reasoning is in def.ts).
 *
 * ONE ROW THAT SCROLLS. It runs from seventeen cards down to one, and a wrapping
 * row would change height as it shrank - the most reliable way there is to make
 * the board above it move. Deliberately NOT centered: a centered row with a
 * horizontal overflow hides its left end behind an edge the pointer cannot reach.
 */
.tp-unseen {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 4px;
    min-height: 42px;
    padding-bottom: 2px;
}
/* TWO classes, so it beats `.card.mini`'s 46px. It carried `mini` once and this
   rule lost to it silently, which rendered the strip at board size. */
.card.tp-unseencard { --cw: 28px; flex: 0 0 auto; opacity: 0.85; }
#tp-unseen-title { font-size: 13px; opacity: 0.75; }

@media (max-width: 900px) {
    .tp-board { --cw: 40px; gap: 6px; }
    .tp-note { min-height: 60px; font-size: 13px; }
}
@media (max-width: 560px) {
    /*
     * THE STATUS LINE IS RESERVED AT TWO LINES HERE, and it is the one
     * reservation on this page that was found by measuring rather than by
     * reading. The recap ("You sent 4C to moment 7, against 2D: the hold
     * stands.") is one line on a desktop and two at 390px, and the platform's
     * 26px default let it grow - which moved the board and everything under it
     * 25px down the screen on most turns. Scoped through .tp-status, the class
     * renderStatus puts on the box: #status-line is on all 95 pages.
     *
     * 52px rather than 50: two lines measure 50.38px when the sentence carries a
     * card label, and a reservation UNDER the natural height is not a reservation
     * - min-height does not cap anything, so the box grew the 0.38px and rounded
     * into a 1px shift of everything below it on most turns.
     */
    .status.tp-status { min-height: 52px; }
    .tp-board { --cw: 34px; gap: 4px; }
    .tp-cap { min-height: 26px; }
    .tp-verdict { font-size: 9px; line-height: 12px; }
    .tp-chain { font-size: 8px; line-height: 12px; }
    .tp-cycle { font-size: 12px; gap: 4px; }
    .tp-kind { padding: 2px 6px; }
    .tp-tray { gap: 10px; }
    .tp-stockstack { width: 46px; height: 62px; }
    .tp-stockstack .card { --cw: 41px; }
    /* Four lines at this width: the commit note is the long one. */
    .tp-note { min-height: 72px; }
    .card.tp-unseencard { --cw: 24px; }
    .tp-unseen { min-height: 36px; }
}
@media (max-width: 399px) {
    .tp-board { --cw: 32px; gap: 3px; }
}
/* ---------- wolf (Lobo, solitaire) ---------- */
/*
 * THE TABLE IS THE RULES SHEET'S DIAGRAM, top to bottom: the Wolf's hand, the
 * deck strip in the middle, your hand, then the controls.
 *
 * Two things this file is careful about, both from
 * .kiro/steering/ui-conventions.md. Everything is CENTERED on the board axis, in
 * one fixed-width column, so nothing re-centers as a hand shrinks. And every box
 * whose contents change size RESERVES its ordinary height - the status line, the
 * readout, the controls row, and each hand row - because the one thing a player
 * reaching for a card cannot forgive is the card moving on the way.
 *
 * The Wolf's hand is the interesting case, because it has no upper bound: an Over
 * deals it the difference in ranks, so it can reach a dozen cards. Its cards are
 * therefore drawn a little smaller than yours (still well clear of the 46px
 * simplified-atlas threshold, so they stay full art), which fits about nine on a
 * row. One row is reserved; a second appears only once the round has gone badly
 * enough to be worth noticing.
 */

/*
 * The recap of the last play, then what to do now. Reserved at the tallest
 * ordinary shape: one line each here, two each at the phone breakpoint. Both
 * sentences are written to a bounded length for exactly this reason (see
 * recapText in def.ts), so the box can be reserved rather than tracked.
 */
.status.wlf-status { min-height: 3.4em; }
.wlf-recap { display: block; font-size: 14px; font-weight: 500; opacity: 0.72; }
.wlf-ask { display: block; }
.wlf-bad { color: var(--red); }

.wlf-scorenote { font-size: 12px; opacity: 0.6; }

.wlf-board {
    --wlf-cardh: calc(var(--cw) * 89 / 64);
    /* The Wolf's own card size: smaller than yours, because there can be a lot
       of them. Kept above 46px so the full art still reads (see the
       simplified-atlas thresholds in the steering). */
    --wlf-themcw: 70px;
    --wlf-themcardh: calc(var(--wlf-themcw) * 89 / 64);
    width: 100%;
    max-width: 760px;
    margin: 0 auto 12px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.wlf-side {
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    padding: 8px 10px 10px;
}
/* Whose row it is, at a glance and without reading the label. */
.wlf-them { border-color: rgba(232, 130, 118, 0.42); }
.wlf-mine { border-color: rgba(255, 209, 102, 0.38); }
.wlf-sidehead {
    display: flex;
    align-items: baseline;
    justify-content: center;
    flex-wrap: wrap;
    gap: 4px 10px;
    margin-bottom: 4px;
}
.wlf-sidename { font-size: 15px; font-weight: 700; white-space: nowrap; }
.wlf-sidefacts { font-size: 13px; opacity: 0.72; white-space: nowrap; }
.wlf-cards {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    gap: 8px;
    /* The 5px .card.sel lift, so picking a card cannot grow the row. */
    padding-top: 5px;
    min-height: calc(var(--wlf-cardh) + 5px);
}
/*
 * YOUR hand cannot wrap: it starts at four cards and every capture spends one,
 * so it only ever shrinks. The WOLF's has no upper bound, and letting it wrap
 * measured 144px of movement in the controls below it at 390px wide - a card
 * strip whose height tracks how far the round has got is the biggest single
 * source of that (see .kiro/steering/ui-conventions.md). So it scrolls inside a
 * row of fixed height instead: the row is one card tall for ever, whatever the
 * Wolf is holding, and the head above it carries the count and the total.
 *
 * `safe center` is the whole reason this can be centered at all. Plain
 * `justify-content: center` on a scroll container puts the overflow behind an
 * unreachable left edge, which is why the platform's own `.cardrow.scroll` stays
 * left-aligned; `safe` falls back to flex-start exactly when the content
 * overflows, so a short hand centers on the board axis and a long one stays
 * reachable. A browser that does not know the keyword drops the declaration and
 * gets flex-start, which is the non-clipping behavior anyway.
 *
 * Not scrolled to either end on purpose: the low ranks answer a Split and the
 * high ones decide whether to fold, so neither end is the one to pin.
 */
.wlf-them .wlf-cards {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: thin;
    justify-content: safe center;
    min-height: calc(var(--wlf-themcardh) + 5px);
}
.wlf-them .wlf-cards .card { --cw: var(--wlf-themcw); flex: none; }

/* The deck strip: what is face down, the face-up cub, and the discard. */
.wlf-mid {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: 18px;
}
.wlf-pile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.wlf-pilecap {
    font-size: 12px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.6;
    white-space: nowrap;
}
.wlf-pilecount {
    font-size: 12px;
    opacity: 0.72;
    white-space: nowrap;
    min-height: 1.2em;
}
.wlf-mid .card { --cw: 66px; }
/* The cub is the card the next draw hands over, so it is the one to look at. */
.wlf-cub .card:not(.empty) { box-shadow: 0 0 0 2px var(--accent); border-radius: 6px; }

/*
 * A card that cannot be part of any capture from here. Faded rather than hidden,
 * because which of your cards is dead is itself information.
 */
.card.wlf-dim { opacity: 0.32; }

/* The readout, the controls, and the note. Three boxes, each with its own
   reserved height, controls in the middle so neither the sentence above nor the
   one below can move the Capture button. */
.wlf-tray {
    width: 100%;
    max-width: 760px;
    margin: 0 auto 8px;
    text-align: center;
}
.wlf-readout {
    min-height: 2.8em;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 15px;
    padding: 0 4px;
}
.wlf-readout.wlf-good { font-weight: 700; color: var(--accent); }
.wlf-controls {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px;
    min-height: 36px;
}
.wlf-note { min-height: 2.6em; margin-top: 6px; font-size: 13px; opacity: 0.72; }

/* The round summary: two columns, you and the Wolf. */
.result-table.wlf-result .result-row { grid-template-columns: 96px 1fr 1fr; }
.wlf-minirow { font-size: 15px; }
.wlf-minipip { font-weight: 800; }
.wlf-mininone { opacity: 0.6; font-style: italic; }
.wlf-summary .result-row.wlf-held { align-items: baseline; }

@media (max-width: 720px) {
    .status.wlf-status { min-height: 5.4em; }
    .wlf-board { --wlf-themcw: 48px; }
    .wlf-mid { gap: 10px; }
    .wlf-mid .card { --cw: 52px; }
    .wlf-cards { gap: 5px; }
    .wlf-readout { min-height: 3.8em; }
    /*
     * ONE row, and short enough to stay one. The three controls reserved TWO rows
     * here for a while, because the Fold button carries the Wolf's total and its
     * width changes with it, so the row flipped between one line and two as the
     * number grew: reserving the taller shape stopped the jitter and left a
     * 88px-tall block of buttons on a phone, which read as the controls
     * dominating the screen. Tightening them and shortening the Fold label fits
     * all three on one line at 390px with a three-digit total, so the
     * reservation is one row again.
     */
    .wlf-controls { min-height: 32px; gap: 6px; }
    .wlf-controls .btn { padding: 5px 10px; font-size: 14px; }
    .wlf-note { min-height: 3.6em; }
    .result-table.wlf-result .result-row { grid-template-columns: 78px 1fr 1fr; }
}
/* =========================================================================
   Bubble Gum (bubblegum)

   Layout intent: three quiet panels in a row - the trump card, the trick, and
   the two bubbles - and NOTHING above the hand may change height mid-round.
   Everything that grows during a round is a one-row scroll strip with a
   reserved height: both bubble rows and the memory line under each bubble. The
   note strip sits INSIDE the hand panel so a throwaway prompt appearing never
   moves the cards.

   The ONE deliberate exception is the blown-away row, which wraps to three
   rows before it scrolls - a pop empties a whole bubble and a single row hid
   most of it. Its own comment carries the reasoning.

   Every class here is prefixed bgm-, and the two rules that reach a shared id
   (#hand-cards inside .bgm-handrow, and .progress-score) are scoped through a
   class this game owns, per .kiro/steering/ui-conventions.md.
   ========================================================================= */

/* TWO lines' worth, but only where two lines are actually needed.

   One message is far longer than the rest, the one explaining a discard: "You
   lost the trick, so 5♠ is now in your hand. Discard any card face down." Every
   other line this game writes is under half its length, and a hint (which takes
   the status line over) is shorter still. Setting each candidate string into the
   real element with the reservation lifted and sweeping the width found it wraps
   at 645px and fits on one line from 650px up, so:

     - at 660px and below, reserve the two lines (2.9em at 18px is 52px, against
       a measured 50px), or the board and the hand step down the screen every
       time a trick is lost;
     - above it, reserve nothing. The shared .status already holds 26px, which
       covers the one line, and the 2.9em that used to apply at every width left
       27px of dead air under a single line of text on every desktop.

   660px carries a 15px margin over the measured crossover. The old phone value
   of 4.4em was three lines' worth for a message that never reaches three. */
@media (max-width: 660px) {
    .status.bgm-status { min-height: 2.9em; }
}

/* --- the match tracker: rounds won, in the shared scoreboard widget ------- */

.bgm-scorename {
    font-size: 12px;
    font-weight: 600;
    opacity: 0.75;
    white-space: nowrap;
}
.bgm-scorenum {
    font-size: 20px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.bgm-scoretarget { font-size: 12px; opacity: 0.55; }
/* Whoever is ahead on rounds. The shared widget colors the number for the
   games listed in platform.css; this game opts in from its own file. */
.progress-score.leading .bgm-scorenum { color: var(--accent); }
/* "Trick 4 of 10" - a quiet counter beside the round wins. */
.bgm-progress-tricks {
    font-size: 13px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.6;
}

/* --- the board: trump · trick · bubbles ---------------------------------- */

.bgm-board {
    display: flex;
    gap: 12px;
    align-items: stretch;
    flex-wrap: wrap;
    margin-bottom: 12px;
}
.bgm-trumpbox,
.bgm-trickbox,
.bgm-bubblebox {
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
/*
 * `min-width: 0` ON ALL THREE, AND IT IS LOAD-BEARING.
 *
 * A flex item's default minimum main size is its MIN-CONTENT width, so a box
 * whose content gets wider mid-round cannot be shrunk past it and the row wraps
 * instead - which doubles the board's height and walks the hand 330px down the
 * screen. Measured: at 1280px the three boxes came to exactly the board's width,
 * so the first time the bubble panel's own content grew (the memory line filling
 * up as cards changed hands) the whole panel dropped onto a second row.
 *
 * With the minimum released, wrapping is decided by the fixed bases alone and
 * nothing a round does can change it. Everything inside is already prepared to
 * be narrowed: the card strips scroll, the head and memory lines clip, and the
 * caption wraps. The bases are also kept comfortably under the board's width so
 * the flex-grow shares out real slack rather than fighting for it.
 */
.bgm-trumpbox { flex: 0 1 180px; min-width: 0; }
.bgm-trickbox { flex: 1 1 230px; min-width: 0; }
.bgm-bubblebox { flex: 2 1 320px; min-width: 0; }
.bgm-boxtitle {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    margin-bottom: 6px;
}

/* The trump suit IS this box's heading, sitting over a centered card, so there
   is no separate "Trump" label to read past. It replaces .bgm-boxtitle here,
   which is why it carries its own bottom margin. */
.bgm-trump {
    font-size: 14px;
    font-weight: 700;
    white-space: nowrap;
    text-align: center;
    margin-bottom: 6px;
}
.bgm-trump-row { display: flex; justify-content: center; }

/* The cards a pop blew up: public, and the only way to account for the deck.

   THREE ROWS DEEP BEFORE IT SCROLLS. One pop empties a whole bubble - eight or
   nine cards - and at this column's width a single non-wrapping row showed
   about four of them with the rest parked off the edge behind a scrollbar,
   which is the worst possible place for the one record of what has left the
   deck. So the row wraps, and only scrolls once it is three rows deep (one
   full pop's worth, plus room to spare).

   This RELAXES the file's one-row rule at the top, deliberately and only here:
   the box grows by a row the first time a pop overflows one. A pop is the
   loudest event in the game and the player is looking straight at this row
   when it happens, so a shift there is expected rather than surprising - and a
   round with no pop, which is the common one, never moves at all. The floor is
   still one row's worth, so an empty round reserves exactly what it did before.

   Deliberately NOT centered: centering a scroll container clips its overflow
   behind an edge the player cannot reach (see the centering rule's exceptions
   in .kiro/steering/ui-conventions.md). The title centers with the rest of the
   box; the row does not. */
.bgm-poppedbox { margin-top: 10px; }
.bgm-poppedtitle {
    text-align: center;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.5;
    margin-bottom: 4px;
    white-space: nowrap;
}
/* The card height PLUS the 8px of vertical padding (6px top, 2px bottom, for
   the lift a selected card takes - the allowance .cardrow.scroll used to carry
   and this row now keeps itself). Everything here is border-box, so a
   reservation that names only the card measured 8px short and the row grew by
   exactly that the first time a card landed in it.

   TWO CLASSES, so these beat .cardrow.mini's 4px gap rather than losing to it
   on specificity (this file is concatenated after platform.css, so an equal
   count wins on order). The old one-class rule silently lost that fight. */
.cardrow.bgm-poppedrow {
    padding: 6px 0 2px;
    gap: 3px;
    /* One row floor; three rows, then scroll. The row height is the card
       height (--cw 30px at the 64x89 sprite ratio) and two 3px gaps sit
       between three rows. */
    min-height: calc(30px * 89 / 64 + 8px);
    max-height: calc(3 * (30px * 89 / 64) + 2 * 3px + 8px);
    overflow-x: hidden;
    overflow-y: auto;
    scrollbar-width: thin;
}
.bgm-poppedrow .card { --cw: 30px; }

/* The trick: a gutter of seat names, one card cell per seat, and a flag
   column - a fixed grid, so nothing shifts as cards land. */
.bgm-trick-grid {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 10px;
    row-gap: 6px;
}
.bgm-seat {
    justify-self: end;
    font-size: 13px;
    font-weight: 600;
    opacity: 0.7;
    white-space: nowrap;
}
.bgm-cell { display: flex; justify-content: center; }
/* Stagger the leader's card left and the answer right, so who led reads at a
   glance even before the flag is noticed. The offset sits on the CELL rather
   than on the card because the FLIP animator drives the card's own transform,
   and an inline transform would win and then snap 8px on landing.

   With nothing on the table there are no two cards to contrast, and the same
   offset applied to a pair of dashed placeholders just reads as bad alignment,
   so .bgm-clear turns it off. Nothing moves in layout either way: a transform
   is paint, so the slots cannot shove the caption or the hand. */
.bgm-cell.bgm-led { transform: translateX(-8px); }
.bgm-cell.bgm-follow { transform: translateX(8px); }
.bgm-trick-grid.bgm-clear .bgm-cell { transform: none; }

/* The card that took the trick, ringed for as long as the settle beat keeps
   the pair on the table (see TRICK_HOLD_MS in def.ts). Outline only, on
   purpose: it is painted on a card that is about to become a flying ghost, and
   a rule carrying `transition` or `position` there would fight .card-fly for
   the cascade - the bug ghostStripClasses exists to fix. */
.bgm-cell .card.bgm-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}
.bgm-flag {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    min-width: 52px;
}

/* TWO lines, reserved at every width. The caption is one line while a trick is
   in progress ("Trick 1: Player 2 to answer.") and two once it settles ("Trick 6
   to Player 2: 2♠ took the bubble to 15 and it popped."), and measurement with
   the reservation lifted found two to be the maximum from 390px to 1440px: even
   in the widest column the settled line wraps once, and in the narrowest it
   still only wraps once. Two lines at line-height 1.4 is 2.8em. Anything less
   and the board steps down the screen the moment a trick settles, which is
   exactly when the player is reaching for their next card. */
.bgm-caption {
    margin-top: 8px;
    font-size: 13px;
    opacity: 0.85;
    line-height: 1.4;
    min-height: 2.8em;
}

/* --- the bubbles: the tracker the whole game is steered by ---------------- */

.bgm-bubble {
    padding: 8px 10px;
    border-radius: 10px;
    border: 1px solid transparent;
    background: rgba(255, 255, 255, 0.04);
    margin-bottom: 8px;
}
.bgm-bubble:last-child { margin-bottom: 0; }
/* Whoever holds the bigger bubble is winning the round right now. */
.bgm-bubble.bgm-ahead { border-color: var(--accent); }
/* One card of some size would burst this one. */
.bgm-bubble.bgm-danger { background: rgba(214, 69, 65, 0.14); }

.bgm-bubble-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 5px;
}
.bgm-bubble-name { font-size: 13px; font-weight: 700; white-space: nowrap; }
.bgm-bubble-total {
    font-size: 22px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.bgm-ahead .bgm-bubble-total { color: var(--accent); }
.bgm-bubble-room { font-size: 12px; opacity: 0.75; white-space: nowrap; }
.bgm-danger .bgm-bubble-room { color: #ffb4b4; opacity: 1; font-weight: 700; }
/* The card count, which is the round's first tie-break. */
.bgm-bubble-cards {
    margin-left: auto;
    font-size: 11px;
    opacity: 0.55;
    white-space: nowrap;
}

/* How full the bubble is, against the 14 it may hold. */
.bgm-gauge {
    height: 6px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.1);
    overflow: hidden;
    margin-bottom: 6px;
}
.bgm-gauge-fill {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: var(--accent);
}
.bgm-danger .bgm-gauge-fill { background: #d64541; }

/* The score cards themselves. One row, reserved, scrolling: a bubble can hold
   eight or nine small cards before it bursts, and a wrapping row would walk
   the hand down the screen as the round ran. */
.bgm-bubble-row {
    min-height: calc(34px * 89 / 64 + 8px);   /* + .cardrow.scroll's padding */
    gap: 3px;
}
.bgm-bubble-row .card { --cw: 34px; }

/* The one memory line per bubble: what you threw away (under your own), and
   what the opponent may still hold of yours (under theirs). Card LABELS, not
   faces, so the line's height is fixed whatever it holds. */
.bgm-bubble-note {
    display: flex;
    align-items: baseline;
    flex-wrap: nowrap;
    gap: 5px;
    margin-top: 5px;
    min-height: 1.5em;
    overflow: hidden;
    font-size: 12px;
}
.bgm-notelabel {
    opacity: 0.55;
    white-space: nowrap;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.bgm-bubble-note .cardlabel { font-size: 12px; }

/* --- the hand panel: cards, with the note strip beneath ------------------- */

/*
 * A ten-card hand is the widest thing on the page, so it gets the panel's
 * whole width and the notes sit in a reserved strip UNDERNEATH. Appearing
 * content still never moves the cards: it is below them, and the strip
 * reserves its height.
 */
.bgm-handrow {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.bgm-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    min-height: 34px;
}
.bgm-actions-note { font-size: 12px; opacity: 0.75; line-height: 1.4; }

/*
 * DISCARD MODE. The one moment a tap on a card does not play it, so it must
 * not look like the moment that does. Three cues, all set by def.ts only while
 * the viewer really can discard (never during the settle beat):
 *
 *   - the note under the cards is set large and in the accent, where the play
 *     notes are 12px small print;
 *   - the hand panel takes the accent border (a COLOR change on the border the
 *     panel already has, so nothing moves);
 *   - the trick box goes idle: its empty slots fade, because nothing the player
 *     can do now puts a card there (.bgm-idle).
 *
 * The note sits below the cards, so the extra line it takes on a phone grows
 * the panel downwards and never moves the hand.
 */
.bgm-actions-note.bgm-discard-note {
    font-size: 16px;
    font-weight: 700;
    line-height: 1.35;
    opacity: 1;
    color: var(--accent);
}
.panel.bgm-handpanel.bgm-discarding { border-color: var(--accent); }
.bgm-trickbox.bgm-idle .card.empty { opacity: 0.3; }

/* The settle beat's 'pop' stage: the bubble at its bursting total, for a moment
   before its cards fly to the blown-away row. Border and color only, and the
   bubble row already carries a transparent border for it, so nothing moves. */
.bgm-bubble.bgm-bursting { border-color: #d64541; }
.bgm-bubble.bgm-bursting .bgm-bubble-total { color: #ff8a86; }

/* A card the follow rule excludes right now: visible, inert, dimmed. */
.card.bgm-mute { opacity: 0.45; }

/* The card the trick just put in this hand, marked while the discard is owed:
   the hand grew a card the player did not choose to hold, so it says which. */
.card.bgm-fresh {
    outline: 2px dashed var(--accent);
    outline-offset: 2px;
}

/* The public-knowledge eye: they played this card face up and handed it over,
   so they know you hold it. TWO classes, so it out-specifies .card-fly's
   position: fixed on an animation ghost - which is also why `bgm-badged` is
   named in ghostStripClasses (see def.ts's animate). */
.card.bgm-badged { position: relative; }
.bgm-eye { color: rgba(255, 226, 160, 0.95); }

/* --- the log: one TABLE ROW per trick ------------------------------------- */

/*
 * Four fixed columns - trick number, winner, the two cards, the bubble - so a
 * round reads as ten aligned rows rather than as thirty lines of prose. See the
 * note on logLine in def.ts for why. `nowrap` on the row is what keeps a row a
 * row: without it a narrow log column wraps the bubble figure under the cards
 * and the alignment is gone.
 */
.logline.bgm-logtrick {
    display: flex;
    align-items: baseline;
    flex-wrap: nowrap;
    gap: 6px;
    font-size: 13px;
}
.bgm-logno {
    flex: none;
    min-width: 1.4em;
    text-align: right;
    font-variant-numeric: tabular-nums;
    opacity: 0.45;
}
.bgm-logwho {
    flex: none;
    min-width: 4.2em;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.bgm-logcards {
    flex: 1 1 auto;
    display: flex;
    align-items: baseline;
    gap: 5px;
    min-width: 0;
}
.bgm-logover { opacity: 0.4; font-size: 11px; }
/* The bubble total, right-aligned so the column can be scanned down. */
.bgm-logsum {
    flex: none;
    min-width: 2.2em;
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    opacity: 0.8;
}
.bgm-logsum.bgm-logpop {
    color: #ffb4b4;
    opacity: 1;
    font-size: 11px;
    letter-spacing: 0.04em;
}

/* --- the round summary ---------------------------------------------------- */

/* A narrow, centered card: rows of small numbers for two seats. */
.bgm-summary { max-width: 480px; margin-left: auto; margin-right: auto; text-align: center; }
.bgm-summary .result-actions { justify-content: center; flex-wrap: wrap; }
.bgm-summary .result-note { text-align: center; }
/* EXPLICIT two seat columns, never auto-fit: the shared .result-row wraps a
   column whose content wants more than its minmax, which puts the two names on
   different rows.

   ONE GRID FOR THE WHOLE TABLE, with every row a SUBGRID of it. Each shared
   .result-row is a grid of its own, so an `auto` label column was sized row by
   row: "Bubble" made a narrow one and "Rounds won" a wide one, and the two seat
   columns started at a different x on every row (measured: the first seat
   column's left edge ran 540, 569, 579, 566, 589px down the five rows). The
   first declaration is the fallback for a browser without subgrid, which gets
   the old per-row sizing rather than a broken table. */
.result-table.bgm-result {
    width: fit-content;
    margin: 0 auto;
    grid-template-columns: auto repeat(2, minmax(5.5em, auto));
    gap: 2px 18px;
}
.result-table.bgm-result .result-row {
    grid-column: 1 / -1;
    grid-template-columns: auto repeat(2, minmax(5.5em, auto));
    grid-template-columns: subgrid;
    gap: 2px 18px;
}
.result-table.bgm-result .result-cell {
    text-align: center;
    font-variant-numeric: tabular-nums;
}
.result-table.bgm-result .result-label { text-align: left; white-space: nowrap; }
.result-table.bgm-result .result-row.totals .result-cell {
    font-size: 20px;
    font-weight: 800;
}
/* The winner's star is a badge, not name text, so the name stays centered over
   its column of numbers. */
.bgm-result .result-cell.bgm-won { position: relative; color: var(--accent); }
.bgm-result .result-cell.bgm-won::after {
    content: '\2605';
    position: absolute;
    margin-left: 4px;
}
/* Each finished bubble, as the round's whole story in one row. Both rows are
   one grid, names right-aligned in the first column and cards starting at the
   left of the second, so the two rows line up however different the two names
   and the two bubbles are. Centering each row on its own put "YOU" and "AI" at
   different x and started the cards wherever each row's width left them. The
   grid as a whole is still centered in the summary. */
.bgm-resultstrips {
    display: grid;
    grid-template-columns: auto auto;
    justify-content: center;
    align-items: center;
    gap: 8px 10px;
    margin-top: 8px;
}
.bgm-resultstrips .bgm-notelabel { justify-self: end; }
.bgm-resultstrips .cardrow.bgm-resultrow {
    gap: 3px;
    justify-content: flex-start;
    min-height: calc(30px * 89 / 64);
    align-items: center;
}
.bgm-resultrow .card { --cw: 30px; }
.bgm-empty { font-size: 12px; opacity: 0.6; white-space: nowrap; }

@media (max-width: 720px) {
    /* Stack the three board panels; each keeps its own reserved heights, and
       every reservation is re-derived at this width rather than inherited. */
    .bgm-board { flex-direction: column; }
    .bgm-bubble-row { min-height: calc(30px * 89 / 64 + 8px); }
    .bgm-bubble-row .card { --cw: 30px; }
    /* The caption needs no override: two lines is its measured maximum stacked
       at full page width as well as in a third-width column. */
    .bgm-flag { min-width: 44px; }
}
/* ---------- jackhammer ---------- */

/*
 * Four seat colors, used by the worker ring, the tracker rows, the score chips
 * and the pile strips, so one player's things all read as one player's things.
 * Chosen to survive the four card-suit inks sitting next to them: none of them
 * is the hearts red or the diamonds orange.
 */
.jh-board,
.jh-tracker,
.jh-piles,
.jh-scoreboard,
#result-panel {
    --jh-c0: #7fd1ff;
    --jh-c1: #ffd166;
    --jh-c2: #a6e3a1;
    --jh-c3: #e0a3ff;
}
.jh-seat0 { --jh-seat: var(--jh-c0); }
.jh-seat1 { --jh-seat: var(--jh-c1); }
.jh-seat2 { --jh-seat: var(--jh-c2); }
.jh-seat3 { --jh-seat: var(--jh-c3); }

/* The seat dot the tracker, the piles and the result table all label rows with. */
.jh-dot {
    display: inline-block;
    width: 10px; height: 10px;
    border-radius: 50%;
    background: var(--jh-seat, var(--ink));
    flex: 0 0 auto;
}

/* ---- the board and the tracker, side by side ---- */

/*
 * THE BOARD AND THE SUIT TOTALS SHARE A ROW WHEREVER THEY BOTH FIT.
 *
 * The tracker is not an end-of-game summary, it is the panel the game is played
 * from: your score is your weakest suit, which is a number you cannot read off
 * your own pile without four sums. Below the board it was a scroll away from the
 * thing you scroll it to answer questions about.
 *
 * WRAPPING RATHER THAN A BREAKPOINT, deliberately. The width at which these two
 * fit is not a property of the viewport: #screen-game hands the play area the
 * viewport less 348px of log panel, gap and padding above 951px and the whole
 * width below it, and the tracker's own width depends on the seat count and the
 * longest player name. Any number written here would be wrong for some
 * combination of those three. `flex-wrap` asks the real question instead - do
 * these two boxes fit on one line - and a line that cannot hold both stacks them,
 * which IS the old layout.
 *
 * Both columns are `flex: 1 1 auto` so a wrapped line still gives each panel the
 * full width it has always had. With `flex: 0 0 auto` they would shrink-wrap
 * their contents on a phone, and a panel narrower than the page does not read as
 * one of the site's panels.
 */
.jh-main {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    /*
     * A COLUMN gap only. The panels already carry `.panel`'s 14px margin-bottom,
     * so a row gap here would double the spacing every time the line wraps and
     * make the phone layout taller than the one it replaced.
     */
    gap: 0 14px;
}
.jh-main > .jh-mainleft,
.jh-main > .jh-tracker { flex: 1 1 auto; }

/* ---- the board ---- */

.jh-board { padding: 10px; }

/*
 * The 6x6 grid. Fixed tracks off a local --jh-cw, so the box is a constant size
 * and a centered board never re-centers itself mid-game as cards become holes.
 */
.jh-grid {
    --jh-cw: 54px;
    display: grid;
    grid-template-columns: repeat(6, var(--jh-cw));
    grid-auto-rows: calc(var(--jh-cw) * 89 / 64);
    gap: 5px;
    width: fit-content;
    margin: 4px auto;
}
.jh-cell { position: relative; }
/* Everything in the grid is one cell wide: a card, a hole, or a worker. */
.jh-grid .jh-card,
.jh-grid .jh-holecard { --cw: var(--jh-cw); }

/* A hole: the cell stays, so position still identifies it, but there is
   visibly nothing there and nothing may pass through it. */
.jh-holecard {
    background-image: none;
    background: rgba(0, 0, 0, 0.22);
    border: 1px dashed var(--panel-border);
    box-shadow: none;
    opacity: 0.5;
}

/* A legal destination. */
.jh-cell.lit .card {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}
.jh-cell.hinted .card {
    box-shadow: 0 0 10px 3px rgba(255, 209, 102, 0.6);
}

/*
 * A worker is the WHOLE cell now. Landing on a square takes its card, so there is
 * nothing underneath to leave visible, and the worker paints at full size with a
 * seat-colored ring instead of as a half-size badge in the corner.
 *
 * The ring is the only thing identifying the seat, so it is a real border rather
 * than an outline: a border is part of the box and cannot be clipped by an
 * ancestor's overflow the way an outline can.
 */
.jh-worker {
    border: 2px solid var(--jh-seat, var(--ink));
    border-radius: 5px;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 2px 7px rgba(0, 0, 0, 0.6);
    pointer-events: none;
}
/*
 * POSITIONING ONLY INSIDE A CELL, and the scope is the whole point of the rule.
 *
 * animate.ts appends its flying clone to document.body and relies on the
 * platform's `.card-fly { position: fixed }`. An unscoped `.jh-worker {
 * position: absolute }` comes later in the cascade than that rule and beats it,
 * so a worker's move animation flew a ghost positioned against the page instead
 * of the viewport - which is the trap tools/hewillneverstop/hwns-shot.mjs was written for, and
 * the browser check caught it here as "stray card in flow" at all five widths.
 * Scoping to `.jh-cell` cannot reach the body-mounted ghost, so `.card-fly` wins
 * where it has to and the visual properties above still apply to the clone.
 */
/* The worker cell holds only the worker, so it needs no stacking of its own. */
.jh-worker-cell { position: relative; }
/* The device's own worker reads a step louder than the others. */
.jh-worker.jh-mine {
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.35), 0 2px 6px rgba(0, 0, 0, 0.65);
}

/* ---- the action bar ---- */

/*
 * Its own container, out of the board panel, so a line appearing here can never
 * walk the board down the page - and a reserved height so its own comings and
 * goings move nothing either.
 *
 * THE RESERVE IS A BUTTON'S HEIGHT, not a line of text's. For almost every turn of
 * almost every game this bar holds one short note ("7 spaces in reach."), which
 * needs 26px; in the rare end state where every rival is out and you are ahead it
 * holds the "Claim the win" BUTTON instead, which needs 38. Reserving the smaller
 * one costs 12px of movement in the tracker and the piles at the exact moment the
 * button appears, and reserving the larger costs 12px of blank space that nobody
 * looks at. tools/jh-shot.mjs injects the button and measures this rather than
 * waiting for a deal that offers one, because the deals that do are rare enough
 * that the check passed on two runs out of three before it was made deterministic.
 */
.jh-actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    min-height: 38px;
    margin: 6px 0;
}
.jh-actionnote { font-size: 13px; opacity: 0.85; }
.jh-actionnote.jh-waiting { opacity: 0.6; font-style: italic; }

/* ---- status and scoreboard ---- */

.jh-status.jh-myturn { font-weight: 600; }
.jh-turnlead { font-weight: 700; color: var(--accent); }
.jh-hintnote { color: var(--accent); }

.jh-scorechip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    border-left: 3px solid var(--jh-seat, var(--panel-border));
    padding-left: 6px;
}
.jh-scorechip.jh-out { opacity: 0.55; }
.jh-scorename { opacity: 0.85; }
.jh-scorenum { font-weight: 700; font-variant-numeric: tabular-nums; }

/* ---- the suit tracker ---- */

.jh-tracker { padding: 10px 12px; }
.jh-panelhead { margin: 0 0 4px; font-size: 15px; }
.jh-panelnote { font-size: 12px; opacity: 0.72; margin: 0 0 8px; max-width: 460px; }

/* The table is centered as a block, matching the board's axis. */
.jh-tracker-table {
    border-collapse: collapse;
    margin: 0 auto;
    font-variant-numeric: tabular-nums;
}
.jh-tracker-table th,
.jh-tracker-table td {
    padding: 3px 8px;
    text-align: center;
    font-size: 13px;
    border-bottom: 1px solid var(--panel-border);
}
.jh-tracker-table thead th { font-size: 12px; opacity: 0.7; font-weight: 600; }
.jh-th-name { text-align: left; }
/*
 * The suit headers are NOT muted with the rest of the header row. They are the
 * only thing naming which column is which, in a game whose entire score is read
 * per suit, so they keep the deck's own four inks at full strength.
 *
 * Written at the FULL depth (`thead .jh-th-suit`, and the glyph named inside it)
 * because the two rules it has to beat are `.jh-tracker-table thead th` and
 * `.jh-suit`, and a bare `.jh-th-suit` loses to the first on specificity however
 * far down the file it sits. That is the third time this stylesheet has been
 * caught by it - see the notes on the worker's size and the pile row's wrapping.
 */
.jh-tracker-table thead .jh-th-suit { opacity: 1; }
.jh-tracker-table thead .jh-th-suit .jh-suit { font-size: 17px; }
.jh-suit { font-size: 15px; }

/*
 * A NAME CELL IS A TABLE CELL, and the flex layout goes on a span inside it.
 *
 * `display: flex` on a <th> takes that cell OUT of the table's layout: it stops
 * being a table-cell box, so it no longer participates in column sizing and the
 * header row above it no longer lines up with the body. That is the bug behind
 * "the summary columns don't line up to the headers", and it applies to the result
 * table's name cell for the same reason.
 */
.jh-tracker-name {
    text-align: left;
    font-weight: 600;
    white-space: nowrap;
}
.jh-namebox { display: flex; align-items: center; gap: 6px; }
.jh-trackrow.mine .jh-tracker-who { color: var(--accent); }
.jh-trackrow.jh-out { opacity: 0.55; }
.jh-tracker-sum { min-width: 34px; }
.jh-sumnum { font-weight: 600; }

/*
 * The marked cell is the whole point of the panel: it says WHICH suit the score
 * came out of, which is what a player needs in order to do something about it.
 */
.jh-weakest {
    background: rgba(255, 209, 102, 0.16);
    box-shadow: inset 0 0 0 1px var(--accent);
    border-radius: 4px;
}
.jh-tracker-score { font-weight: 700; }
.jh-tracker-cards { font-size: 12px; opacity: 0.6; }

/* A suit with nothing left on the board cannot be repaired, however badly
   somebody needs it. */
.jh-gone { opacity: 0.45; }
/*
 * The board's remaining sums, drawn as a PEER of the player rows: same size, same
 * weight, full strength. It used to be smaller and dimmed like a footnote, and it
 * read as one - it was asked for as a new feature while it was already on screen.
 * It is half of every decision (what you have against what is still reachable), so
 * it gets the same weight as the rows it is compared with. The heavier rule above
 * it is the only thing marking it as a different kind of row.
 */
.jh-tracker-left th,
.jh-tracker-left td {
    border-bottom: none;
    border-top: 2px solid var(--panel-border);
}

.jh-turntag,
.jh-outtag {
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 1px 4px;
    border-radius: 6px;
    white-space: nowrap;
}
.jh-turntag { background: var(--accent); color: var(--accent-ink); }
.jh-outtag { border: 1px solid var(--panel-border); opacity: 0.7; }

/* ---- the collected piles ---- */

/*
 * `minmax(0, 1fr)` and `min-width: 0`, and both are load-bearing rather than
 * defensive boilerplate.
 *
 * A grid column sized `auto` (and a grid item's default `min-width: auto`) is at
 * least as wide as its content's MIN-CONTENT width, and the min-content width of
 * a nowrap flex row is the sum of every card in it. So a late-game strip of
 * twenty-four cards widened its column, widened the panel, and took the whole
 * PAGE sideways with it - a 390px phone rendered a 551px document, with the strip
 * hanging outside the panel border instead of scrolling inside it. The
 * `overflow-x: auto` on the row cannot help while the row is being handed all the
 * width it asks for. Caught by tools/jh-shot.mjs at the phone widths.
 */
/*
 * TWO COLUMNS: the names, which stay put, and ONE scroller holding every seat's
 * row, so a single swipe moves all the piles together (see renderPiles). A row
 * and its name are the same fixed height, which is what keeps them level without
 * sharing a parent.
 */
.jh-piles {
    --jh-pilerow: calc(26px * 89 / 64 + 6px);
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 8px;
    align-items: start;
    padding: 8px 10px;
}
.jh-pilenames,
.jh-pilescroll {
    display: grid;
    grid-auto-rows: var(--jh-pilerow);
    row-gap: 4px;
}
.jh-pile-head {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    white-space: nowrap;
    border-left: 3px solid var(--jh-seat, var(--panel-border));
    padding-left: 8px;
}
.jh-pile-head.mine .jh-pile-who { color: var(--accent); font-weight: 600; }
/*
 * THE SCROLLER. `overflow-x: scroll`, not `auto`, so the scrollbar's space is
 * there from the first turn: with `auto` the bar appears the turn a pile first
 * overflows, and the panel grows by its height and walks the log down the page.
 *
 * The bar is STYLED so it is always drawn. An unstyled bar on macOS is an overlay
 * that only appears mid-scroll, which made a pile that ran off the right edge
 * look cut off rather than scrollable. Blink and WebKit draw a styled
 * `::-webkit-scrollbar` permanently; Chrome ignores those pseudo-elements on any
 * element that sets `scrollbar-width` or `scrollbar-color`, so the standard
 * properties are only set where the pseudo-elements are not supported (Firefox).
 */
.jh-pilescroll {
    min-width: 0;
    overflow-x: scroll;
    overflow-y: hidden;
    padding-bottom: 2px;
}
.jh-pilescroll::-webkit-scrollbar { height: 8px; }
.jh-pilescroll::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.18); border-radius: 4px; }
.jh-pilescroll::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.38);
    border-radius: 4px;
}
@supports not selector(::-webkit-scrollbar) {
    .jh-pilescroll {
        scrollbar-width: thin;
        scrollbar-color: rgba(255, 255, 255, 0.38) rgba(0, 0, 0, 0.18);
    }
}

/*
 * ONE ROW, at a reserved height. The strip grows all game, so a second row would
 * walk the log down the page every few turns.
 *
 * `max-content` is what makes the shared scroller work: each row is exactly as
 * wide as its cards, so the scroller's scroll width is the longest pile and every
 * row moves by the same offset. The row is deliberately NOT a `.cardrow` (see
 * renderPiles), so nothing in platform.css can wrap it at a phone width.
 */
.jh-pile-cards {
    display: flex;
    flex-wrap: nowrap;
    gap: 3px;
    align-items: center;
    width: max-content;
}
/* Two classes, so this also beats the phone breakpoint's `.card.mini { --cw:
   34px }` - a read-only strip wants to stay small there, not grow. */
.jh-pile-cards .card { --cw: 26px; }
.jh-pile-empty { font-size: 12px; opacity: 0.5; font-style: italic; }

/* ---- the result ---- */

.jh-result-table {
    border-collapse: collapse;
    margin: 10px auto 6px;
    font-variant-numeric: tabular-nums;
}
.jh-result-table th,
.jh-result-table td {
    padding: 4px 10px;
    font-size: 13px;
    border-bottom: 1px solid var(--panel-border);
    text-align: center;
}
.jh-result-table thead th { font-size: 12px; opacity: 0.7; }
.jh-result-name { text-align: left; white-space: nowrap; }
.jh-result-table tr.jh-won { background: rgba(255, 209, 102, 0.12); }
.jh-result-cards { font-size: 12px; opacity: 0.65; }

/* The four totals in the order they are actually compared, weakest first. */
.jh-result-ladder { display: flex; gap: 5px; justify-content: center; }
.jh-ladderchip {
    min-width: 24px;
    padding: 1px 5px;
    border-radius: 5px;
    background: rgba(0, 0, 0, 0.25);
    border: 1px solid var(--panel-border);
    font-size: 12px;
}
/* The position the game was actually decided at. */
.jh-ladderchip.jh-deciding {
    background: var(--accent);
    color: var(--accent-ink);
    border-color: #8a6a00;
    font-weight: 700;
}

/* ---- log ---- */

.jh-logfell { opacity: 0.85; font-style: italic; }
.jh-logover { font-weight: 600; }

/* ---- narrower screens ---- */

/*
 * The card size is driven by --jh-cw on the board, not by a rule on .card, so
 * the grid tracks and the row height recompute with it. The atlas does NOT change
 * with it: every grid card is on the simplified atlas at every width, because the
 * reason is how many cards are read at once rather than how big each one is (see
 * cellNode in def.ts and "Dense grids of cards" in ui-conventions.md).
 */
@media (max-width: 900px) {
    .jh-grid { --jh-cw: 48px; }
}
@media (max-width: 720px) {
    .jh-grid { --jh-cw: 44px; }
    .jh-tracker-table th,
    .jh-tracker-table td { padding: 3px 5px; font-size: 12px; }
}
@media (max-width: 560px) {
    .jh-grid { --jh-cw: 40px; gap: 4px; }
    .jh-worker { border-width: 2px; }
    /*
     * TWO LINES RESERVED for the status line, because that is what it needs here
     * and it sits directly above the board. Its longest ordinary sentence names a
     * card ("Move to any lit card and you collect the 7 of diamonds you are
     * standing on") and wraps at this width while "AI 1 is moving." does not, so
     * without the reserve the board stepped 25px up and down between turns -
     * measured, at both this breakpoint and 390px, by tools/jh-shot.mjs. Scoped
     * to the game's own class: #status-line is on every page in the site.
     */
    .jh-status { min-height: 50px; }
    .jh-panelnote { font-size: 11px; }
    .jh-tracker-table th,
    .jh-tracker-table td { padding: 2px 4px; }
    .jh-tracker-cards, .jh-th-cards { display: none; }
}
@media (max-width: 400px) {
    .jh-grid { --jh-cw: 36px; gap: 3px; }
}
/* =========================================================================
   Packs Kingdoms (packskingdoms)

   Layout intent: three stacked bands, and the two that matter never change
   height. The rivals' kingdoms sit at the top, the market in the middle, your
   own kingdom and your hand at the bottom. Every one of those is built from
   FIXED cells rather than from whatever it happens to be holding:

     - the market is a 6-column grid with three rows of its own (a price row and
       a card row per market row), so a card bought and a coin taken change
       nothing about the grid's size. The coins are badges on the CELL rather
       than rows of their own;
     - every tableau column reserves all three of its card slots, so a suit
       filling up does not walk the hand down the screen;
     - the action bar and its note are separate containers, controls first,
       each with its own reservation, because a prompt appearing under a button
       must not move the button;
     - the rival strip reserves its own height, and a rival's card is a fixed
       shape whatever is in their tableau.

   Every class here is prefixed pk-. Every rule that reaches a shared id or a
   shared class (#hand-cards inside .pk-handpanel, #hand-title inside
   .pk-handcaption, .status.pk-status, .progress.pk-progress, .panel-head inside
   .pk-marketpanel and .pk-youpanel, .result-row inside .pk-resulttable) is
   scoped through a class this game owns, per .kiro/steering/ui-conventions.md.

   The whole board is sized to fit 1280x900 and 390x844 without scrolling; see
   the note on .pk-board, and tools/pk-strings.mjs for where every reserved
   height comes from.
   ========================================================================= */

/* --- the setup screen ----------------------------------------------------- */

/*
 * The two difficulty controls are mutually exclusive: the bot level decides
 * nothing in a solo game (the automaton consults no policy), and the head start
 * decides nothing in any other one.
 *
 * `.pk-solo` is put on #screen-setup by initPkSetup whenever one player is
 * selected. The level group is reached with `:has()` because it is shared markup
 * with no class of its own, and a class on the screen cannot fight the shell's
 * own applier, which only ever writes `hidden` on #opt-level itself.
 */
.pk-headstart { display: none; }
#screen-setup.pk-solo .pk-headstart { display: flex; }
#screen-setup.pk-solo .opt-group:has(#opt-level) { display: none; }
/* And in game, where the same control sits under the board (see def.render). */
#screen-game.pk-solo-game #level-line { display: none; }

/* --- status and progress -------------------------------------------------- */

/*
 * The status line carries a recap AND the current requirement, as two blocks,
 * because both are wanted at once: what the last player did, and what you have
 * to do about it. The recap is the quieter of the two.
 *
 * TWO LINES RESERVED ON A PHONE, and one on a desktop. The longest line this
 * game writes is the attack prompt ("Ann attacked with 2 spades. Hand over 2
 * coins or cards.") over the longest recap ("Ann handed over 2 things."). Set
 * into the real element with the reservation lifted, the pair fits on two lines
 * from 700px up and needs three below it, so the reservation is per breakpoint
 * rather than flat.
 */
.status.pk-status {
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* 46.8px against the 45px the longest pair wants (tools/pk-strings.mjs).
       3.1em was the first guess and reserved 11px of nothing. */
    min-height: 2.6em;
}
.pk-recap { font-size: 13px; opacity: 0.6; }
.pk-ask { font-weight: 600; }
.pk-warn { color: #ffb4b4; font-weight: 600; }

/* One chip a player: runs out of three, and cards on the table. The acting
   seat's chip is the whose-turn marker, and it is the only one that moves. */
.progress.pk-progress {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 8px;
    /* One row of chips, which is what the chips now come to at every width down
       to 390px: they carry the runs and nothing else. 44px was reserved for the
       second row the card count used to force. */
    min-height: 34px;
}
.pk-scorechip {
    display: flex;
    align-items: baseline;
    gap: 6px;
    padding: 4px 10px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.16);
    white-space: nowrap;
}
.pk-scorechip.pk-turn {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.12);
}
.pk-scorechip.pk-won { border-color: var(--accent); }
.pk-scorename { font-size: 12px; font-weight: 600; opacity: 0.8; }
.pk-scorenum {
    font-size: 17px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.pk-scorechip.pk-turn .pk-scorenum,
.pk-scorechip.pk-won .pk-scorenum { color: var(--accent); }
/* No .pk-scorecards rule: the chip carries the runs and nothing else. See the
   note in renderProgress for why the card count went. */
.pk-turncount {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.5;
    white-space: nowrap;
}

/* --- shared furniture ---------------------------------------------------- */

/*
 * THE BOARD IS SIZED TO FIT A LAPTOP. The whole of it - rivals, market, your
 * kingdom, your hand and the action bar - has to be reachable at 1280x900 and at
 * 390x844 without scrolling, which is 810px of usable height on the laptop once
 * the site header and the page padding are off. Measured with tools/pk-measure
 * and a box-by-box walk, the first build wanted 986px, so 133px came off in six
 * places: the market's coin note moved onto the title's row, the hand panel's
 * title moved onto the note's row, panel padding went 14px to 10px, the action
 * bar's own 12px margins went to the column's gap, the panel titles' 10px
 * bottoms went to 2px, and this gap went 12px to 8px.
 *
 * The LOG COLUMN is exempt and cannot be fixed here: platform.css caps it at
 * `calc(100vh - 24px)` and makes it sticky, so on any long match the page has a
 * scroll extent whatever the board does. The two collapsed summaries under the
 * hand are exempt for the same reason - they are closed, optional, and last.
 */
.pk-board { display: flex; flex-direction: column; gap: 6px; }
/* Tighter than the shared .panel, which is written for a page that scrolls. */
.pk-marketpanel,
.pk-youpanel,
.pk-handpanel { padding: 10px 14px; }
.pk-marketpanel .panel-head h3,
.pk-youpanel .panel-head h3 { margin-bottom: 2px; }

/* The one chip shape the whole board spends: coins, run counts, hand sizes,
   deck depth. Content-sized, so it needs no centering of its own. */
.pk-chip {
    font-size: 11px;
    padding: 2px 7px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.2);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.pk-chip.pk-coin { color: var(--accent); font-weight: 700; }
.pk-suit { font-weight: 700; }
.pk-empty { font-size: 12px; opacity: 0.6; white-space: nowrap; }
.pk-quiet { font-size: 12px; opacity: 0.72; line-height: 1.4; }
.pk-hintnote { font-size: 13px; color: var(--accent); line-height: 1.4; }

/* A card the rules refuse right now: visible, inert, dimmed. Never hidden, and
   only lightly dimmed: WHICH card you cannot afford is information a player uses
   to plan the next turn, so it has to stay readable. At 0.4 the market read as a
   row of gray rectangles whenever the purse was empty, which is most of the early
   game. */
.card.pk-mute { opacity: 0.55; }
/* A hand card that fits nowhere in the tableau, so it can only be discarded. */
/*
 * A CARD YOUR KINGDOM CANNOT TAKE, marked wherever it is shown: in your hand,
 * where it can only be discarded, and in the market, where buying it would cost
 * you an action to throw it away.
 *
 * The mark is a small cross at the foot of the card rather than a dimming,
 * because a market card that does not fit may still be worth buying (to deny it)
 * and a dimmed one reads as disabled. Only the hand dims, where it really does
 * mean "this is the one thing you cannot do with it".
 *
 * `position: relative` is what the cross anchors to, and a FLYING CLONE KEEPS
 * THE CLASS: ui/animate.ts places a ghost with `position: fixed`, and two
 * classes here would beat it and lay the ghost out in flow. Three classes win it
 * back, and `position: fixed` is itself a containing block, so the cross still
 * anchors to the card. The pattern, and the bug it avoids, are src/cardcapture.
 */
/*
 * A MARKET CARD THAT WOULD FINISH ONE OF YOUR SUITS, ringed in gold. A box
 * shadow rather than an outline, because `.hinted` and `.lit` are outlines and a
 * hinted finisher must show both; the card's own drop shadow is restated so the
 * ring does not replace it. Shown whether or not you can afford it, like the
 * cross below: it is about your kingdom, not your purse.
 */
.card.pk-finisher {
    box-shadow: 0 0 0 2px var(--accent), 0 0 10px rgba(255, 209, 102, 0.55),
        0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
/* Still visible on a card you cannot afford, which dims it. */
.card.pk-finisher.pk-mute { opacity: 0.8; }

.pk-handpanel .card.pk-nofit { opacity: 0.72; filter: saturate(0.5); }
.card.pk-nofit { position: relative; }
.card.pk-nofit.card-fly { position: fixed; }
.card.pk-nofit::after {
    content: '\00d7';
    position: absolute;
    left: 50%;
    bottom: 1px;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: calc(var(--cw) * 0.32);
    height: calc(var(--cw) * 0.32);
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.62);
    color: #ff9c9c;
    font-size: calc(var(--cw) * 0.26);
    font-weight: 700;
    line-height: 1;
    pointer-events: none;
}
/* Staged: picked toward a payment, but not committed. */
.card.pk-staged { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A card the attack just put in this hand, while the discard is owed. */
.card.pk-fresh { outline: 2px dashed var(--accent); outline-offset: 2px; }

/* An empty slot is NOT a card: it carries the card's geometry and none of its
   paint. Giving it the `card` class would paint the atlas at 0% 0%, which is
   the corner of the Ace of clubs (see the note in shared/ui/sprites.ts). */
.pk-slot {
    box-sizing: border-box;
    border-radius: 6px;
    border: 1px dashed var(--panel-border);
    background: rgba(0, 0, 0, 0.14);
    width: var(--cw);
    height: calc(var(--cw) * 89 / 64);
}
.pk-slot.pk-slotopen {
    border-color: var(--accent);
    border-style: solid;
    background: rgba(255, 209, 102, 0.12);
}

/* --- the rivals: every other kingdom, ONE LINE EACH ---------------------- */

/*
 * A scrolling strip, and therefore NOT centered: `justify-content: center` on a
 * scroll container clips the left overflow behind an edge the player cannot
 * reach. Three rivals at four players is the widest case.
 *
 * ONE ROW PER RIVAL now, where the first build gave each one a card with a head
 * and three stacked card rows: 177px for the strip whatever the table size. A
 * rival's four suits laid out side by side fit on the same line as their name and
 * their purse, so the strip is a third of the height and the information is the
 * same.
 */
.pk-rivals {
    /* One knob for the strip's card size, so the reservation below recomputes
       with it instead of drifting out of step at a breakpoint. 18px, because
       three rival lines at a four-handed table is 117px of board at 20px and
       109px at 18px, and a simplified card reads either way. */
    --pk-rcw: 18px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    overflow-x: auto;
    padding-bottom: 2px;
}
/*
 * Reserved at its tallest ordinary shape: an empty rival prints dashes, which
 * are 11px text, and the line grows to a card height the moment they buy
 * anything. Measured before the reservation, the strip walked 65px to 150px
 * during a match, and every panel below it - market, kingdom, hand - moved with
 * it. 8px is the padding and the border, which `border-box` puts inside this.
 */
.pk-rival {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 3px 8px;
    min-height: calc(var(--pk-rcw) * 89 / 64 + 8px);
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.16);
    width: max-content;
    min-width: 100%;
}
.pk-rival.pk-turn {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.1);
}
/* Name and purse, in a fixed-width gutter so the four suit groups of every
   rival line up down the strip and can be compared at a glance. */
.pk-rivalwho {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 104px;
    white-space: nowrap;
}
.pk-rivalname {
    font-size: 12px;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 72px;
}
.pk-rival.pk-turn .pk-rivalname { color: var(--accent); }

/*
 * The kingdom is CENTERED in whatever the name, purse and hand leave, by auto
 * margins rather than by `justify-content` on the strip: the strip scrolls, and
 * centering a scroll container clips the left overflow behind an edge nobody can
 * reach (.kiro/steering/ui-conventions.md). An auto margin absorbs free space
 * when there is some and resolves to zero when there is not, so a line too wide
 * for the screen still starts at its left edge.
 */
.pk-minitab {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-inline: auto;
}
/* Each suit is a glyph and its cards, left to right. A fixed minimum keeps the
   four groups aligned across rivals even when one of them is empty. */
.pk-minigroup {
    display: flex;
    align-items: center;
    gap: 3px;
    min-width: 76px;
}
.pk-minigroup .pk-suit { font-size: 11px; opacity: 0.55; }
.pk-minigroup.pk-done .pk-suit { opacity: 1; color: var(--accent); }
/* TWO classes, because platform.css's `.card.mini { --cw: 46px }` is itself two
   and a bare `.pk-mini` loses the tie. Written as one class first, this shipped
   46px rival cards and a strip that grew 85px over a match. */
.card.pk-mini { --cw: var(--pk-rcw); }
/* An empty suit, and an empty hand: a dash rather than three placeholders,
   because a rival's unused suit is not a shape anybody is filling in. */
.pk-none { font-size: 11px; opacity: 0.35; }

/*
 * A rival's hand, FACE UP, beside their purse: the two together are what they
 * can spend next turn. Every card in it was bought off the face-up market, so
 * none of it is private (see handBacks).
 */
.pk-backs {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 2px;
    padding-right: 8px;
    border-right: 1px solid var(--panel-border);
    min-width: 46px;
}
.pk-back {
    --cw: 18px;
    width: var(--cw);
    height: calc(var(--cw) * 89 / 64);
}
/* Overlapped: one back looks like any other, and only the number is read. */
.pk-back + .pk-back { margin-left: -9px; }
/* SOLO: the automaton's instruction sits where a hand would, face up beside the
   deck it came off, and is wide enough for both. */
.pk-autoslot { gap: 4px; min-width: 52px; }
.pk-autoslot .pk-back + .card { margin-left: 0; }

/* --- the market: the board's centerpiece --------------------------------- */

.pk-marketpanel { margin: 0; }
/* The deck, and the discard pile beside it once there is one. */
.pk-pile { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.pk-pile-spent { opacity: 0.6; }

/* The deck sits to the LEFT of the grid because that is what the prices mean:
   the column against the deck costs five, and the far column is free. */
.pk-marketrow {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
}
.pk-deck {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 5px;
}
.pk-deckback {
    --cw: 40px;
    width: var(--cw);
    height: calc(var(--cw) * 89 / 64);
}
.pk-deckback.pk-dry { opacity: 0.35; }
.pk-deckcount {
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    opacity: 0.6;
}

/*
 * FIVE ROWS OF FIXED CELLS: prices, row 0's cards, row 0's coins, row 1's
 * cards, row 1's coins. Reserving the coin rows whether or not any coins are
 * there is the whole anti-jitter story for this panel - money appears under
 * cards constantly as people buy things, and a row that sized itself to its
 * contents would step the tableau and the hand down the screen on every
 * purchase.
 */
.pk-market {
    flex: 0 1 auto;
    display: grid;
    grid-template-columns: repeat(6, var(--pk-mcw));
    grid-auto-rows: auto;
    justify-content: center;
    gap: 4px 5px;
    --pk-mcw: 52px;
}
.pk-market .card { --cw: var(--pk-mcw); }
/* `relative` so the coin badge can sit on the card's corner. */
.pk-cell {
    position: relative;
    display: flex;
    justify-content: center;
    min-height: calc(var(--pk-mcw) * 89 / 64);
}
.pk-cell .pk-slot { --cw: var(--pk-mcw); }
.pk-cost {
    font-size: 11px;
    font-weight: 700;
    text-align: center;
    letter-spacing: 0.03em;
    opacity: 0.6;
    white-space: nowrap;
}
.pk-cost.pk-free { color: var(--accent); opacity: 0.9; }
/*
 * The coins sit ON the card, in its top-right corner, and the badge is OPAQUE.
 *
 * Opaque on purpose: a translucent badge over card art reads as part of the art,
 * and this is a number the player adds up. Being on the card also settles which
 * card the money belongs to, which a chip in a row underneath never quite did -
 * it sat between two rows of cards and could be read as either.
 *
 * On the CELL rather than on the card, so no `position: relative` lands on a
 * `.card`: an animation ghost inherits that and drops into document flow (the
 * same trap .kiro/steering/ui-conventions.md records for badged cards).
 */
.pk-coins {
    position: absolute;
    top: -5px;
    right: -5px;
    z-index: 2;
    font-size: 11px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: #3a2c00;
    background: var(--accent);
    border: 1px solid #8a6a00;
    border-radius: 999px;
    padding: 0 5px;
    line-height: 16px;
    min-width: 16px;
    text-align: center;
    white-space: nowrap;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
button.pk-coins { cursor: pointer; font-family: inherit; }
.pk-coins.pk-staged {
    background: #fff3d0;
    outline: 2px solid var(--accent);
}
/* ALWAYS one line, and reserved as one: the money figure is there whether or
   not any coins are, and the income prompt joins it on the same row. It rides
   the panel title's row, so it takes the width the title leaves and never adds
   a row of its own. */
.pk-marketnote {
    flex: 1 1 auto;
    display: flex;
    justify-content: flex-end;
    flex-wrap: nowrap;
    gap: 6px;
    min-height: 1.5em;
    overflow: hidden;
    white-space: nowrap;
}
.pk-marketask { font-size: 12px; font-weight: 600; color: var(--accent); }

/* --- your own kingdom ---------------------------------------------------- */

.pk-youpanel { margin: 0; }
.pk-purse { display: flex; gap: 6px; flex-wrap: nowrap; }

/*
 * MONEY MOVING. A coin is a number here rather than an object, so nothing about
 * a payment animates by itself (see showMoneyMoves); this is the figure that
 * floats off the purse that changed.
 *
 * It is `position: absolute` inside the chip, which is `relative` for it, so it
 * cannot move anything: the chips sit in reserved rows and a figure that took
 * part in layout would walk the board every time somebody paid for a card.
 */
.pk-chip.pk-coin { position: relative; }
.pk-fx {
    position: absolute;
    left: 50%;
    bottom: 100%;
    transform: translateX(-50%);
    padding: 0 2px;
    font-size: 13px;
    font-weight: 800;
    white-space: nowrap;
    color: var(--accent);
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
    pointer-events: none;
    animation: pk-float 1.1s ease-out forwards;
}
.pk-fx-down { color: #ff9c9c; animation-name: pk-sink; }
@keyframes pk-float {
    0% { opacity: 0; transform: translate(-50%, 6px); }
    18% { opacity: 1; }
    100% { opacity: 0; transform: translate(-50%, -18px); }
}
@keyframes pk-sink {
    0% { opacity: 0; transform: translate(-50%, -8px); }
    18% { opacity: 1; }
    100% { opacity: 0; transform: translate(-50%, 14px); }
}

/*
 * Four suit groups, each laid out LEFT TO RIGHT, wrapping to two rows only when
 * the width cannot take four.
 *
 * Sideways because a run IS a sequence: 5 6 7 reads as one, and the same three
 * cards stacked vertically read as a pile. It also costs one card height instead
 * of three, which is about 170px of the board.
 */
.pk-tableau {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    flex-wrap: wrap;
    gap: 6px 10px;
    --pk-tcw: 46px;
}
.pk-suitgroup {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 4px 6px;
    border-radius: 10px;
    border: 1px solid transparent;
    min-width: 0;
}
.pk-suitgroup.pk-done { border-color: rgba(255, 209, 102, 0.45); }
/*
 * A SUIT THAT CAN'T BE FINISHED: every card that would finish it is in another
 * kingdom (suitBlockers). The label replaces the power note in red, the count is
 * struck, and the empty slots fade, so the suit reads as closed at a glance. Its
 * cards stay at full strength: they still power the suit.
 */
.pk-suitgroup.pk-dead .pk-power { color: #ff9c9c; opacity: 0.9; }
.pk-suitgroup.pk-dead .pk-suitcount { text-decoration: line-through; }
.pk-suitgroup.pk-dead .pk-slot { opacity: 0.35; }
.pk-suitgroup.pk-target {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.1);
}
/* A suit offering to take the one card in hand that fits it, on one press. A
   quieter ring than pk-target's: that one answers a card the player has already
   picked, this one is an offer they have not asked for yet. */
.pk-suitgroup.pk-offer { border-color: rgba(255, 209, 102, 0.3); }
/* ONE line: the suit, how far along it is, and what it is doing for its owner.
   `nowrap` throughout, because a wrapped power note makes the group taller than
   the space reserved for it. */
.pk-suithead {
    display: flex;
    align-items: baseline;
    gap: 5px;
    white-space: nowrap;
}
.pk-suitid { display: flex; align-items: baseline; gap: 5px; }
.pk-suithead .pk-suit { font-size: 15px; }
/* The phone wording, hidden wherever the long one fits. */
.pk-powershort { display: none; }
.pk-suitcount {
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    opacity: 0.6;
}
.pk-suitgroup.pk-done .pk-suitcount { color: var(--accent); opacity: 1; }
.pk-power {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    opacity: 0.5;
    white-space: nowrap;
}
.pk-suitgroup.pk-done .pk-power { opacity: 0.35; }

/* A suit's cards, side by side. Shared by the kingdom and the rival strip; the
   card size comes from whichever of them the row is sitting in. */
.pk-suitrow { display: flex; align-items: center; gap: 3px; }
.pk-tableau .pk-suitrow .card { --cw: var(--pk-tcw); }
.pk-tableau .pk-suitrow .pk-slot { --cw: var(--pk-tcw); }

/* The open slot IS the "play here" control, so it must keep the slot's exact
   geometry: a button beneath the group instead measured 28px of growth, which
   walked the hand panel down the screen every time a card was armed. */
button.pk-slot.pk-place {
    cursor: pointer;
    font-family: inherit;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--accent);
    padding: 0;
    white-space: nowrap;
}

/* --- the hand panel, with the action bar inside it ----------------------- */

/*
 * The action bar and its note live INSIDE the hand panel, below the cards, so a
 * prompt appearing or being answered never moves the cards a player is reaching
 * for. Controls first and prose second, each with its own reservation.
 *
 * NO min-height ON #hand-cards. The tray's height is reserved at runtime by
 * src/shared/ui/handspace.ts, per viewport width and per deal, which is a
 * better answer than any per-game figure could be.
 */
.pk-handrow { display: flex; flex-direction: column; gap: 6px; }
.pk-handpanel #hand-cards { gap: 8px; }
.pk-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 8px;
    /* The column's own gap does the spacing, so the shared .controls margin of
       12px top and bottom is 24px of board for nothing. */
    margin: 0;
    /* 38px, not 36: a primary button is two pixels taller than a plain one, so
       36 let the bar breathe by exactly that whenever a play was armed. */
    min-height: 38px;
}
/* The hand's title and the action note, on one row and centered as a pair. The
   row's height is the note's reservation, which the title cannot exceed: it is
   one line and never wraps. */
.pk-handcaption {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: baseline;
    gap: 2px 10px;
}
.pk-handcaption #hand-title {
    flex: 0 0 auto;
    margin: 0;
    font-size: 14px;
    white-space: nowrap;
}
/* ONE line's worth here: every note this game writes fits one line from 661px
   up, measured with tools/pk-strings.mjs (22px wanted, 24px reserved). The
   two-line reservation this started with was a guess, and 21px of the board. */
.pk-actionnote {
    text-align: center;
    min-height: 1.5em;
}

/* --- the log ------------------------------------------------------------- */

/* Text, so left-aligned: centering is for the board axis, not for prose. */
.pk-logrun { font-weight: 600; }
.pk-logattack { color: #ffc9a8; }
.pk-logquiet { opacity: 0.55; font-size: 12px; }
/* SOLO: the instruction line, which is the automaton explaining itself. */
.pk-logauto { color: #cfe3ff; }
.pk-logover { font-weight: 700; color: var(--accent); }

/* --- the result panel ---------------------------------------------------- */

.pk-result { text-align: center; }
.pk-result .result-actions { justify-content: center; flex-wrap: wrap; }
.pk-result .result-note { text-align: center; }
/*
 * EXPLICIT seat columns, never auto-fit.
 *
 * The shared `.result-row` asks for `repeat(auto-fit, minmax(180px, 1fr))`,
 * which is right for a game whose cells hold card rows. Every cell here is three
 * characters, and 180px each is 840px at four seats: more than the panel has in
 * a modal, so auto-fit dropped the last seats onto rows of their own and the
 * table read as columns spanning several rows. One class per table size, because
 * `repeat(var(--n), ...)` is not reliable across the browsers this ships to.
 *
 * `minmax(54px, 1fr)` under a `fit-content` table gives equal columns sized to
 * the widest cell, and cannot wrap whatever the panel's width turns out to be.
 *
 * The label column is a FIXED width, not `auto`. Every `.result-row` is its own
 * grid, so an `auto` first column is sized per row, and the longest label
 * (diamonds) shunted that one row's figures out of line with the rest.
 */
.result-table.pk-resulttable {
    --pk-rlabel: 88px;
    width: fit-content;
    margin: 0 auto;
}
.result-table.pk-resulttable .result-row { gap: 6px 16px; }
.result-table.pk-resulttable.pk-seats-2 .result-row {
    grid-template-columns: var(--pk-rlabel) repeat(2, minmax(54px, 1fr));
}
.result-table.pk-resulttable.pk-seats-3 .result-row {
    grid-template-columns: var(--pk-rlabel) repeat(3, minmax(54px, 1fr));
}
.result-table.pk-resulttable.pk-seats-4 .result-row {
    grid-template-columns: var(--pk-rlabel) repeat(4, minmax(54px, 1fr));
}
.result-table.pk-resulttable .result-cell {
    text-align: center;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.result-table.pk-resulttable .result-label {
    text-align: left;
    white-space: nowrap;
}
.result-table.pk-resulttable .result-row.totals .result-cell {
    font-size: 19px;
    font-weight: 800;
}
/* A finished suit, or a finished game: the numerator that has reached its
   denominator. The whole table reads `n/3`, so this is what the eye is looking
   for. */
.result-table.pk-resulttable .pk-full {
    color: var(--accent);
    font-weight: 800;
}

/* --- narrower screens ---------------------------------------------------- */

@media (max-width: 900px) {
    .pk-market { --pk-mcw: 46px; }
    .pk-tableau { --pk-tcw: 42px; }
    .pk-deckback { --cw: 30px; }
}

@media (max-width: 660px) {
    /* A third line: the income prompt is the longest sentence the game writes
       and wraps twice in a column this narrow, with the recap above it. 71px
       measured against the 73.8px this reserves (tools/pk-strings.mjs). */
    .status.pk-status { min-height: 4.1em; }
    .pk-market { --pk-mcw: 40px; gap: 3px 4px; }
    /*
     * The market's own title goes, and the coin note has the row to itself. Two
     * of them on one row made "The card market" wrap to two lines in a column
     * this narrow, which cost more than the title is worth: the price row, the
     * grid and the deck beside it say what this panel is.
     */
    .pk-marketpanel .panel-head h3 { display: none; }
    .pk-marketnote { justify-content: center; }
    /*
     * Two groups a row rather than four, which is what fits: the four-across
     * layout needs about 600px and this is a 340px content column.
     *
     * A GRID rather than a wrapping flex row, because wrapping has a cliff here:
     * the four groups are 163px to 185px wide depending on which power note they
     * carry, so at 340px two of them fit or they do not, and when they do not
     * every group takes a row of its own and the panel jumps 124px. Two fixed
     * columns cannot do that.
     */
    .pk-tableau {
        --pk-tcw: 40px;
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        justify-items: center;
        gap: 4px 6px;
    }
    /*
     * THE SUIT HEAD MOVES BESIDE THE CARDS. Above them it is two more rows of
     * text on a board that has two rows of suit groups, which measured 43px; to
     * the left it fits in the height the cards already take. The phone wording
     * of the power note (.pk-powershort) exists for exactly this 45px.
     */
    .pk-suitgroup {
        flex-direction: row;
        align-items: center;
        gap: 6px;
        padding: 3px 4px;
    }
    .pk-suithead {
        flex-direction: column;
        align-items: flex-start;
        gap: 0;
    }
    .pk-power { display: none; }
    .pk-power.pk-powershort { display: inline; }
    /* "Can't finish" is wider than the 45px the head has here, so it takes two
       lines. The head is beside a card 56px tall and holds three lines in 41px,
       so this cannot make the group taller. */
    .pk-suitgroup.pk-dead .pk-power.pk-powershort {
        display: block;
        max-width: 46px;
        white-space: normal;
        line-height: 1.15;
    }
    .pk-deckback { --cw: 26px; }
    /* The panels themselves, tighter again: 4px a panel is 12px of board, and
       the board is 12px short of the fold on a 390x844 phone without it. */
    .pk-marketpanel,
    .pk-youpanel,
    .pk-handpanel { padding: 8px 10px; }
    .pk-board { gap: 6px; }
    .pk-handrow { gap: 5px; }
    /* The rival strip keeps its own card size here: 20px is already at the floor
       where the simplified atlas is compulsory, so there is nothing to shrink. */
    .pk-minigroup { min-width: 68px; gap: 2px; }
    .pk-minitab { gap: 5px; }
    .pk-rivalwho { min-width: 88px; }
    /* Nothing to reorder here: the hand is in front of the suits at every width
       now, which is where a phone needed it anyway (the line scrolls, and the
       hand was the part always off the right-hand edge). */
    /* Three lines' worth: the attack explanation and the discard-down note both
       wrap twice in a column this narrow, and want 45px, against the 48px this
       reserves (tools/pk-strings.mjs). */
    .pk-actionnote { min-height: 3em; }
    /* NOTHING extra for .pk-actions here: every bar this game builds fits ONE
       row down to 390px with the short labels, measured button by button. */
}

/*
 * The narrowest phones, where a three-button bar really does need two rows.
 *
 * Measured with every bar this game builds, at 1280, 660, 480, 390 and 360px:
 * one row at every width down to 390px, and two at 360px for the three-button
 * cases (play / discard / cancel, and add a coin / hand over / clear). The bar
 * sits ABOVE the hand, so a second row appearing would push the cards down; the
 * dead space when the bar is empty is the price, and it is only paid here.
 */
@media (max-width: 380px) {
    .pk-actions { min-height: 84px; }
}

@media (max-width: 420px) {
    /* Six columns of 34px plus gaps is 224px, which still fits a 390px page
       with the panel padding. Below 35px the market would have to change atlas
       (see the small-card rule), so this is the floor. */
    .pk-market { --pk-mcw: 35px; }
    .pk-tableau { --pk-tcw: 36px; }
    .pk-suithead .pk-suit { font-size: 14px; }
    /* A grid cell here is 167px and the group wants 171px of it, so 4px comes
       off the row's gaps and the group's padding. Anything that widens the
       power note again has to come off somewhere too. */
    .pk-suitrow { gap: 2px; }
    .pk-suitgroup { padding: 3px; }
    /* And the uppercase tracking, which is 4px on the widest label ("Shield 0")
       and the difference between fitting the cell and bleeding out of it. */
    .pk-power { letter-spacing: 0; }
    /*
     * Four score chips and the turn count on ONE row. At the sizes above they
     * come to 360px in a 348px column, so the fourth chip wraps and the row is
     * 52px instead of 34px - which is 18px of a four-handed phone board that has
     * 27px too little to begin with. Shrinking the chip keeps the turn count,
     * where hiding it would have bought the same 18px by throwing information
     * away on two- and three-handed tables that do not need the room.
     */
    .pk-progress.pk-progress { gap: 6px; }
    .pk-scorechip { gap: 5px; padding: 3px 7px; }
    .pk-scorename { font-size: 11px; }
    .pk-scorenum { font-size: 15px; }
    /* 6px a panel, which is the last of it. */
    .pk-marketpanel,
    .pk-youpanel,
    .pk-handpanel { padding: 6px 8px; }
}
/* ==========================================================================
 * A Matter of Honor (matter-of-honor/) - Edin Mujadzevic's two-player
 * deckbuilding duel. Opponent on top, the shared market in the middle, you
 * below, and the action column inside the hand panel so a decision never moves
 * the cards (the Khmer/Le Truc recipe).
 *
 * Everything here is scoped through a `.moh-` class, never a bare shared id:
 * `#hand-cards`, `#status-line` and friends are on all hundred-odd pages, so an
 * unscoped rule in this block would resize every game's hand (see
 * .kiro/steering/ui-conventions.md, "Shared ids need a game class").
 *
 * NOTHING ABOVE THE HAND MAY MOVE AS THE DUEL RUNS, so every box that grows is
 * either a fixed grid or a reserved height: the market is 4x3 whatever has been
 * bought (a cleared pile paints an empty slot), each side's court row is four
 * slots whether or not there is a card in them, and a discard pile scrolls
 * sideways rather than wrapping to a second row.
 * ========================================================================== */
/* The recap line plus TWO lines of the ask: a picked card's note (effectOf in
   def.ts) runs to two lines at a laptop width, and at 3.1em the line grew by one
   when it did, walking the board and the hand down 15px (measured at 1280px). */
.status.moh-status { min-height: 4.2em; }
.moh-recap { display: block; font-size: 15px; font-weight: 500; opacity: 0.78; }
.moh-ask { display: block; }
@media (max-width: 620px) {
    .status.moh-status { min-height: 5.6em; }
}
/* --- the progress line, and the health component it shares with a seat head -
 * The progress line is where the two totals sit SIDE BY SIDE, which is the only
 * place both are visible once the board stacks into one column; each seat head
 * carries the same component at `big`. A player called seeing the life totals
 * critical, so this is deliberate duplication rather than an oversight.
 * ---------------------------------------------------------------------------- */
.progress.moh-scoreboard {
    opacity: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 10px;
    min-height: 34px;
}
.moh-round {
    font-size: 13px;
    padding: 4px 10px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.25);
    white-space: nowrap;
}
.moh-health {
    display: inline-flex;
    align-items: baseline;
    gap: 7px;
    padding: 3px 10px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.25);
    white-space: nowrap;
}
.moh-health.mine { border-color: rgba(255, 209, 102, 0.5); }
.moh-healthname { font-size: 13px; opacity: 0.8; }
.moh-healthnum {
    font-size: 19px;
    font-weight: 800;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}
.moh-healthof { font-size: 11px; opacity: 0.55; }
/* The seat-head copy: bigger, and only as wide as it needs to be. It was full
   width with the bar pushed to the far edge by `margin-left: auto`, which read as
   two separate things once the panel got wide. */
.moh-health.moh-health-big {
    display: flex;
    align-items: baseline;
    justify-content: center;
    align-self: center;
    gap: 9px;
    padding: 5px 13px;
}
.moh-health-big .moh-healthnum { font-size: 27px; line-height: 1.05; }
/* Ten pips, so a hit is visible as a shape and not only as a number. */
.moh-bar { display: inline-flex; gap: 2px; align-self: center; }
.moh-pip {
    width: 7px;
    height: 13px;
    border-radius: 2px;
    background: rgba(255, 255, 255, 0.12);
}
.moh-health-big .moh-pip { width: 8px; height: 16px; }
.moh-pip.lit { background: #e0533c; }
/* --- the board: three layouts, one DOM ---------------------------------------
 * THE GRID IS THE ONLY THING THAT CHANGES, through named areas, because nothing
 * in this game is wide - the market is four cards across and so is a hand - so a
 * single stack left two thirds of a desktop empty and pushed the hand 800px down
 * the page.
 *
 *   wide    opponent | market | you      the market between the two holdings
 *   medium  opponent | you                and the market full width beneath them
 *           market  market
 *   narrow  opponent / market / you      one stack
 *
 * THE MIDDLE LAYOUT EARNS ITS KEEP, measured: the play area is about 350px
 * narrower than the viewport because the log sits beside it, so at a 1100px
 * window the three-column form gave each seat only 213px - seven cards to a row,
 * and a discard pile scrolling in a box. Collapsing straight to one stack instead
 * made the board 1439px tall, which is the complaint this whole layout exists to
 * fix. Two columns at that width give each seat 370px and keep the board near
 * 600px.
 *
 * `auto` for the market track and `minmax(0, 1fr)` for the seats: the market is
 * content-sized and must never be squeezed, and the 0 floor is what stops a long
 * card group from widening its own column past its share (a bare 1fr uses
 * min-content as the floor, and a twenty-card row's min-content is twenty cards).
 * ---------------------------------------------------------------------------- */
.moh-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas: 'opp market me';
    /* EVERY COLUMN HUGS ITS CONTENT. Stretching the seat panels to the row made
       them the same box whatever they held, which was tidy while they held a lot
       and wrong once the empty sections came out: the taller column is now the
       market, so a stretched seat panel was a bordered box with 240px of nothing
       under its cards. Unequal panels read fine now that both carry a border. */
    align-items: start;
    gap: 12px;
    margin: 10px 0 14px;
}
.moh-side.moh-opp { grid-area: opp; }
.moh-side.moh-mine { grid-area: me; }
.moh-middle {
    grid-area: market;
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-self: start;
}
@media (max-width: 1279px) {
    .moh-board {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        grid-template-areas:
            'opp me'
            'market market';
    }
    .moh-middle { align-self: center; }
}
@media (max-width: 700px) {
    .moh-board {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas:
            'opp'
            'market'
            'me';
    }
}
.moh-side {
    display: flex;
    flex-direction: column;
    /* Centered on the panel's axis, like every other board on the site: a column
       of left-aligned rows inside a centered three-column board reads as
       unfinished (see .kiro/steering/ui-conventions.md). */
    align-items: center;
    gap: 6px;
    min-width: 0;
    padding: 8px 10px;
    border-radius: 12px;
    /* A faint panel either way, so the two sides read as the same KIND of thing.
       With a transparent border on the idle one, the acting highlight was the
       only box on screen and the two columns looked structurally different. */
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.18);
}
/* The acting side lights up so whose-turn is unmistakable. */
.moh-side.moh-active {
    border-color: rgba(255, 209, 102, 0.55);
    background: rgba(255, 209, 102, 0.07);
}
/* --- one seat's head: name, health, and the two reach numbers --------------- */
.moh-seathead {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
}
.moh-seatline {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 8px;
    min-height: 22px;
}
.moh-seatname { font-weight: 700; font-size: 15px; }
.moh-toact {
    font-size: 11px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--accent);
}
/* The marker keeps its slot when idle so the head row never jumps. */
.moh-toact.off { visibility: hidden; }
.moh-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; }
.moh-chip {
    font-size: 12px;
    padding: 3px 8px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.25);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
/* The two numbers the duel turns on: whether your best hit out-reaches their
   best answer. Both are public (see reachOf in def.ts). */
.moh-chip.moh-hit { border-color: rgba(224, 83, 60, 0.55); }
.moh-chip.moh-blockchip { border-color: rgba(110, 160, 220, 0.55); }
/* --- a labeled group of cards, the unit a side column is built from -------- */
.moh-grouphead {
    display: flex;
    align-items: baseline;
    justify-content: center;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 3px;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.62;
    text-align: center;
}
/* "4 in hand": the one fact the row of cards cannot state and must not. */
.moh-groupaside { font-size: 10.5px; opacity: 0.8; white-space: nowrap; }
.moh-grouplabel { min-width: 0; }
.moh-group { display: flex; flex-direction: column; align-self: stretch; }
/* The panel is as wide as its widest row, so every row is told to fill it and
   its own `justify-content: center` then has something to center against. The
   two content-sized pills opt back out (see .moh-health-big, .moh-front). */
.moh-side > * { align-self: stretch; }
.moh-groupcount {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    opacity: 0.85;
}
/* ONE ROW RESERVED, AND A SECOND ONLY WHEN THERE IS SOMETHING IN IT.
 *
 * The reservation exists so a group that GROWS does not walk the hand down the
 * screen, and two rows everywhere bought that at 36px of white space under the
 * words "nothing thrashed", in both panels, all game. One row plus `max-content`
 * growth is the same guarantee for half the cost, because of where these
 * groups sit: the market is the tallest column, so it sets the grid row, and a
 * seat panel has slack to grow into before anything outside it moves. The panels
 * stretch to that row either way, so the slack is invisible.
 *
 * Two rows is the cap. Past that a pile scrolls, which is rare - a discard of
 * more than two rows means twenty-plus cards at this size - and is still better
 * than a third row of height reserved against the chance of it.
 */
.moh-cards {
    flex-wrap: wrap;
    gap: 3px;
    align-items: flex-start;
    justify-content: center;
    min-height: calc(26px * 89 / 64);
    max-height: calc(2 * (26px * 89 / 64) + 3px);
    overflow-y: auto;
    scrollbar-width: thin;
}
.moh-cards .card { --cw: 26px; }
.moh-none { font-size: 12px; opacity: 0.55; font-style: italic; }
/* The opponent's discard and thrashed piles: one under the other in a wide
   panel, as if the wrapper were not there, and side by side on a phone (below),
   where a full-width row for each was most of the panel's height. */
.moh-grouppair { display: contents; }
/* --- the court cards standing in front ------------------------------------- */
/* Four slots always, one per suit, so the row's height and width never change
   and a player can see at a glance which suits are boosted. */
.moh-front {
    display: flex;
    justify-content: center;
    gap: 8px;
    /* Four slots always, one per suit, so the row's height never changes and a
       player can see at a glance which suits are boosted. */
    min-height: calc(40px * 89 / 64 + 16px);
}
.moh-frontslot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
}
.moh-frontslot .card { --cw: 40px; }
.moh-emptyfront {
    display: flex;
    align-items: center;
    justify-content: center;
}
.moh-emptysuit { font-size: 17px; opacity: 0.35; }
.moh-boost {
    font-size: 11px;
    font-weight: 700;
    color: var(--accent);
    white-space: nowrap;
}
.moh-boost.off { color: inherit; opacity: 0.35; font-weight: 400; }
.moh-covered { font-size: 10px; opacity: 0.5; white-space: nowrap; }
/* --- the table: the card just played ----------------------------------------
 * A player could not tell what the opponent had done without reading the log, so
 * the card goes here, big, in the middle of the board. Always present and always
 * the same height, so the market below it never moves.
 * ---------------------------------------------------------------------------- */
/* One ROW, the card beside its two lines of words (see tableSlot): stacked, the
   slot was 120px tall, a sixth of a phone screen. Its height is the card's plus
   the padding whatever it shows, and both text lines are nowrap, so the market
   under it never moves. */
.moh-table {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 6px 12px;
    border-radius: 12px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.25);
}
.moh-tabletext {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
    min-width: 0;
}
.moh-table.moh-table-attack {
    border-color: rgba(224, 83, 60, 0.7);
    background: rgba(224, 83, 60, 0.1);
}
.moh-tablelabel {
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.8;
    white-space: nowrap;
}
.moh-table .card.moh-tablecard { --cw: 58px; }
.moh-tablenote { font-size: 11px; opacity: 0.7; white-space: nowrap; }
.moh-table.moh-table-empty { opacity: 0.55; }
/* --- the market ------------------------------------------------------------
 * FIXED COLUMNS, NOT CONTENT-SIZED ONES. The column is as wide as the widest
 * thing a slot can ever hold, which is the price line with a Diamond picked
 * ("11 free"), not the card. Sized to its content, a column widened by a few
 * pixels the moment a Diamond armed the market, the grid re-centered, and the
 * animator flew every market card sideways on that tap: measured, 9 of 12 cards
 * moved 4 to 6px, and a player saw the market jump.
 *
 * The cards are the SIMPLIFIED atlas (see marketSlot), so the size can come down
 * on a phone without the cards turning into smudges.
 * ---------------------------------------------------------------------------- */
.moh-market {
    --moh-colw: 62px;
    display: grid;
    grid-template-columns: repeat(4, var(--moh-colw));
    justify-content: center;
    gap: 8px 10px;
    padding: 10px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.3);
    border: 1px solid rgba(255, 209, 102, 0.3);
}
.moh-col {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    min-width: 0;
}
.moh-colhead { display: flex; align-items: baseline; gap: 4px; white-space: nowrap; }
.moh-colsuit { font-size: 17px; font-weight: 700; }
/* The four suit inks the sprites use, so a column reads as its suit. */
.moh-colsuit.s1 { color: var(--ink); }
.moh-colsuit.s2 { color: var(--red); }
.moh-colsuit.s3 { color: var(--blue-club); }
.moh-colsuit.s4 { color: var(--red-diamond); }
.moh-colrole {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.7;
}
.moh-slot { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.moh-market .card { --cw: 54px; }
.moh-soldout { opacity: 0.35; }
/* ONE LINE, idle or armed: the price pill, and beside it, with a Diamond picked,
   what the buy costs. The pill is the tallest thing in the line (12px at 1.25 plus
   its border), so reserving its height keeps the line the same in both states. */
.moh-price {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 4px;
    font-size: 11px;
    line-height: 1.25;
    white-space: nowrap;
    min-height: 18px;
}
/* A PILL, not a bare digit. At 11px under a 54px card the price read as one of
   the sprite's own corner indices, which a screenshot made obvious: a border and
   a background are what separate a number the interface is saying from a number
   the card is saying. */
.moh-pricenum {
    font-size: 12px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    min-width: 20px;
    padding: 0 5px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.28);
    background: rgba(0, 0, 0, 0.45);
    text-align: center;
}
.moh-price.off { opacity: 0.4; }
.moh-cost { white-space: nowrap; font-weight: 700; }
.moh-cost.free { color: var(--accent); }
/* "-2" and a health pip: the same red bar the health readouts are drawn in, so
   the number reads as health without a word that would not fit the column. */
.moh-cost.pay { color: #e0533c; }
.moh-cost.pay::after {
    content: '';
    display: inline-block;
    width: 5px;
    height: 10px;
    margin-left: 2px;
    border-radius: 1px;
    background: #e0533c;
    vertical-align: -1px;
}
.card.moh-buyable {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    border-radius: 6px;
}
/* --- the draft ------------------------------------------------------------- */
.moh-draftnote {
    display: flex;
    align-items: baseline;
    justify-content: center;
    flex-wrap: wrap;
    gap: 10px;
    min-height: 22px;
}
.moh-drafthead { font-weight: 700; font-size: 15px; }
/* --- the hand panel + action column (the Le Truc recipe) -------------------
 * Three areas: the hand, the action column beside it, and the viewer's own deck
 * and thrashed pile under the hand (#moh-mypiles). The piles take the height the
 * action column leaves free under the cards, so they cost the panel nothing on
 * a wide screen. On a narrow one everything stacks: hand, actions, piles, so the
 * buttons stay next to the cards.
 * ---------------------------------------------------------------------------- */
.moh-handrow {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 258px;
    grid-template-rows: auto 1fr;
    grid-template-areas:
        'cards actions'
        'piles actions';
    align-items: start;
    column-gap: 16px;
    row-gap: 8px;
}
.moh-handrow #hand-cards { grid-area: cards; }
.moh-actions { grid-area: actions; }
.moh-mypiles {
    grid-area: piles;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 10px;
}
/* A class display rule outranks the UA's [hidden] rule, so restate it. */
.moh-mypiles[hidden] { display: none; }
.card.moh-picked-card {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
}
/* A hand card of a boosted suit wears what it is actually worth. TWO classes,
   so it out-specifies `.card-fly`'s `position: fixed` on an animation ghost -
   and `moh-badged` is listed in ghostStripClasses for the same reason (see the
   eyeBadge note in shared/ui/panels.ts). */
.card.moh-badged { position: relative; }
.moh-val {
    position: absolute;
    right: -5px;
    top: -6px;
    min-width: 19px;
    padding: 1px 3px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-ink);
    font-size: 12px;
    font-weight: 800;
    text-align: center;
    font-variant-numeric: tabular-nums;
}
/* Cards that cannot answer an attack stay legible but read as unavailable. */
.card.moh-idle { opacity: 0.4; }
.moh-actions {
    /* A fixed column, present even when empty, with a reserved height covering
       the tallest state (a picked card: three buttons) so it never pulses as
       turns pass. Buttons only, since the status line says what a picked card
       would do (see renderActions); the notes that used to sit between them made
       the reservation 262px. */
    min-height: 168px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 9px 11px;
    border-radius: 12px;
    border: 1px solid transparent;
}
.moh-actions.filled {
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 209, 102, 0.35);
}
.moh-actions .btn { width: 100%; min-height: 44px; }
/* The draft has no buttons, so it reserves one line of prose rather than the
   duel's tallest menu (see renderActions). */
.moh-actions.moh-actions-draft { min-height: 64px; }
.moh-note { font-size: 12px; opacity: 0.8; line-height: 1.35; }
@media (max-width: 760px) {
    .moh-handrow {
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: auto;
        grid-template-areas: 'cards' 'actions' 'piles';
    }
    /* The buttons go in a row and wrap, so a picked card's three take two rows
       of a phone's width rather than three; the reservation is those two rows. */
    .moh-actions {
        flex-direction: row;
        flex-wrap: wrap;
        align-content: flex-start;
        min-height: 117px;
    }
    .moh-actions .btn { width: auto; flex: 1 1 auto; }
    .moh-actions .moh-note { flex: 1 1 100%; }
    .moh-actions.moh-actions-draft { min-height: 52px; }
}
/* --- the result panel ------------------------------------------------------ */
.panel.moh-summary {
    max-width: 620px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
.panel.moh-summary .result-actions { justify-content: center; flex-wrap: wrap; }
.panel.moh-summary .result-note { text-align: center; }
.moh-deckblock {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    margin: 10px auto;
    padding: 8px 10px;
    border-radius: 10px;
    border: 1px solid var(--panel-border);
}
.moh-deckblock.moh-won {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.08);
}
.moh-deckname { font-weight: 700; }
.moh-deckrow { flex-wrap: wrap; justify-content: center; gap: 3px; }
.moh-deckrow .card { --cw: 30px; }
/* --- the stacked board (one column) ------------------------------------------
 * Everything here is about height, because a player on a phone could not see the
 * board and the hand together: the market alone was 335px and the board 1024px
 * at 390px wide. The seat head goes to two rows (name and health, then the two
 * reach chips), and the opponent's discard and thrashed piles go side by side.
 * ---------------------------------------------------------------------------- */
@media (max-width: 700px) {
    .moh-board { gap: 8px; margin: 6px 0 10px; }
    .moh-seathead {
        flex-direction: row;
        flex-wrap: wrap;
        justify-content: center;
        align-items: center;
        gap: 4px 10px;
    }
    .moh-seathead .moh-chips { flex: 1 1 100%; }
    .moh-grouppair {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        gap: 8px;
    }
}
/* --- the phone breakpoint -------------------------------------------------- */
@media (max-width: 620px) {
    /* 34px is the size ui-conventions puts in the simplified tier outright, and
       the column is the price line's width ("11 free"), not the card's. */
    .moh-market {
        --moh-colw: 54px;
        gap: 5px 6px;
        padding: 7px 5px;
    }
    .moh-market .card { --cw: 34px; }
    .moh-colsuit { font-size: 15px; }
    .moh-colrole { font-size: 9px; letter-spacing: 0.02em; }
    .moh-front { gap: 6px; min-height: calc(34px * 89 / 64 + 16px); }
    .moh-frontslot .card { --cw: 34px; }
    .moh-side { padding: 6px 6px; gap: 4px; }
    .moh-cards .card { --cw: 22px; }
    .moh-cards {
        min-height: calc(22px * 89 / 64);
        max-height: calc(2 * (22px * 89 / 64) + 3px);
    }
    .moh-grouphead { font-size: 10px; margin-top: 1px; }
    .moh-table { padding: 5px 10px; }
    .moh-table .card.moh-tablecard { --cw: 40px; }
    .moh-health.moh-health-big { padding: 3px 8px; }
    .moh-health-big .moh-healthnum { font-size: 22px; }
    /* The progress line keeps both totals as numbers and drops their pips, so it
       is one line rather than two; each seat head still draws its own pips. */
    .progress.moh-scoreboard { gap: 6px; min-height: 30px; }
    .moh-scoreboard .moh-bar { display: none; }
}
/* =========================================================================
   CRYBT (src/crybt/, pages/crybt.mjs)
   Solo cribbage dungeon crawler. Shared classes (.card, .card.simple.back,
   .card.empty, .card.hinted, .panel, .btn, .logline, .winner-banner,
   .result-actions) are reused as-is; everything below is this game's own and
   prefixed `crybt-`.

   THE BOARD IS ONE SENTENCE, READ LEFT TO RIGHT: the Monster, what your cards
   come to, your Hero. The middle column is the largest thing on the page
   because it is the only number the game ever asks you about, and it is live
   from the moment the hand is dealt - a count you would otherwise redo by hand
   every single encounter.

   THE COLOR CHANNELS, and no two share one:
       bone white, larger type   a boss, and its power
       green                     you are level or over, so it falls
       red                       you are short, and by how much
       gold (the shared accent)  something of yours is armed or chosen
       hatched, drained          a card already spent this encounter

   EVERY ROW THAT CAN GROW RESERVES ITS TALLEST HEIGHT, because all of them sit
   above the hand: the altar keeps four slots from the first encounter, the army
   keeps one row, and the versus column is a fixed block. Feeding the altar
   must never walk the hand out from under the pointer.
   ========================================================================= */

/* --- the two lines above the board ---------------------------------------
   Both are shared ids, so both are scoped by a class the renderer puts on
   them. Two lines are reserved for the status line: the offering and shop
   prompts are a good deal longer than "Face the Monster". */
.status.crybt-status { min-height: 52px; }
.progress.crybt-progress {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px 18px;
    opacity: 1;
    min-height: 40px;
}
.crybt-stat {
    display: inline-flex;
    align-items: baseline;
    gap: 5px;
    white-space: nowrap;
}
.crybt-statlabel {
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    opacity: 0.6;
}
.crybt-statval { font-size: 15px; font-weight: 600; }
.crybt-statsub { font-size: 11.5px; opacity: 0.55; }
/* Life is the resource the whole run is made of, so it is the one number on
   this line drawn at a size you can read without looking for it. */
.crybt-lifeval {
    font-size: 22px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.crybt-lifeval.well { color: var(--ink); }
.crybt-lifeval.hurt { color: var(--accent); }
.crybt-lifeval.dire { color: var(--red); }

/* --- the crypt ----------------------------------------------------------- */
.crybt-crypt, .crybt-boons, .crybt-shop, .crybt-piles {
    max-width: 1000px;
    margin: 0 auto 10px;
}
/* The altar and your army share a row: both are piles you are building for
   later, both are one row of small cards, and stacking them cost a whole
   panel of height. They wrap to one column on a narrow screen. */
.crybt-piles {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: stretch;
}
.crybt-piles > .panel { flex: 1 1 340px; margin: 0; min-width: 0; }
.crybt-lane {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: 16px;
}
.crybt-side {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    min-width: 170px;
}
/* THE MONSTER SIDE PINS THE LANE, because it is always the taller of the two
   (measured over four full runs: monster 264px at its worst, Hero 175px), so
   reserving it is what stops the altar, your army, the playmat and your hand
   walking down the page as an encounter progresses. The number comes from
   tmp-measured natural heights with every reservation switched off, not from
   arithmetic over the parts: a guessed 250px was 14px short of a Lich. */
.crybt-monster { min-height: 250px; }
.crybt-sidetitle {
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    opacity: 0.6;
}
.crybt-sidename {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    font-size: 13.5px;
    min-height: 34px;
}
.crybt-bossname {
    font-size: 15px;
    font-weight: 700;
    letter-spacing: 0.03em;
}
.crybt-name { font-variant-numeric: tabular-nums; }
/* TWO LINES RESERVED, and wide enough that the longest boss note fits in two.
   The Lich's and the Ghost's notes are the tallest things in the crypt, and at a
   narrower width they ran to three lines and pushed the whole board down by 14px
   on a boss encounter - past the side's reservation, which is set from a
   measurement of this box (see .crybt-monster). */
.crybt-sidenote {
    font-size: 11.5px;
    opacity: 0.7;
    text-align: center;
    max-width: 200px;
    min-height: 30px;
}
/* The size travels on the CARD's own class rather than on the row, so an
   animation ghost - a clone appended to the body, outside every row here -
   flies at the size it left at instead of at the sheet's base card width. */
.card.crybt-card { --cw: 66px; }
.card.crybt-card.mini { --cw: 36px; }
.crybt-slot {
    display: flex;
    align-items: center;
    justify-content: center;
    --cw: 66px;
}
.crybt-slotlabel {
    font-size: 10.5px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.45;
}
/* A boss is the only card on the board drawn with a ring, because it is the
   only card that does something beyond its number. */
.card.crybt-boss { box-shadow: 0 0 0 2px rgba(242, 239, 233, 0.75), 0 6px 14px rgba(0, 0, 0, 0.45); }
/* The power, and the arithmetic behind it, so a player never takes the number
   on trust. */
.crybt-power {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 3px 10px 4px;
    border-radius: 9px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
}
.crybt-powerval {
    font-size: 21px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}
.crybt-powerterms {
    font-size: 10.5px;
    opacity: 0.65;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
/* The face-down chip: the same shape, holding the terms that are known anyway,
   so the hidden and revealed sides are the same height. Dimmed, because the
   number it is standing in for is the one thing it cannot tell you. */
.crybt-power-unknown { opacity: 0.7; border-style: dashed; }

/* --- the middle column: the count ---------------------------------------- */
.crybt-versus {
    flex: 0 1 230px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 8px 12px 10px;
    border-radius: 12px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.22);
    /* Fixed, so the gap line changing wording never resizes the board. Measured
       natural maximum 144px; the Monster side is always taller, so this never
       decides the lane's height either way. */
    min-height: 146px;
}
.crybt-vslabel {
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    opacity: 0.6;
    text-align: center;
}
.crybt-vsscore {
    font-size: 38px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
/* The addition that makes the total. Two lines reserved: it is a sum now
   ("fifteens 2 + flush 4 = 6"), which can wrap. */
.crybt-vsparts {
    font-size: 12px;
    opacity: 0.85;
    text-align: center;
    min-height: 30px;
    font-variant-numeric: tabular-nums;
}
.crybt-gap {
    font-size: 15px;
    font-weight: 600;
    text-align: center;
    padding: 3px 10px;
    border-radius: 8px;
    min-height: 24px;
}
.crybt-gap-win { color: #8ee6a8; background: rgba(142, 230, 168, 0.1); }
.crybt-gap-lose { color: var(--red); background: rgba(255, 156, 156, 0.1); }
.crybt-gap-blind { opacity: 0.6; font-weight: 400; font-size: 12.5px; }

/* The Broken Mirror's second copy of the Hero, in the hand tray after a fight:
   the SAME card counted again, so it is drawn as a reflection rather than as a
   second card. Safe to carry a transform because an echo has its data-card-id
   stripped, so the animator never clones one. */
.card.crybt-echo { opacity: 0.6; transform: scaleX(-1); }

/* Always painted, and reserving the height of a real button row (38px measured,
   not the 34px a .btn's padding suggests), so the 'offer' phase - which offers
   no button at all - is exactly as tall as the two that do. */
.crybt-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin-top: 10px;
    min-height: 38px;
}
.crybt-hintline { font-size: 12.5px; opacity: 0.7; }

/* --- panel heads, shared by the altar, the army and the playmat ---------- */
.crybt-panelhead {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 2px 10px;
    margin-bottom: 6px;
}
.crybt-paneltitle { margin: 0; font-size: 15px; }
/* THE NOTE GETS ITS OWN LINE AND RESERVES TWO. It changes with the position -
   how many Heroes are corrupting the altar, whether a corpse can be raised - and
   the altar and the army SHARE A ROW, so the taller of the two sets where the
   playmat below them starts. A note growing from one line to two used to move it
   by 18px. */
.crybt-panelnote {
    font-size: 12px;
    opacity: 0.7;
    flex: 1 1 100%;
    min-height: 32px;
}

/* --- the altar ----------------------------------------------------------- */
/* A felt surface, like every other card tray on the site, so the offering
   reads as a place cards are laid rather than as a list of them. */
.crybt-altarrow, .crybt-armyrow {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    padding: 10px;
    border-radius: 10px;
    background: #006400;
    border: 1px solid rgba(0, 0, 0, 0.35);
    /* One row of minis, reserved from the first encounter. */
    min-height: 76px;
}
.crybt-altarrow .crybt-slot, .crybt-armyrow .crybt-slot { --cw: 40px; }
/* The villager completes the offering, and is the one card on the altar that
   was not your choice. */
.card.crybt-villager { box-shadow: 0 0 0 2px var(--accent); }
.crybt-altarline, .crybt-tallyline {
    font-size: 12.5px;
    margin-top: 6px;
}
/* Reserved whether or not it has anything to say (see renderAltar). */
.crybt-altarline { min-height: 18px; }
.crybt-tally { margin-top: 4px; }
.crybt-corrupt { color: var(--red); }
.crybt-darkgod { color: var(--accent); font-weight: 600; }

/* --- your army ----------------------------------------------------------- */
/* A corpse not currently being raised: drained, because it is not a choice
   right now. An overlay rather than a `filter`, which would create a
   containing block and drop animation ghosts out of their fixed position. */
.card.crybt-resting { opacity: 0.72; }

/* --- the playmat of boons ------------------------------------------------ */
.crybt-boonlist {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: 8px;
}
.crybt-boon {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 8px 10px;
    border-radius: 10px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.18);
    opacity: 0.6;
}
/* Carried: the sheet's coin on the playmat. */
.crybt-boon.owned {
    opacity: 1;
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.08);
}
.crybt-boonhead { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.crybt-boonname { font-size: 13.5px; font-weight: 600; }
.crybt-booncost {
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    opacity: 0.8;
    white-space: nowrap;
}
.crybt-boon.owned .crybt-booncost {
    font-size: 10.5px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--accent);
}
.crybt-boontext { font-size: 12px; opacity: 0.85; }
.crybt-boonbuys { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
/* A boon you could afford right now, while the shop is open. */
.crybt-boon:not(.owned):has(.crybt-boonbuys) { opacity: 1; border-color: rgba(255, 255, 255, 0.3); }

/* --- the offering and the shop ------------------------------------------- */
.crybt-shopline { font-size: 13.5px; margin-bottom: 4px; }
.crybt-spend {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 10px 18px;
}
.crybt-spendmeter { display: inline-flex; align-items: baseline; gap: 5px; }
.crybt-spendval {
    font-size: 30px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    color: var(--accent);
}
.crybt-spendlabel {
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    opacity: 0.65;
}
.crybt-healrow {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 8px;
}
.crybt-healnote { font-size: 12.5px; opacity: 0.75; }

/* --- your hand ----------------------------------------------------------- */
/* Scoped to the panel class the page puts on #hand-panel: the id belongs to
   every page on the site, so an unscoped rule here would resize every game's
   hand (six games have shipped that bug). */
.crybt-hand #hand-cards { justify-content: center; gap: 6px; align-items: flex-start; }
/* ONE LINE, reserved. The title changes per phase, and a two-line one moved
   every pile below the tray (see renderHand). */
.crybt-hand #hand-title {
    min-height: 20px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* A card and the price of giving it away, as one unit, so the number stays under
   its own card however the tray wraps. */
.crybt-pick {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
/* The count, as a chip that opens its breakdown. It keeps the big type it had as
   a plain div: platform.css is concatenated FIRST by tools/gen-css.mjs, so a
   game rule beats the shared .scorebd-chip on equal specificity. */
.scorebd-chip.crybt-vsscore {
    display: block;
    margin: 0 auto;
    line-height: 1;
}
/* A sum sitting inside a line of prose (the altar note, the offering tally):
   underlined the way a link is, because that is what says "there is more here"
   in the middle of a sentence. */
.scorebd-chip.crybt-notechip {
    padding: 0 2px;
    font-variant-numeric: tabular-nums;
    text-decoration: underline dotted rgba(255, 255, 255, 0.45);
    text-underline-offset: 2px;
}
.scorebd-chip.crybt-notechip:hover:not(:disabled) {
    border-color: transparent;
    text-decoration-color: var(--accent);
    color: var(--accent);
}
/* What the rest of your hand loses without this card. Painted in every phase and
   reserving its height, blank when there is no choice to make, so the tray does
   not shrink between the offer and the fight and walk the piles below it up. */
.crybt-cost {
    min-height: 15px;
    font-size: 12px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1.25;
}
/* A card holding points up is the expensive one to give away, so it reads as a
   cost. A card carrying nothing is the free throw, and says so quietly. */
.crybt-cost.costly { color: var(--red); }
.crybt-cost.free { opacity: 0.45; }
/* Choosing the card to discard rather than to offer: the same click, a
   different destination, so it gets a different accent. */
.card.crybt-culling.clickable:hover { box-shadow: 0 0 0 2px var(--red); }
/* Held but no longer yours to play: this encounter's cards, after the Hero is
   drawn. Faded rather than hidden, because they are still what you are
   counting with. */
.card.crybt-spent { opacity: 0.75; }

/* --- the end ------------------------------------------------------------- */
.crybt-resultline { font-size: 14px; margin: 4px 0 10px; }

/* --- the log ------------------------------------------------------------- */
.crybt-logwin { color: #8ee6a8; }
.crybt-loghit { color: var(--red); }
.crybt-logboss { color: #f2efe9; }
.crybt-loggod { color: var(--accent); }
/* The weight only in the flat log, the crawl in progress. Inside a finished
   crawl's section only the heading is bold (ui-conventions, "Nothing under a
   collapsed round is bold"), and the color still marks the line. */
#log-entries > .crybt-logboss, #log-entries > .crybt-loggod { font-weight: 600; }

@media (max-width: 760px) {
    /* The Hero drops under the Monster and the count, which is the reading
       order anyway: what is coming, what you have, who fights for you. */
    .crybt-lane { flex-wrap: wrap; gap: 10px; }
    .crybt-versus { order: -1; flex: 1 1 100%; min-height: 0; }
    .crybt-side { flex: 1 1 45%; min-width: 0; min-height: 0; }
    .card.crybt-card { --cw: 62px; }
    .crybt-slot { --cw: 62px; }
    .crybt-vsscore { font-size: 36px; }
    .status.crybt-status { min-height: 66px; }
    .crybt-progress { min-height: 58px; }
}
@media (max-width: 480px) {
    .card.crybt-card { --cw: 52px; }
    .crybt-slot { --cw: 52px; }
    .crybt-boonlist { grid-template-columns: 1fr; }
}
/* ---------- triangulations (solitaire) ---------- */

/* The supply pile and the free-move bank. The supply is the game's clock, so it
   sits above the board rather than in a corner. */
.tri-supply {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    margin: 10px 0;
}
.tri-chip {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 6px 11px;
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.2);
    font-size: 13px;
    white-space: nowrap;
}
.tri-chip-count { font-weight: 700; font-size: 15px; }
/* The supply is nearly gone: the hand is about to be judged as it stands. */
.tri-chip.low { border-color: var(--accent); color: var(--accent); }
/* A banked free pawn move; a spent one is a hollow ring in the same place, so the
   row never changes width. */
.tri-free {
    width: 13px; height: 13px;
    border-radius: 50%;
    border: 2px solid var(--accent);
    background: var(--accent);
}
.tri-free.spent { background: transparent; opacity: 0.4; }

/* The supply pile, which is a button: clicking it opens what is still in it. */
.tri-pile { cursor: pointer; color: inherit; font: inherit; }
.tri-pile.open { border-color: var(--accent); }
.tri-pile-card {
    width: 13px;
    height: calc(13px * 89 / 64);
    border-radius: 2px;
    background-size: 1400% 400%;
}

/* ---- what is still in the supply ---- */

.tri-supply-view { margin: 0 auto 10px; width: fit-content; max-width: 100%; }
.tri-supply-note {
    font-size: 13px; opacity: 0.8;
    margin: 0 auto 10px;
    max-width: 44em; text-align: center;
}
/* The whole deck as a fixed grid - a suit per row, a value per column - with the
   cards you can already see blanked in place. The question it answers is "is that
   card still out there", and only a fixed grid makes that a glance rather than a
   search. */
.tri-supply-grid {
    display: grid;
    grid-template-columns: 22px repeat(13, var(--tri-supply-cw, 27px));
    gap: 3px;
    align-items: center;
    width: fit-content;
    margin: 0 auto;
}
.tri-supply-grid .card { --cw: var(--tri-supply-cw, 27px); }
.tri-supply-rank { font-size: 11px; opacity: 0.7; text-align: center; }
.tri-supply-suit { font-size: 14px; text-align: center; }
.tri-supply-pip { font-size: 15px; }
/* A card already on the table or in your hand: the slot stays, so position still
   identifies it, but the face is gone. The dashed placeholder itself is
   `.card.empty` from the platform; this only quietens it, because 38 of the 52
   slots are blanks and at full strength they are what the eye lands on. */
.tri-supply-gone { opacity: 0.45; }

/* How collecting would change the hand. Gold reads as good everywhere else here,
   so an improvement is gold and a cost is red. */
.tri-delta { font-weight: 700; }
.tri-delta.better { color: var(--accent); }
.tri-delta.worse { color: var(--red); }

/* ---- the two pawn controls ---- */

/* One button per pawn, reading out the card it stands on and the step counts
   that card allows. This is the game's least guessable rule (odd cards move an
   odd number of steps, even an even number), so it is spelled out rather than
   left to be discovered from which cells light up. */
.tri-pawns {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    justify-content: center;
    margin: 4px 0 10px;
}
.tri-pawnbtn {
    display: flex;
    flex-direction: column;
    gap: 3px;
    align-items: flex-start;
    min-width: 172px;
    padding: 8px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.2);
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.tri-pawnbtn:disabled { cursor: default; opacity: 0.55; }
.tri-pawnbtn.sel { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.tri-pawnbtn-who { display: flex; align-items: center; gap: 7px; font-weight: 700; font-size: 14px; }
.tri-pawnbtn-steps { font-size: 12px; opacity: 0.85; }
.tri-pawnbtn-steps.none { color: var(--accent); opacity: 1; }

/* The pawn token, on a cell and in a pawn button. Two colors, because "move the
   other pawn" is an instruction the player gives themselves constantly. */
.tri-token {
    display: flex; align-items: center; justify-content: center;
    width: 22px; height: 22px;
    border-radius: 50%;
    border: 2px solid rgba(0, 0, 0, 0.55);
    font-size: 12px; font-weight: 800;
    color: #10121a;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.tri-token.p0 { background: #ffd166; }
.tri-token.p1 { background: #6fd3e8; }

/* ---- the tableau ---- */

.tri-board {
    --tri-cw: 52px;
    display: grid;
    grid-auto-rows: calc(var(--tri-cw) * 89 / 64);
    gap: 6px;
    width: fit-content;
    /* Centered, with fixed tracks, so the board is a constant size and never
       re-centers mid-game. The column count comes from the shape, as an inline
       grid-template-columns (see renderBoard). */
    margin: 0 auto 12px;
}
.tri-cell {
    position: relative;
    padding: 0;
    border: 0;
    background: none;
    cursor: default;
}
.tri-cell .card { --cw: var(--tri-cw); width: 100%; height: 100%; }
.tri-cell.act { cursor: pointer; }
/* A hole in the layout: no card ever sits here, so it is not drawn at all. */
.tri-gap { }

/* The two pawn tokens ride the bottom corners of their card, so a cell holding
   both (legal, and useless - it triangulates nothing) still shows both. */
.tri-cell-tokens {
    position: absolute;
    left: 3px; right: 3px; bottom: 3px;
    display: flex;
    justify-content: space-between;
    pointer-events: none;
}

/* A card the pawns triangulate: yours to collect. Gold, like `.lit`, but as a
   filled corner flag rather than an outline, so a cell that is BOTH a legal
   destination and a collectible corner shows both at once. */
.tri-corner::after {
    content: '';
    position: absolute;
    top: 0; right: 0;
    border-top: 18px solid var(--accent);
    border-left: 18px solid transparent;
    border-top-right-radius: 4px;
    pointer-events: none;
}

/* The rectangle the two pawns define, drawn faintly so the geometry of a
   triangulation is visible before the player has worked it out. */
.tri-cell.inbox .card { box-shadow: inset 0 0 0 2px rgba(255, 209, 102, 0.28); }

/* Cells the picked-up pawn cannot reach: dimmed so the eye goes to the ones it
   can. Only dimmed, never hidden - the cards it cannot reach this round are
   exactly the ones it is walking towards, so they have to stay readable. */
.tri-cell.faded { opacity: 0.55; }

/* ---- the collect control ---- */

/* Its own row with a reserved height, so the button appearing and disappearing
   between rounds never moves the hand below it. */
.tri-actions { margin-bottom: 6px; }
/* The button lives in its own row with its own reserved height, so a note that
   grows to two lines cannot walk the primary control up the screen. */
.tri-actions-row {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    min-height: 38px;
}
/* Two lines reserved: the note carries the pair, what it costs, and which of the
   two cards finds a home. */
.tri-actions-note {
    font-size: 13px; opacity: 0.85;
    text-align: center;
    max-width: 44em;
    margin: 4px auto 0;
    min-height: 2.6em;
}

/* ---- the hand, fanned into sets and runs ---- */

.tri-hand { margin-top: 10px; }
.tri-hand-head {
    display: flex;
    gap: 10px;
    align-items: baseline;
    justify-content: center;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.tri-hand-head h3 { margin: 0; }
.tri-hand-note { font-size: 13px; opacity: 0.8; }
.tri-groups {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    justify-content: center;
}
.tri-group {
    display: flex;
    flex-direction: column;
    gap: 5px;
    padding: 7px 9px 9px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(0, 100, 0, 0.18);
    /* A long run - and by the last round a hand can hold a run of eight - must
       not push the panel wider than the screen. The box shrinks to the panel and
       its cards wrap inside it. min-width: 0 is what lets a flex item holding
       content shrink at all. */
    max-width: 100%;
    min-width: 0;
}
/* The cards no arrangement can place. Red-edged because at the end of the game
   this box has to be empty, and it is the only thing that decides the game. */
.tri-group.spare { background: rgba(120, 20, 20, 0.22); border-color: #8a3a3a; }
.tri-group-title { font-size: 12px; opacity: 0.85; white-space: nowrap; }
.tri-group.spare .tri-group-title { opacity: 1; }
/* A run's caption names its suit, and the suit is what makes it a run, so the
   glyph is colored. The platform's own suit colors are scoped to #log-entries,
   so a caption outside the log opts in for itself - see
   .kiro/steering/ui-conventions.md on suits. */
.tri-group-title .red { color: var(--red); }
.tri-group-title .red.diamond { color: var(--red-diamond); }
.tri-group-title .club { color: var(--blue-club); }
/* Wraps, and it is the one place on the page that may: the hand is the last
   thing on the screen, so a group growing a second row moves nothing above it -
   and the hand grows all game whatever this does. */
.tri-group-cards { display: flex; gap: 3px; flex-wrap: wrap; justify-content: center; }
/* Read-only and small: the flat rank-over-suit art is what makes a run legible
   at this size (see .kiro/steering/ui-conventions.md). */
.tri-group-cards .card { --cw: 40px; }

/* ---- the result panel's own lines ---- */

.tri-result-groups { margin: 12px 0; }

@media (max-width: 560px) {
    .tri-board { --tri-cw: 44px; }
    .tri-group-cards .card { --cw: 34px; }
    .tri-pawnbtn { min-width: 0; flex: 1 1 46%; }
    /* 13 columns plus a suit label has to fit the screen, so the deck view shrinks
       further than any other card zone here. Well under 34px, which is why these
       are unconditionally the simplified atlas.
       It also stops centering and starts scrolling, which is the pair the steering
       calls for: `justify-content: center` on an overflowing strip hides the left
       end behind an edge nothing can reach. */
    .tri-supply-view { --tri-supply-cw: 21px; overflow-x: auto; }
    .tri-supply-grid { gap: 2px; margin: 0; }
    .tri-supply-note { text-align: left; }
    .tri-actions-note { min-height: 3.9em; }
}
@media (max-width: 380px) {
    .tri-board { --tri-cw: 38px; }
    .tri-supply-view { --tri-supply-cw: 17px; }
}
/* =========================================================================
   Draft Cribbage
   The board is THREE STACKED ROWS - opponent strip, center (deck | left pile |
   right pile | crib), viewer strip - with the live hand-score panel directly
   under it and the hand below that. Every layout declaration below is
   load-bearing: without the flex rules the strips and the center collapse to a
   single card's width and the board becomes one tall vertical line of cards.

   The two discard piles are the game, so they are the biggest thing on the
   board and the only things that grow when they become actionable.
   ========================================================================= */
/* --- a status line that does not resize ----------------------------------- */
/* The line carries two-clause instructions during the draft ("take 2 from the
   deck and throw one back, or take one showing card"), so it is reserved at
   three lines on a phone rather than letting the board jump every half-turn. */
.status.dcrib-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.dcrib-status { min-height: 4.4em; }
}

/* --- the turn counter, at the right-hand end of the scoreboard ------------- */
/* A round has no fixed length in this game, so "how long has this one run" is
   the only sense of pace there is. Quieter than cribbage's count chip, because
   it is context rather than a number being played to. */
.dcrib-turnchip {
    margin-left: auto;
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    padding: 4px 12px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.28);
    border: 1px solid var(--panel-border);
}
.dcrib-turn-label {
    font-size: 11px;
    opacity: 0.7;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
.dcrib-turn-value {
    font-size: 18px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
@media (max-width: 620px) {
    .dcrib-turnchip { margin-left: 0; flex-basis: 100%; justify-content: center; }
}

/* --- the board: three strips STACKED, each spanning the full width -------- */
/* display:flex + column is what stacks the three strips as ROWS; without it
   they - and the cards inside them - collapse into a single vertical line.
   align-items:stretch (the default, stated for emphasis) makes each strip span
   the playarea rather than shrink-wrap to one card. */
.dcrib-table {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    padding: 12px 14px;
    margin-bottom: 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}

/* A seat's strip: name gutter on the left, then the seat's cards in a ROW. */
.dcrib-seatrow {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: calc(44px * 89 / 64);
}
.dcrib-seatrow.active .dcrib-seatname { color: var(--accent); opacity: 1; }
.dcrib-seatname {
    flex: 0 0 92px;
    text-align: right;
    font-size: 13px;
    font-weight: 600;
    opacity: 0.7;
    white-space: nowrap;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
}
.dcrib-dealertag {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 1px 7px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.25);
    opacity: 0.9;
}
/* "knocked" is the one badge that means the round is OVER, so unlike the quiet
   dealer tag it is colored. */
.dcrib-knocktag {
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 1px 7px;
    border-radius: 999px;
    color: var(--felt-dark);
    background: var(--accent);
}
/*
 * CENTERED IN THE TRUE CENTER, not in what is left over.
 *
 * The strip is a name gutter plus the cards, so centering the cards inside their
 * own box puts them half a gutter right of the board's middle - which reads as
 * "nearly centered", the worst of both. The ::after below is an invisible twin of
 * the gutter on the trailing edge, so the cards' box is symmetric about the board
 * and `justify-content: center` lands where the eye expects. It carries no
 * content and no height of its own, so it costs nothing but the width it is
 * balancing.
 */
.dcrib-seatcards {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    flex-wrap: wrap;
    flex: 1 1 auto;
    min-width: 0;
}
.dcrib-seatrow::after {
    content: '';
    flex: 0 0 92px;
}
/* Backs are context, not the thing being played to. */
.dcrib-seatcards .card { --cw: 40px; flex: none; }
.dcrib-seatnote { font-size: 11.5px; opacity: 0.65; }
/*
 * A card in the opponent's hand that the viewer WATCHED them take off a pile.
 * Ringed rather than merely face up, because the row mixes it with backs and the
 * point is that this one is known: a face among backs should look deliberate and
 * not like a rendering slip.
 */
.card.dcrib-known { box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.75); }

/* The center strip: deck | left pile | right pile | crib, in a horizontal ROW.
   flex-wrap lets the sections drop under one another only on a phone; on any
   real width they sit side by side. */
.dcrib-center {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    justify-content: center;
    flex-wrap: wrap;
    padding: 8px 0;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
/* Deck, pile and crib all share the same shape: a card over a small label. */
.dcrib-deck,
.dcrib-pile,
.dcrib-cribchip {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    padding: 6px 8px;
    border-radius: 12px;
    border: 1px solid transparent;
}
.dcrib-deck .card,
.dcrib-pile .card { --cw: 68px; flex: none; }
.dcrib-pile-label {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    text-align: center;
}
.dcrib-pile-name {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.7;
    white-space: nowrap;
}
.dcrib-pile-depth { font-size: 10.5px; opacity: 0.5; white-space: nowrap; }
.dcrib-deck-note { font-size: 10.5px; opacity: 0.7; white-space: nowrap; }
/* The deck is a button when it can be drawn from, so it has to lose the
   browser's button chrome and keep the card's own shape. */
.dcrib-deckface {
    background: none;
    border: 0;
    padding: 0;
    display: flex;
    cursor: default;
}
button.dcrib-deckface { cursor: pointer; }
button.dcrib-deckface:hover .card,
button.dcrib-deckface:focus-visible .card {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
button.dcrib-deckface.hinted .card {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}

/*
 * A PILE READY TO RECEIVE A CARD. The second half of every turn is a choice of
 * pile, so the two legal destinations have to be unmistakable - this is the one
 * decision in the game with no card to click, and an unlit pile is the commonest
 * way to get "why did nothing happen".
 */
.dcrib-pile.droppable {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.1);
}
.dcrib-pile.dropzone-active {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.2);
}
/* The pile a knock has covered: its card is face down and stays that way. */
.dcrib-pile.knocked .card { opacity: 0.85; }
/* The pile that supplies the up-card, once a knock has settled which one it is.
   Every hand in the show counts this card, so it is ringed like cribbage rings
   its starter. */
.dcrib-pile.upcard .card { outline: 2px solid rgba(255, 255, 255, 0.5); }
/*
 * THE TWO RESERVED SLOTS UNDER EVERY PILE: the knock preview, and the throw
 * button. Both come and go several times a turn, so the room is held open
 * whether they are filled or not - see the note in def.ts's pileChip. The
 * heights are the natural heights of what goes in them (a pill of 10-15px text
 * with 2px padding, and a `btn small`), fixed rather than min- so a long
 * up-card label cannot reopen the problem by wrapping.
 */
.dcrib-pile-slot {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 22px;
    width: 100%;
}
.dcrib-pile-slot:empty { pointer-events: none; }
.dcrib-throwbtn { margin-top: 0; }

/*
 * WHAT BURYING HERE WOULD COST, while a knock is armed.
 *
 * Choosing which pile to cover is choosing the up-card everybody scores
 * against, and that is the sharpest decision in the game - so each legal pile
 * says which card the OTHER pile would then contribute and what this hand would
 * score with it. Gold, because it is a points figure.
 */
.dcrib-pile-preview {
    display: flex;
    align-items: baseline;
    gap: 5px;
    padding: 2px 8px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.35);
    border: 1px solid rgba(255, 209, 102, 0.4);
    white-space: nowrap;
    /* Once a card is picked the whole pill is a button (keepButton). */
    font: inherit;
    color: inherit;
}
button.dcrib-pile-preview { cursor: pointer; }
button.dcrib-pile-preview:hover { border-color: var(--accent); }
.dcrib-preview-label,
.dcrib-preview-note {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.65;
}
.dcrib-preview-pts {
    font-size: 15px;
    font-weight: 800;
    color: var(--accent);
    line-height: 1;
}
.dcrib-preview-heels { font-size: 10.5px; color: var(--accent); opacity: 0.9; }

/*
 * The crib: FOUR FACE-DOWN BACKS IN BARELY MORE THAN ONE CARD'S WIDTH, the same
 * treatment and the same reasoning as cribbage's. It is the least informative
 * thing on the board - cards nobody may look at until the show - so a tight
 * stack of small backs says the two facts that matter (how many, face down)
 * without taking room from the piles, which are what the turn is about.
 */
.dcrib-cribcards { display: flex; }
.dcrib-cribcards .card { --cw: 34px; flex: none; }
.dcrib-cribcards .card + .card { margin-left: -27px; }

/* --- the live hand-score panel -------------------------------------------- */
/*
 * THE PANEL THIS LAYOUT IS BUILT AROUND (see def.ts's header). It sits directly
 * between the board and the hand because it is about both: the cards are below
 * it and the two candidate fifth cards are above it.
 */
.panel.dcrib-score {
    display: flex;
    flex-direction: column;
    gap: 7px;
    padding: 9px 12px;
    margin-bottom: 10px;
}
.dcrib-score-head {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 8px;
}
.dcrib-score-title {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.65;
}
.dcrib-score-row {
    display: flex;
    align-items: stretch;
    justify-content: center;
    gap: 8px;
    flex-wrap: wrap;
}
/*
 * A TILE IS ONE BUTTON (scoreTile in def.ts), so the whole face opens the
 * breakdown. It is also a `.scorebd-chip`, whose rules (a 4px padding, a
 * transparent border) come first in the stylesheet and are overridden here
 * property by property, so every property the chip sets is restated below.
 */
.dcrib-scorechip {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 12px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.28);
    border: 1px solid var(--panel-border);
    font: inherit;
    color: inherit;
    cursor: pointer;
}
.dcrib-scorechip:hover:not(:disabled) { border-color: var(--accent); }
.dcrib-scorechip:disabled { cursor: default; }
/*
 * A HAND OF FEWER THAN FOUR CARDS HAS NOTHING TO SHOW, but the panel keeps its
 * room so it does not push the hand down when the fourth card arrives.
 * `visibility`, not `display` or `opacity`: it must hold its height and take no
 * clicks, and it hides its contents from assistive technology too.
 */
.panel.dcrib-score.idle { visibility: hidden; }
/* The chip that is no longer hypothetical: the real up-card, once a knock has
   fixed it, or the four cards actually being kept in the crib phase. */
.dcrib-scorechip.primary {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.12);
}
.dcrib-chip-label {
    display: flex;
    align-items: baseline;
    gap: 4px;
    font-size: 12px;
    opacity: 0.85;
    white-space: nowrap;
}
.dcrib-chip-withlabel {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.6;
}
.dcrib-chip-pts {
    font-size: 21px;
    font-weight: 800;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.dcrib-chip-with { display: flex; align-items: baseline; gap: 4px; }
/* --- the hand, and the controls beside it --------------------------------- */
/* Cards and the action column are a flex row aligned to the TOP: the action
   column's contents changing height never nudges the hand, and picking a card
   lifts it with a transform (layout-neutral), so the row does not reflow. */
.dcrib-handrow {
    /* The action column's width, named once because the empty twin on the other
       side of the cards (below) has to be exactly as wide. */
    --dcrib-actions-w: 236px;
    /* The most cards the tray ever holds: a hand of four plus the card a turn
       draws. The Crib variant raises it (below). */
    --dcrib-hand-max: 5;
    display: flex;
    align-items: flex-start;
    gap: 16px;
    min-height: calc(var(--cw) * 89 / 64 + 8px);
    padding-top: 8px;
}
/*
 * THE HAND CENTERS IN THE WHOLE PANEL, not in whatever is left beside the action
 * column. Centering the cards in the leftover box lands them half a column
 * (126px) left of the middle of the board above them, which reads as misaligned
 * even though every box is individually centered.
 *
 * The fix is the one the opponent's strip uses for its name gutter: an empty twin
 * of the action column's width on the other side of the cards (the ::before), so
 * the cards sit in the true center and the column keeps the right edge. The twin
 * is dropped where the row wraps (below), because there the column is under the
 * cards and nothing needs balancing.
 *
 * THERE IS NOT ALWAYS ROOM FOR THE TWIN, and it has to give way rather than push
 * a card onto a second row. Two column widths plus a full hand is about 900px of
 * row, and the row is only that wide from a ~1300px window up (the log column
 * takes 382px of the page from 1000px). So:
 *
 *  - The cards sit in a tray whose width is FIXED at a full hand (`--dcrib-hand-max`
 *    cards: five mid-turn, seven in The Crib variant, which keeps six).
 *    Four cards and five then center on the same point, so drawing the fifth does
 *    not slide the others sideways. It is a basis, not a minimum, so it grows
 *    into spare room (the symmetric case) and almost never shrinks.
 *  - The twin shrinks 100,000 times faster than the tray (`flex-shrink`), so when
 *    the row is short it is the twin that gives up its width, down to nothing,
 *    and the cards drift off-center by half of what it gave up. That is the
 *    smooth version of the old behavior: exactly centered from ~1300px, half a
 *    column off at ~1000px, and never a wrapped hand.
 *
 * Measured by tools/dcrib-shot.mjs (the `handOffset` it reports) at every width.
 */
.dcrib-handrow::before {
    content: '';
    flex: 0 100000 var(--dcrib-actions-w);
    min-width: 0;
}
/* The hand centers in its own box for the same reason the strips do: a row of
   four cards hugging the left edge under a centered board reads as misaligned. */
.dcrib-handrow #hand-cards {
    flex: 1 0.00001 calc(var(--dcrib-hand-max) * var(--cw)
        + (var(--dcrib-hand-max) - 1) * 8px);
    min-width: 0;
    justify-content: center;
    /*
     * ONE CARD ROW IS RESERVED IN CSS, on top of the runtime reservation every
     * game gets from shared/ui/handspace.ts. That one measures the hand it has
     * SEEN, so it can only hold a height the hand has already had - and in this
     * game a hand starts EMPTY every round and fills one card at a time, so the
     * very first draft of a match grows the panel from nothing to a full card
     * row. Measured with tools/dcrib-measure.mjs: 62px on a phone, which shoved
     * the action column (and the Knock button in it) down the screen in the same
     * frame as the click that drew the card.
     *
     * A row is the honest floor here, not padding: the hand is 4 cards for most
     * of a round and 5 mid-turn, which is one row at every width. The height is
     * derived from `--cw` rather than written down, so it tracks the responsive
     * card size instead of going stale at the next breakpoint.
     */
    min-height: calc(var(--cw) * 89 / 64);
}
/*
 * THE PADDING IS CONSTANT, and only the background and border change with
 * `.filled`. It used to be the padding that appeared too, which is 18px of pure
 * layout movement for a change that is entirely decorative - the column is
 * telling the player whether it is their turn, not resizing for it.
 */
.dcrib-actions {
    flex: 0 0 var(--dcrib-actions-w);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 9px 11px;
    border-radius: 12px;
    border: 1px solid transparent;
    min-height: 78px;
}
.dcrib-actions.filled {
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 209, 102, 0.35);
}
.dcrib-actions .btn { width: 100%; }
/*
 * THE THREE RESERVED SLOTS (see renderActions): a button, a note, and a quiet
 * second note. Each holds its room whether it is filled or not, so the column's
 * height does not change as the turn goes on - which matters most on a phone,
 * where this column sits below the hand and used to drag the panel 63px.
 *
 * The note heights are two and one lines of their own type. `height` rather than
 * min-height on the button slot, because a `btn small` is a known height and the
 * slot must not grow for a long label either.
 */
.dcrib-actions-button {
    display: flex;
    align-items: stretch;
    height: 32px;
}
/*
 * `display: block` IS LOAD-BEARING, not tidying. These are spans, and
 * `min-height` does not apply to a non-replaced INLINE element - so the first
 * version of this reservation was ignored outright and the empty note still
 * collapsed from 32px to 16px. That was most of the 33px the action column was
 * still swinging after the slots went in.
 */
.dcrib-actions-note {
    display: block;
    font-size: 12px;
    opacity: 0.8;
    line-height: 1.35;
    min-height: 2.7em;
}
/*
 * TWO LINES HERE TOO, which is not obvious from the type size: the column is
 * 236px wide on a desktop, about 44 characters a line, and this slot's only
 * message - "Nobody can knock until both hands are full." - wraps to two. A
 * one-line reservation therefore reserved nothing, and the second line appeared
 * and vanished with the message, growing the column 16px and taking the bottom of
 * the hand panel (and the rules block under it) with it. Measured by
 * tools/dcrib-measure.mjs, which named this slot rather than the note above it.
 *
 * The same text is one line on a phone, where the column runs full width, so the
 * reservation costs 16px of dead space there. That is the cheaper side of the
 * trade: on a phone this column is the last thing on the page.
 */
.dcrib-actions-note.quiet {
    opacity: 0.55;
    font-size: 11.5px;
    min-height: 2.7em;
}
/* The Crib variant keeps six cards, and a draw on top of that makes seven. The
   six is the toss, and the seventh is the mid-turn peak that wrapped the tray
   onto a second row when this said six (found by tools/dcrib-measure.mjs). */
.dcrib-handpanel.dcrib-crib .dcrib-handrow { --dcrib-hand-max: 7; }
@media (max-width: 760px) {
    .dcrib-handrow { flex-wrap: wrap; }
    .dcrib-handrow::before { display: none; }
    /* Back to the plain share of the row: the column is below it now. */
    .dcrib-handrow #hand-cards { flex: 1 1 auto; }
    .dcrib-actions { flex: 1 1 100%; }
}
.dcrib-actions .btn.hinted {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}

/* A card picked to throw or to send to the crib: lifted and ringed. The lift is
   a transform, so it is layout-neutral and the hand row does not reflow. */
.card.dcrib-picked {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    transform: translateY(-8px);
}
/*
 * The card just taken off a pile, which the rules forbid throwing straight back
 * ("then discard a DIFFERENT card"). Dimmed like an unplayable card everywhere
 * else in the fleet, so the restriction reads off the board instead of arriving
 * as a refused click.
 */
.card.dcrib-locked { opacity: 0.45; filter: saturate(0.6); }
/* The card (or two) this turn just drew, so a hand that changed under the
   player says which cards are new. Fades on the next repaint by itself. */
.card.dcrib-fresh { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.55); }

/* --- the show (round summary) ---------------------------------------------- */
.panel.dcrib-summary {
    max-width: 560px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
.panel.dcrib-summary .result-actions {
    justify-content: center;
    flex-wrap: wrap;
}
.panel.dcrib-summary .result-note { text-align: center; }
/*
 * THE DEAL BUTTON IS ALWAYS IN VIEW. Lifted over the board, the summary is its
 * own scroll box (max-height min(80vh, 680px)), and a round with three counted
 * hands (The Crib variant) is taller than that on a laptop or a phone, which put
 * "Deal round" below the fold of the dialog. The row sticks to the bottom of the
 * box instead and the hands scroll under it. Scoped to `.result-overlay`, so the
 * inline summary (review, tutorial, final result) is untouched.
 *
 * The negative offsets are the panel's own padding (14px below, 16px at the
 * sides), so nothing scrolls past under or beside the bar. Its background is the
 * dialog's color AS IT APPEARS, which is the panel fill under the shell's dimming
 * backdrop: the backdrop (.result-overlay::before) paints over the panel's own
 * fill, and this bar sits above it. See expeditions/style.css, which found it.
 */
.panel.dcrib-summary.result-overlay .result-actions {
    position: sticky;
    bottom: -14px;
    z-index: 1;
    margin: 12px -16px 0;
    padding: 10px 16px 18px;
    background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)),
        linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
    box-shadow: 0 -6px 10px -8px rgba(0, 0, 0, 0.6);
}
/* The up-card repeated on the end of each counted row, set apart by the '+' and
   ringed: it is the one card in the row that belongs to nobody. */
.dcrib-showplus {
    opacity: 0.5;
    font-weight: 700;
    margin: 0 2px;
    /* Centered on the cards' height, not on the top of the row: the row is a flex
       box of 52px-wide cards (72px tall) and a bare span sat at its top edge. A
       line-height of 1 keeps the glyph's own box from adding a lopsided gap. */
    align-self: center;
    line-height: 1;
}
.card.dcrib-showup { outline: 2px solid rgba(255, 255, 255, 0.35); }
.dcrib-showrow {
    padding: 8px 0 10px;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
}
.dcrib-showhead {
    display: flex;
    align-items: baseline;
    gap: 10px;
    justify-content: center;
}
.dcrib-showname { font-size: 14px; font-weight: 600; }
.dcrib-showpts { font-size: 18px; font-weight: 800; color: var(--accent); }
.dcrib-showcards {
    display: flex;
    gap: 4px;
    justify-content: center;
    align-items: center;
}
.dcrib-showcards .card { --cw: 52px; flex: none; }
.dcrib-showdetail { font-size: 12.5px; opacity: 0.8; }
.result-table.dcrib-result { width: fit-content; gap: 1px; margin: 10px auto 0; }
/*
 * FIXED TRACKS, not `auto`. A content-sized track is as wide as THIS row's
 * content, and the two rows here (the names, the totals) have different content,
 * so the header's "You" and "Bot" sat 17px and 5px off the totals under them. The
 * widths are the widest thing each column holds: "Total", and a seat name, which
 * is "Player 2" at its longest in hotseat. The cells are right-aligned, so
 * the right edges line up whatever the text. (ui-conventions: "Score tables line
 * up by column".)
 */
.result-table.dcrib-result .result-row {
    grid-template-columns: 4em repeat(2, 5.5em);
    gap: 2px 14px;
    align-items: baseline;
}
.result-table.dcrib-result .result-cell {
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.result-table.dcrib-result .result-label {
    text-align: left;
    white-space: nowrap;
}
.result-table.dcrib-result .result-row.totals .result-cell {
    font-size: 20px;
    font-weight: 800;
}

/* --- log ------------------------------------------------------------------ */
.dcrib-logshow { color: var(--accent); }
/*
 * A counted hand in the log, collapsed to its one line until asked about - the
 * cribbage treatment, for the same reason: this is where a hand stays viewable
 * after the next deal has replaced the summary panel, but a log is read for the
 * story of the game and must not become a wall of card faces.
 */
.dcrib-logfold > summary {
    cursor: pointer;
    /* The default marker sits badly against the log's tight line height, and
       the accent-colored line already reads as a heading. */
    list-style: none;
}
.dcrib-logfold > summary::-webkit-details-marker { display: none; }
/* A caret that turns, so a collapsed count looks openable. */
/* Drawn, not the U+25B8 glyph, which sits low against capitals: the same
   triangle as the shared log caret (see `details.loggroup > summary::before`
   in src/shared/styles/platform.css). */
.dcrib-logfold > summary::before {
    content: '';
    display: inline-block;
    width: 0.38em;
    height: 0.44em;
    background: currentColor;
    clip-path: polygon(0 0, 100% 50%, 0 100%);
    vertical-align: 0.13em;
    margin: 0 0.56em 0 0.06em;
    opacity: 0.6;
    transition: transform 0.12s ease-out;
}
.dcrib-logfold[open] > summary::before { transform: rotate(90deg); }
.dcrib-logcards {
    display: flex;
    align-items: center;
    gap: 3px;
    flex-wrap: wrap;
    margin: 4px 0 2px 1em;
}
.dcrib-logcards .card { --cw: 28px; flex: none; }
.dcrib-logfold-detail { margin-left: 1em; font-size: 12px; opacity: 0.8; }
body.motion-off .dcrib-logfold > summary::before {
    transition: none;
}

/* --- phones: keep the center strip on one row ------------------------------ */
/*
 * The center holds four sections in The Crib variant (deck, two piles, crib) at
 * 68px a card, which is ~330px plus gaps and padding - wider than a phone
 * column, so it wrapped and the board grew by a card's height.
 *
 * The piles keep the most room because they are what the turn is about; the
 * deck and the crib give way first, and the seat strips come down to 30px so
 * six backs plus the name gutter still clear a 320px screen.
 */
@media (max-width: 620px) {
    /*
     * THE NAME GOES ABOVE THE CARDS HERE, not beside them, and that is what
     * makes the cards centered on a phone.
     *
     * Beside them it cannot be. The centering on a wide screen works by pairing
     * the fixed 92px name gutter with an empty twin of the same width on the
     * right, so the cards sit in the true middle of the strip. A phone cannot
     * afford a fixed gutter - a long name has to be allowed to give way before
     * the cards do - and a twin cannot balance a gutter whose width is whatever
     * the name happens to be. Centering the cards in the box LEFT OVER beside
     * the name is the wrong center by half the name's width, which measured 41px
     * off at 390px (tools/dcrib-shot.mjs reports it as seatOffset).
     *
     * Stacking sidesteps the arithmetic instead of estimating it: the cards get
     * the full width of the strip, so centering them is just centering them. It
     * costs one line of height a seat, and it is the layout a phone wants anyway.
     */
    .dcrib-seatrow {
        flex-direction: column;
        align-items: center;
        gap: 3px;
        /* A name line plus a row of 30px cards. Reserved, because the strip is
           the name alone until the opponent holds a card and the board grew by
           the difference (14px, once a round) the moment they drew. */
        min-height: calc(30px * 89 / 64 + 20px);
    }
    .dcrib-seatname {
        flex: 0 1 auto;
        flex-direction: row;
        align-items: center;
        justify-content: center;
        gap: 4px;
        font-size: 12px;
        text-align: center;
        /* Long names give way before the cards do. */
        max-width: 100%;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .dcrib-dealertag,
    .dcrib-knocktag { padding: 0 5px; font-size: 9px; }
    /* Nothing to balance once the name is on its own line. */
    .dcrib-seatrow::after { display: none; }
    .dcrib-seatcards { gap: 3px; flex-wrap: nowrap; width: 100%; }
    .dcrib-seatcards .card { --cw: 30px; }
    .dcrib-center { gap: 8px; }
    .dcrib-deck,
    .dcrib-pile,
    .dcrib-cribchip { padding: 4px 5px; }
    .dcrib-pile .card { --cw: 56px; }
    .dcrib-deck .card { --cw: 44px; }
    .dcrib-cribcards .card { --cw: 28px; }
    .dcrib-cribcards .card + .card { margin-left: -23px; }
    .dcrib-preview-pts { font-size: 13px; }
    .dcrib-chip-pts { font-size: 18px; }
    .dcrib-scorechip { padding: 4px 9px; gap: 6px; }
}
/* ==========================================================================
   Chutney
   ==========================================================================
   Three-player climbing game: one Dipper against two Moochers, over a face-up
   Chutney of three cards. Every shared class (.card, .cardrow, .panel, .logline,
   .result-actions, .winner-banner, .eye-badge, ...) is reused as-is, so only what
   is genuinely Chutney's own lives here, all of it prefixed `cht-`.

   THE LAYOUT HAS ONE JOB THE SIBLINGS DO NOT: three seats of asymmetric
   allegiance have to be legible at a glance, and who is alone changes every deal.
   So each chip carries the role as a word, the hand size as a bar of backs, and
   the cards the table has WATCHED that seat take out of the Chutney as faces.
   Four privacy levels sit side by side and each has its own treatment: the
   viewer's hand (faces, full art), a public Chutney card in somebody's hand
   (faces, simplified art, no card id), an unseen card (a back), and a trick set
   aside face down (a number).

   NOTHING ABOVE THE HAND MAY GROW. The seat chips reserve their known-cards row,
   the Chutney and the trick share one cell that is always the taller of the two,
   the Chutney row keeps three slots even while a bidder is holding it, the table
   row scrolls rather than wraps, and the action column holds its width and height
   whether or not it has a button in it. See ui-conventions.md, "Containers must
   not move". */

/* The two card sizes this board sets, as variables on the board rather than as
   rules on `.card`, so the ROWS' reserved heights below are derived from the same
   numbers and cannot drift out of step with them (ui-conventions.md, "Containers
   must not move").

   THEY ARE PLAIN VALUES, and that is load-bearing. The first version wrote
   `--cw: min(var(--cw), 62px)` on the card, reading "no bigger than the fluid
   size" - but a custom property may not reference ITSELF. The cycle makes the
   declaration invalid at computed-value time, `width: var(--cw)` then has nothing
   to fall back on, and every Chutney card and every card on the table rendered at
   ZERO WIDTH. Nothing under the DOM fakes can see that (they have no geometry and
   no CSS at all); a browser shows an empty board. The min() was pointless anyway:
   the root clamp floors at 72px, so 62 and 58 are always the smaller. */
.cht-board {
    --cht-chutneycw: 62px;
    --cht-tablecw: 58px;
    margin-bottom: 12px;
}

/* ---------- the seat chips ----------
   Centered on the board axis, not left-aligned: this row is the board. */
.cht-seats {
    display: flex;
    justify-content: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.cht-seat {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 7px 11px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    min-width: 168px;
}
/* The seat that has to act. One line, so nothing reflows when it moves. */
.cht-seat.active { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
/* The Dipper is playing alone, which is the fact the whole deal turns on. */
.cht-seat.dipper { background: rgba(255, 209, 102, 0.1); }
.cht-seat.mine { border-style: solid; border-width: 2px; padding: 6px 10px; }

.cht-seathead {
    display: flex;
    gap: 7px;
    align-items: baseline;
    white-space: nowrap;
}
.cht-seatname { font-weight: 700; font-size: 13px; }
.cht-role {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.02em;
    padding: 1px 7px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-ink);
    white-space: nowrap;
}
.cht-role.choosing { background: var(--panel); color: var(--ink); border: 1px solid var(--panel-border); }

/* The hand as a bar: a number says four, a bar says "nearly out". The backs
   overlap hard so fifteen of them still fit a chip, and the reserved height is
   one back's height so an emptying hand cannot shorten the chip. */
.cht-backs {
    display: flex;
    align-items: flex-end;
    min-height: calc(18px * 1.39);
    padding-left: 2px;
}
.card.cht-back {
    --cw: 18px;
    margin-left: -11px;
    border-radius: 2px;
    box-shadow: -1px 0 0 rgba(0, 0, 0, 0.35);
}
.card.cht-back:first-child { margin-left: 0; }

.cht-seatmeta {
    display: flex;
    gap: 9px;
    align-items: baseline;
    font-size: 12px;
    white-space: nowrap;
}
.cht-count { font-weight: 700; }
.cht-score { opacity: 0.8; font-variant-numeric: tabular-nums; }

/* Cards this seat is PUBLICLY known to hold. Always rendered at a reserved
   height, empty or not, so the first one to appear cannot push the hand down. */
.cht-seatknown {
    display: flex;
    align-items: center;
    gap: 4px;
    min-height: calc(22px * 1.39);
    font-size: 11px;
    opacity: 0.85;
}
.cht-knownlabel { white-space: nowrap; }
.card.cht-exhibit { --cw: 22px; }

/* ---------- the middle of the table ----------
   ONE GRID CELL, TWO PANES. The Chutney is only live until the Moochers have
   traded, and nothing is played before then, so the two never need the screen at
   the same time. Both panes are placed in the same cell and the renderer makes
   one of them invisible (`.cht-off`).

   `visibility: hidden` is the whole trick, and `hidden` would break it. An
   invisible pane keeps its box, so the cell is always the TALLER of the two and
   the board is the same height in every phase: the hand does not move when an
   auction ends or when the next one starts. `hidden` takes the pane out of the
   layout and would move the hand twice a deal.

   This replaced a separate Chutney panel stacked above the trick, which kept
   three full-size cards on screen for the whole deal after they stopped
   mattering. The saving is that panel's height, less the one line the Chutney
   takes under the trick once it is out of play. */
.cht-center {
    display: grid;
    margin-bottom: 10px;
}
.cht-center > .cht-pane { grid-area: 1 / 1; }
.cht-pane.cht-off { visibility: hidden; }

/* ---------- the Chutney ---------- */
.cht-chutney {
    display: flex;
    flex-direction: column;
    align-items: center;
    /* The cell is as tall as the trick pane, so the Chutney sits in the middle
       of it rather than at the top. */
    justify-content: center;
    gap: 5px;
    padding: 8px 10px;
    border: 1px dashed var(--panel-border);
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.14);
}
.cht-chutneylabel { font-size: 12px; opacity: 0.85; text-align: center; }
.cht-chutneyrow {
    display: flex;
    justify-content: center;
    gap: 7px;
    min-height: calc(var(--cht-chutneycw) * 1.39);
}
.card.cht-chutneycard, .card.cht-slot { --cw: var(--cht-chutneycw); }

/* ---------- the trick ----------
   Scrolls rather than wraps: a climb of three plays would otherwise grow a
   second row and walk the hand panel down the screen mid-trick. It is centered
   with auto margins rather than justify-content, because centering a scrolling
   strip that way clips the left overflow behind an unreachable edge. */
.cht-table { min-width: 0; }
.cht-tablelabel {
    font-size: 12.5px;
    opacity: 0.9;
    text-align: center;
    /* Two lines reserved: the answer list is long for a Quartet trick. */
    min-height: 2.6em;
    margin-bottom: 5px;
}
/* THE RESERVATION IS ONE PLAY STACK, counted part by part, because the first
   version counted only the cards (+20px) and a real browser measured the row
   growing from 101px to 112px the moment the first play arrived, which moved the
   hand 12px on every lead. The +31px is: the stack's padding 8 and border 2, the
   name line 14 (pinned by .cht-playwho's line-height, so it cannot drift with
   the font), the gap 3, and this row's own padding 3, plus 1 of rounding slack. */
.cht-tablerow {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 12px;
    padding-bottom: 3px;
    min-height: calc(var(--cht-tablecw) * 1.39 + 31px);
}
/* CENTERED BY AUTO MARGINS, not by justify-content. A climb of five plays is
   wider than the board and this row scrolls, and `justify-content: center` on a
   scroll container puts the left overflow behind an edge no scrollbar can reach.
   Auto margins on the end children do the same centering while the content fits
   and collapse to 0 the moment it does not, so nothing is ever unreachable. */
.cht-tablerow > :first-child { margin-inline-start: auto; }
.cht-tablerow > :last-child { margin-inline-end: auto; }
.cht-play {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 4px 7px;
    border: 1px solid transparent;
    border-radius: 8px;
    flex: 0 0 auto;
}
/* Whatever has to be beaten. */
.cht-play.best { border-color: var(--accent); background: rgba(255, 209, 102, 0.09); }
.cht-playwho { font-size: 11px; line-height: 14px; opacity: 0.85; white-space: nowrap; }
.cht-playcards { display: flex; gap: 3px; }
.card.cht-tablecard { --cw: var(--cht-tablecw); }

/* The Chutney once it is out of play: one line of text under the trick. Its
   line is reserved in every phase, so it appearing does not grow the pane. */
.cht-aside {
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 6px;
    min-height: 1.5em;
    margin-top: 4px;
    font-size: 12px;
    white-space: nowrap;
}
.cht-asidelabel { opacity: 0.8; }

/* ---------- the hand panel ---------- */
/* The cards and the action column share a row, so the row needs its own
   reservation: a note wrapping to a second line inside the column would
   otherwise make the row taller and take the cards with it. */
.cht-handrow {
    display: flex;
    gap: 12px;
    align-items: flex-start;
}
.cht-handpanel #hand-cards { flex: 1 1 auto; }
.cht-actions {
    display: flex;
    flex-direction: column;
    gap: 6px;
    flex: 0 0 auto;
    width: 210px;
    min-height: 116px;
}
.cht-actions .btn { width: 100%; }
.cht-note { font-size: 12px; opacity: 0.88; line-height: 1.35; }
.cht-note.bad { color: var(--red); opacity: 1; }
.cht-note.hint { color: var(--accent); opacity: 1; }

/* The public-knowledge eye needs a positioning context, and it is stated as two
   classes so it out-specifies .card-fly's `position: fixed` on a ghost. The
   class is listed in ghostStripClasses for the same reason. */
.card.cht-badged { position: relative; }
.eye-badge.cht-eye { right: 2px; bottom: 2px; }

/* ---------- the deal summary (painted into the shared #result-panel) ---------- */
.cht-scoretable {
    display: grid;
    grid-template-columns: auto 1fr 1fr;
    gap: 2px 14px;
    margin: 8px auto;
    width: fit-content;
    font-variant-numeric: tabular-nums;
}
.cht-scorerow { display: contents; }
.cht-scorerow.head .cht-scorecell { font-size: 11px; opacity: 0.7; font-weight: 700; }
.cht-scorecell { padding: 2px 0; font-size: 13px; text-align: right; }
.cht-scorecell.name { text-align: left; font-weight: 600; white-space: nowrap; }
.cht-scorecell.total { font-weight: 800; }
.cht-scorerow.best .cht-scorecell { color: var(--accent); }
.cht-resultnote {
    font-size: 12px;
    opacity: 0.85;
    text-align: center;
    max-width: 44ch;
    margin: 0 auto 8px;
    line-height: 1.4;
}

/* ---------- the log ---------- */
#log-entries .logline.cht-loghigh { font-weight: 700; }
#log-entries .logline.cht-logredeal { opacity: 0.7; font-style: italic; }

/* ---------- phone ----------
   Every reservation above is re-derived here rather than inherited: leaving the
   desktop figures in place is the same jitter with different numbers. The action
   column drops under the cards, which is the one place it may grow, so it keeps
   its own reserved height. */
@media (max-width: 720px) {
    /* THE STATUS LINE WRAPS HERE AND NOWHERE WIDER, so the taller shape is
       reserved here and nowhere else. Measured over a whole match in a real
       browser: at 390px the line runs to two lines during the auction and one
       line during play, and without this every panel below it - the seat chips,
       the Chutney, the table AND the hand - moved 24 to 28px together on the
       phase change. At 660px and 1280px it never left one line, so reserving
       there would only be dead space.
       Two classes, because #status-line is on all 100 pages and a bare rule
       would reserve height on every one of them. */
    .status.cht-status { min-height: calc(2 * 1.45em); }
    .cht-seat { min-width: 0; flex: 1 1 148px; padding: 6px 8px; }
    .cht-seat.mine { padding: 5px 7px; }
    .cht-seats { gap: 6px; }
    .card.cht-back { --cw: 14px; margin-left: -9px; }
    .cht-backs { min-height: calc(14px * 1.39); }
    .card.cht-exhibit { --cw: 20px; }
    .cht-seatknown { min-height: calc(20px * 1.39); }
    /* Both sizes move together with their reservations, because the rows read
       the same two variables. */
    .cht-board { --cht-chutneycw: 46px; --cht-tablecw: 40px; }
    .cht-tablerow { gap: 8px; }
    /* Four lines, not the three the desktop needs: the answer list for a
       Quartet trick is long and the phone is narrow. Measured at 4.2em,
       which is what takes the last 4px of movement out of the board. */
    .cht-tablelabel { min-height: 4.2em; font-size: 12px; }
    .cht-handrow { flex-direction: column; gap: 8px; }
    .cht-actions { width: 100%; min-height: 128px; }
}
/* ---------------------------------------------------------------------------
   Woo
   ---------------------------------------------------------------------------
   Authored here and concatenated into site/webplay.css by tools/gen-css.mjs.
   Never edit the served sheet: `npm run gen` rebuilds it from this file and
   silently reverts anything written straight into it.

   Every selector below is scoped to a `woo-` class. The shared ids
   (#hand-panel, #hand-cards, #status-line, #progress-line, #result-panel,
   #log-entries) are on every page in the site, so a rule naming one of them
   without a game class reaches all ninety games - see the table of eight such
   leaks in .kiro/steering/ui-conventions.md. The hand panel carries
   `woo-handpanel` from pages/woo.mjs and the status line gets `woo-status`
   from the renderer, which is what the two rules below hang off.

   Centering and the hand-height reservation are both site-wide already
   (src/shared/styles/platform.css, src/shared/ui/handspace.ts), so nothing
   here repeats them.
   --------------------------------------------------------------------------- */

.woo-board {
    display: flex;
    flex-direction: column;
    gap: 10px;
    align-items: center;
}

/* ---- NO WOO CARD CARRIES A COSMETIC TRANSFORM ----
   The platform lifts a card translateY(-5px) twice: when it is selected
   (`.card.sel`) and while the pointer is over it (`.card.clickable:hover`).
   Both are overridden here, because the card animator measures every card with
   getBoundingClientRect - which reports the TRANSFORMED box - so a 5px lift
   reads as a 5px MOVE: it flies a ghost and hides the real card for 420ms.
   Every tap blinked. That is what "the animations are jittery" was, and
   tools/woo-jitter.mjs measured it: 214 of 234 ghosts in a 120-step run were
   sub-8px flights of a card that had not gone anywhere, all of them carrying
   `clickable`.

   A CSS cure rather than a "this repaint was only a selection" flag (dracula's
   `selectionRepaint`) because the geometry then never lies to the animator at
   all: the hover lift reproduces the flash on its own, so a flag has to be
   right about every path, whereas a card that never moves cosmetically cannot
   be misread by any of them. Same reasoning as clubfight's `.cf-card.sel`.

   The affordances survive without geometry: `.card.sel`'s own accent outline
   still applies (only its transform is dropped) and gains a glow, and hover
   brightens instead of lifting (caravan's `.cvn-target` treatment). A `filter`
   does not affect a layout box.

   Scoped to Woo's three card containers, never to bare `.card`, so it cannot
   reach another game's page. */
.woo-handpanel .card.sel,
.woo-sharedrow .card.sel {
    transform: none;
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 0 4px rgba(255, 209, 102, 0.25);
}

.woo-handpanel .card.clickable:hover,
.woo-sharedrow .card.clickable:hover,
.woo-tablerow .card.clickable:hover {
    transform: none;
    filter: brightness(1.14);
}

/* ---- the scoreboard: two teams, two pips each ---- */

.woo-score {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 10px;
}

.woo-teampill {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    background: var(--panel);
    /* Reserved, so the accent appearing on a turn change moves nothing. */
    box-shadow: inset 0 0 0 2px transparent;
}

.woo-teampill.active {
    box-shadow: inset 0 0 0 2px var(--accent);
}

.woo-teamname {
    font-weight: 600;
    white-space: nowrap;
}

.woo-pips {
    display: inline-flex;
    gap: 4px;
}

.woo-pip {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 1px solid var(--panel-border);
    background: transparent;
}

.woo-pip.on {
    background: var(--accent);
    border-color: var(--accent);
}

/* ---- the seat ring: partner opposite, opponents either side ----
   Three columns so the middle cell is the partner and the outer two are the
   opponents. At two seats there is one panel and it simply centers. */

.woo-seats {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    grid-template-areas: 'left across right';
    align-items: start;
    justify-items: center;
    gap: 8px;
    width: 100%;
    max-width: 760px;
}

.woo-seats:has(.woo-seat:only-child) {
    grid-template-columns: 1fr;
    grid-template-areas: 'across';
}

.woo-seat {
    grid-area: across;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 6px 10px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    min-width: 128px;
    box-shadow: inset 0 0 0 2px transparent;
}

.woo-seat.woo-left { grid-area: left; }
.woo-seat.woo-right { grid-area: right; }

/* The partner is marked as well as placed: the arrangement carries it on a wide
   screen, and the border carries it once the grid collapses on a phone. */
.woo-seat.partner {
    border-color: var(--accent);
    border-style: dashed;
}

.woo-seat.acting {
    box-shadow: inset 0 0 0 2px var(--accent);
}

.woo-seathead {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.woo-seatname {
    font-weight: 600;
    white-space: nowrap;
}

.woo-seatrole,
.woo-seatnote {
    font-size: 0.78rem;
    opacity: 0.75;
    white-space: nowrap;
}

.woo-seatnote {
    /* Always present, empty when nothing is wanted, so the panel keeps its
       height as the turn goes round the table. */
    min-height: 1.1em;
    line-height: 1.1;
    color: var(--accent);
    opacity: 1;
}

.woo-seatcount {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.woo-seatcount.low {
    color: var(--accent);
    font-weight: 600;
}

/* Thirteen backs, OVERLAPPED into a fan.
   Two reasons. Laid out side by side they ran the full width of the panel and the
   three seats' rows met, so the ring read as one continuous strip of forty backs
   rather than as three hands - and the count is printed above them anyway, so the
   individual cards are texture, not information. And a row that wraps grows as it
   goes, which would push the table down the screen mid-round.
   `margin-left` on every card but the first, so the fan is as wide as one card
   plus a sliver each, and the row can never wrap or scroll. */
.woo-seatcards {
    --cw: 22px;
    gap: 0;
    flex-wrap: nowrap;
    justify-content: center;
    /* Reserved at the height of the TALLEST thing the row can hold, which is a
       known face (26px) rather than a back (22px). Reserving the back's height
       instead let the panel grow 5px the first time a card became known, and a
       seat panel sits ABOVE the table, so the whole board stepped down with it
       (measured by tools/woo-measure.mjs at 744px and 1280px). */
    min-height: calc(26px * 1.39);
    align-items: center;
    max-width: 100%;
}

.woo-seatcards .woo-back + .woo-back {
    margin-left: -15px;
}

/* Known cards sit BEFORE the fan of backs and are not overlapped: they are the
   readable part of the row, so they get their own width. Keep this in step with
   the row's reservation above. */
.woo-seatcards .woo-seatknown {
    --cw: 26px;
    margin-right: 3px;
}

/* The eye badge marks a card the whole table has seen arrive. `.card` needs the
   positioning context for it; the platform sheet styles `.eye-badge` itself. */
.card.woo-badged,
.card.woo-seatknown {
    position: relative;
}

.eye-badge.woo-eye {
    right: 2px;
    bottom: 2px;
}

.woo-empty {
    font-size: 0.82rem;
    opacity: 0.6;
    font-style: italic;
}

/* ---- the table: the climb, and the half-built play ----
   Table cards are drawn from the SIMPLIFIED atlas (`simple` at the call site in
   def.ts). They are read-only, and they fall to 34px at the phone breakpoint,
   which is the unconditional simplified tier in the ui-conventions table: at
   that size the full illustration is a colored smudge whichever resolution it
   comes from. Board and hand cards stay full art, because those are the ones
   the player picks up. */

.woo-table {
    width: 100%;
    max-width: 760px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    align-items: center;
    padding: 10px;
    border-radius: 10px;
    background: var(--felt-dark);
}

.woo-tablelabel {
    text-align: center;
    font-size: 0.9rem;
    /* Two lines reserved: the label says what is on the table AND how to answer
       it, which wraps to two lines at most widths and one on a wide screen. */
    min-height: 2.6em;
    line-height: 1.3;
}

.woo-tablerow {
    /* A SCROLLING STRIP, not a wrapping row. A climb runs to several melds, and a
       row that wraps grows as it goes: measured at 390px it swung 125px and
       walked the hand panel 91px down the screen mid-round. One row, always, with
       the newest end scrolled into view by the renderer. */
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    justify-content: flex-start;
    align-items: flex-start;
    gap: 10px;
    /* One row plus its caption and the horizontal scrollbar, so the strip
       never changes height as melds pile up. */
    min-height: calc(46px * 1.39 + 2.6em);
    width: 100%;
}

/* Centered while the strip is short enough not to scroll, which is most of a
   round, so a single meld still sits in the middle of the table. */
.woo-tablerow > :first-child { margin-left: auto; }
.woo-tablerow > :last-child { margin-right: auto; }

.woo-play {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 5px 7px;
    border-radius: 8px;
    border: 1px solid transparent;
}

.woo-play.top {
    border-color: rgba(255, 255, 255, 0.65);
    background: rgba(255, 255, 255, 0.08);
}

.woo-playwho {
    font-size: 0.8rem;
    white-space: nowrap;
    opacity: 0.9;
}

.woo-playcards {
    --cw: 46px;
    gap: 3px;
}

/* The partial is NOT a meld, and is drawn as its own thing: dashed, tilted, and
   captioned with who owes the rest. A player who reads it as a finished play has
   misread the position. */
.woo-partial {
    border-style: dashed;
    border-color: rgba(255, 255, 255, 0.8);
    background: rgba(255, 255, 255, 0.12);
}

/* `rotate`, never `transform`: the hover rule above sets `transform: none`,
   which used to straighten the tilt (ui-conventions, "A sideways card turns
   with rotate"). Scoped under the play row so a flight ghost is not tilted
   (def.ts also strips the class from ghosts). */
.woo-playcards .woo-partialcard {
    rotate: -4deg;
}

/* ---- the shared cards: the draft, the board, a reclaim ---- */

.woo-shared {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    width: 100%;
    max-width: 760px;
    /* Reserved: the title wraps to two lines at narrow widths and the row comes
       and goes with the phase, which together swung this 42px. */
    min-height: calc(56px * 1.39 + 2.6em);
}

.woo-sharedtitle {
    font-size: 0.85rem;
    opacity: 0.75;
    text-align: center;
    min-height: 1.2em;
}

.woo-sharedrow {
    --cw: 56px;
    gap: 6px;
    justify-content: center;
}

.woo-spare,
.woo-reclaimcard {
    box-shadow: 0 0 0 2px var(--accent);
}

/* The returning cards ride the table strip, dashed like the partial they were:
   they are not a meld and nobody is beating them. */
.woo-reclaim {
    border-style: dashed;
    border-color: rgba(255, 255, 255, 0.8);
    background: rgba(255, 255, 255, 0.12);
}

/* ---- the hand panel and the action column ---- */

.woo-handpanel .woo-handrow {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 12px;
}

.woo-handpanel #hand-cards {
    flex: 1 1 60%;
    min-width: 0;
}

/* Cards that cannot extend the current selection. Fading is complete here, not
   just a hint: a sub-multiset of a reachable meld is itself reachable, so tapping
   one card at a time can never lock a player out of a legal selection. */
.woo-handpanel #hand-cards .card.dim,
.woo-sharedrow .card.dim {
    opacity: 0.38;
    filter: grayscale(0.5);
    cursor: default;
}

.woo-actions {
    flex: 1 1 250px;
    min-width: 230px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
}

.woo-actionnote {
    font-size: 0.84rem;
    opacity: 0.85;
    line-height: 1.3;
}

.woo-actionnote.bad {
    color: var(--red);
    opacity: 1;
}

.woo-couldbe {
    font-style: italic;
}

.woo-hint {
    color: var(--accent);
    opacity: 1;
}

/* ---- status, and the round summary ---- */

/* Both scoped through the class the renderer puts on the shell-owned line,
   because #status-line and #progress-line belong to every page in the site. */
.woo-status.yours {
    font-weight: 600;
    color: var(--accent);
}

.woo-progress {
    /* Two lines reserved. The line carries a RECAP of the last event, so its
       length varies with the position; unreserved it swung 20px at 390px and
       pushed the whole board down with it. */
    min-height: 2.8em;
}

.woo-resultline {
    margin: 6px 0;
}

.woo-scoretable {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 8px 0;
}

.woo-scorerow {
    display: grid;
    grid-template-columns: minmax(8em, 1fr) minmax(8em, 1fr) auto;
    gap: 8px;
    align-items: baseline;
}

.woo-scorecell.label {
    font-weight: 600;
}

.woo-scorecell.points {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* ---- the log ---- */

#log-entries .woo-logout {
    font-weight: 600;
}

#log-entries .woo-loggone {
    opacity: 0.7;
}

/* ---- the closing note of Meld types in the Cheat_Sheet ----
   The list itself is the shared `.reflist`. Scoped to the note's own class
   rather than to the collapsible around it, so the same rule styles the
   in-game Rules summary, the Reference_Dialog, and the woo/rules/ page. */

.woo-meldnote {
    opacity: 0.8;
}

/* ---- the phone ----
   Every figure above is re-derived here rather than inherited: leaving the
   desktop numbers in place is the same bug with different values. */

@media (max-width: 720px) {
    .woo-seats {
        /* Two opponents side by side above a full-width partner, so the partner
           is still the one nearest the player's own hand. */
        grid-template-columns: 1fr 1fr;
        grid-template-areas:
            'left right'
            'across across';
    }

    .woo-seat {
        min-width: 0;
        width: 100%;
        padding: 5px 7px;
    }

    .woo-seatcards {
        --cw: 17px;
        /* The known face again, scaled with the backs (the same ~1.18 ratio), and
           the row reserved for it - see the base rule. */
        min-height: calc(20px * 1.39);
    }

    .woo-seatcards .woo-seatknown {
        --cw: 20px;
    }

    .woo-seatcards .woo-back + .woo-back {
        margin-left: -12px;
    }

    .woo-tablelabel {
        /* FIVE lines at this width. It carries the longest prose on the board -
           what the table demands, or who owes the partial and what it could still
           become - and that is deliberately here rather than in the strip below,
           which cannot be reserved (see .woo-tablerow). */
        min-height: 6.5em;
    }

    .woo-playcards {
        --cw: 34px;
    }

    .woo-tablerow {
        min-height: calc(34px * 1.39 + 2.6em);
    }

    .woo-sharedrow {
        --cw: 42px;
    }

    .woo-shared {
        min-height: calc(42px * 1.39 + 2.6em);
    }

    .woo-progress {
        /* Three lines at this width: the same sentence, wrapped further. */
        min-height: 4.2em;
    }

    .woo-status {
        /* Two lines. "Finish AI partner's meld, or pass." fits one line on a
           desktop and wraps here, and the difference was moving the whole board
           24px between phases. `.status` reserves one line site-wide. */
        min-height: 2.9em;
    }

    .woo-actions {
        flex: 1 1 100%;
        min-width: 0;
    }

    .woo-scorerow {
        grid-template-columns: 1fr auto;
        grid-template-areas:
            'label points'
            'cards cards';
    }

    .woo-scorecell.label { grid-area: label; }
    .woo-scorecell.points { grid-area: points; }
    .woo-scorecell:not(.label):not(.points) { grid-area: cards; }
}
/* =========================================================================
   WYSIWYG (wysiwyg, wys-)
   Layout intent: the board reads top to bottom as the three questions a
   player asks. First the CONTRACT strip - both hand evaluations, the Mods
   they produced, the trump and the Goal - because in this game the
   arithmetic set before a card is played is what the hand is about. Then
   the two face-up cards beside the trick, side by side, since choosing
   between them is the move the title names. Then the standing: each pile
   drawn as marks counted in fives the way the rules count them, and the
   cards the viewer knows of the other hand.
   Nothing above the hand may change height mid-trick, and a settled trick
   stays on display until the next lead, so the caption reserves its line.
   ========================================================================= */
/* Two lines' worth, always: the auction wording is long ("Bid 16 or more, or
   pass and let AI play for a Goal of 21"). */
.status.wys-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.wys-status { min-height: 4.4em; }
}
.wys-scorename { margin-right: 6px; }
.wys-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }
/* --- the board ------------------------------------------------------------ */
.wys-board {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-bottom: 12px;
}
/* --- the contract strip: Evals, Mods, trump, Goal ------------------------- */
/* The loud line on the board, because it is the one thing a new player has to
   keep in view: their Goal is not their bid. Muted while the auction is still
   running, accented once there is a contract to play. */
.wys-contract {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    padding: 7px 12px;
    border-radius: 12px;
    border: 1px solid rgba(255, 209, 102, 0.55);
    background: rgba(255, 209, 102, 0.12);
}
.wys-contract.wys-bidding {
    border-color: var(--panel-border);
    background: rgba(0, 0, 0, 0.16);
    opacity: 0.9;
}
.wys-evallabel {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    opacity: 0.6;
    white-space: nowrap;
}
.wys-evals { display: flex; gap: 8px; flex-wrap: wrap; }
/* One chip per seat: name, the Eval they announced, and the Mod it bought
   them. The Mod is the number that matters, so it is the bright one. */
.wys-evalchip {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    padding: 2px 9px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.28);
    white-space: nowrap;
}
.wys-evalname { font-size: 12px; opacity: 0.75; }
.wys-evalnum { font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }
.wys-modnum {
    font-size: 12px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    padding: 0 5px;
    border-radius: 5px;
    background: rgba(255, 255, 255, 0.10);
}
.wys-evalchip.mine .wys-modnum { color: var(--accent); }
/* The contract itself, pushed to the far end of the strip. */
.wys-deal {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-left: auto;
}
.wys-dealtrump { font-size: 14px; font-weight: 700; white-space: nowrap; }
.wys-dealgoal {
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    padding: 2px 9px;
    border-radius: 999px;
    background: var(--accent);
    color: #1a1a1a;
    white-space: nowrap;
}
.wys-flag {
    font-size: 10px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 2px 7px;
    border-radius: 999px;
    white-space: nowrap;
}
.wys-flag.wys-doubled {
    background: rgba(239, 71, 111, 0.85);
    color: #fff;
}
.wys-flag.wys-gamebid {
    background: rgba(17, 138, 178, 0.9);
    color: #fff;
}
/* --- the pair and the trick, side by side --------------------------------- */
/* The two face-up cards ARE the decision, and the trick is the question they
   answer, so they share a row and neither grows. The standing takes the width
   underneath, because the open hand it prints is 13 cards long. */
.wys-playrow {
    display: flex;
    gap: 12px;
    align-items: stretch;
    flex-wrap: wrap;
}
.wys-exposedbox,
.wys-trickbox,
.wys-standingbox {
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
.wys-exposedbox,
.wys-trickbox { flex: 0 0 auto; }
.wys-standingbox { flex: 1 1 100%; }
.wys-boxtitle {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.55;
    margin-bottom: 6px;
}
/* What a trick is worth changes three times in a hand (1, then 2, then 3 on
   the last), and it is the whole reason the second half matters more, so the
   Trick box wears the figure rather than burying it in a tooltip. */
.wys-worthchip {
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.04em;
    background: var(--accent);
    color: #1a1a1a;
}
/* The pair: two cards, both face up, both clickable when the choice is the
   viewer's. Bigger than the trick's cards on purpose - this is the game's
   signature moment and the player is comparing two faces. */
.wys-pair { display: flex; gap: 8px; }
.wys-pair .card.wys-prize { --cw: 56px; }
.wys-pair .card.wys-prize.clickable:hover {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
    border-radius: 6px;
}
.wys-stocknote { font-size: 12px; opacity: 0.75; margin-top: 6px; max-width: 200px; }
/* Second half: the box says the stock is gone AND states the open-hand fact
   once, which is why there are no per-card eyes from here on. */
.wys-stockdone {
    font-size: 12px;
    max-width: 220px;
    color: #ffd166;
    font-weight: 600;
}
/* The trick: a gutter of seat names, one card cell per seat, and a "led"
   flag column - fixed grid, so nothing shifts as cards land. */
.wys-trick-grid {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 10px;
    row-gap: 6px;
}
.wys-seat {
    justify-self: end;
    font-size: 13px;
    font-weight: 600;
    opacity: 0.7;
    white-space: nowrap;
}
.wys-cell { display: flex; justify-content: center; }
.wys-cell .card { --cw: 50px; }
/* Stagger the leader's card left, so who led reads at a glance. */
.wys-cell.wys-led { transform: translateX(-8px); }
.wys-flagcell {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.5;
    min-width: 26px;
}
.card.wys-winning {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    border-radius: 6px;
}
/* A settled trick stays on display until the next lead, but visibly OVER. */
.wys-cell .card.wys-settled { opacity: 0.55; }
/* One line, reserved: "Trick 7: AI takes it for 2 marks". */
.wys-caption {
    margin-top: 8px;
    font-size: 13px;
    opacity: 0.8;
    min-height: 1.4em;
}
/* --- the standing: marks banked and the PUBLIC cards ---------------------- */
.wys-track { margin-bottom: 8px; }
.wys-track:last-child { margin-bottom: 0; }
.wys-track-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 3px;
}
.wys-track-name { font-size: 13px; font-weight: 700; }
.wys-track-marks {
    font-size: 12px;
    opacity: 0.8;
    font-variant-numeric: tabular-nums;
}
.wys-declarerflag {
    font-size: 10px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--accent);
    color: #1a1a1a;
}
/* The pile, drawn as a pile. A mark IS a banked card, and the rules count
   them in crosswise groups of five, so every fifth tick gets a gap: the
   count stays readable at a glance up to 40 without printing 40 numbers. */
.wys-pile {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 14px;
    margin: 2px 0 4px;
}
.wys-mark {
    display: block;
    width: 3px;
    height: 13px;
    border-radius: 1px;
    background: rgba(255, 209, 102, 0.75);
}
.wys-mark.wys-mark-five { margin-right: 7px; }
/* The feature row: what the viewer knows of the other hand. */
.wys-knownrow {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    margin-top: 2px;
    padding: 4px 7px;
    border-radius: 8px;
    background: rgba(255, 209, 102, 0.10);
    border: 1px solid rgba(255, 209, 102, 0.35);
}
.wys-known-label {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.8;
}
.wys-knownpile { display: inline-flex; gap: 4px; flex-wrap: wrap; }
/* Big enough to READ at a glance, drawn from the simplified atlas: these
   cards are the point of the row, not decoration on it. */
.wys-knownpile .card { --cw: 44px; position: relative; }
.wys-known-none { font-size: 12px; opacity: 0.55; margin-top: 2px; }
/* Second half: the row becomes the opponent's whole open hand, so it gets
   room to wrap and the brighter frame. */
.wys-knownrow.wys-openhand {
    background: rgba(255, 209, 102, 0.14);
    border-color: rgba(255, 209, 102, 0.5);
    align-items: flex-start;
}
.wys-knownrow.wys-openhand .wys-knownpile { gap: 3px; }
/* --- the hand ------------------------------------------------------------- */
/* A card the follow rule excludes right now: visible, inert, dimmed. */
.card.wys-mute { opacity: 0.45; }
/* Trumps in the viewer's hand wear a quiet gold keel. */
.card.wys-trumpcard { box-shadow: 0 3px 0 -1px rgba(255, 209, 102, 0.55); }
/* A hand card the OPPONENT knows: it wears the site's shared eye badge (see
   .eye-badge in webplay.css, and eyeBadge in shared/ui/panels.ts), the same
   way public knowledge is marked in German Whist, Cuttle, Cupid, Evolution
   Poker, Bubble Gum and The Crew. Here that is a card taken from a face-up
   pair. From the second half every card is public, so the badges come off
   and the stock box states the fact once instead.
   `position: relative` is a TWO-class rule so it out-specifies `.card-fly`'s
   `position: fixed`, and `wys-seen` is listed in this game's
   ghostStripClasses - a ghost that kept the class landed in document flow. */
.card.wys-seen { position: relative; }
/* --- the auction bar ------------------------------------------------------ */
/* Every button prints the CONSEQUENCE, so the bar is a row of two-line
   buttons rather than a row of numbers. */
.wys-actions {
    display: flex;
    align-items: stretch;
    gap: 10px;
    flex-wrap: wrap;
    padding: 9px 14px;
    margin-bottom: 12px;
    border-radius: 12px;
    border: 1px solid rgba(255, 209, 102, 0.55);
    background: rgba(255, 209, 102, 0.12);
}
.wys-actions .btn.wys-bid {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    line-height: 1.15;
}
/* The Goal a bid would set: the number that decides whether the bid is sane,
   so it rides on the button rather than in a tooltip. */
.wys-bidgoal {
    font-size: 10px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.75;
    font-variant-numeric: tabular-nums;
}
.wys-actions .btn.wys-double { white-space: nowrap; }
/* --- the hand summary ----------------------------------------------------- */
.wys-summary {
    max-width: 480px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
.wys-summary .result-table { margin-top: 10px; }
.result-cell.wys-won { color: var(--accent); font-weight: 800; }
/* The shared .result-row auto-fits 180px seat columns, which WRAPS the
   second seat onto its own row inside this narrow two-seat summary (and
   inside the log dropdowns): pin the shape to label + two seats. */
.wys-result .result-row {
    grid-template-columns: minmax(120px, auto) 1fr 1fr;
}
/* --- the log -------------------------------------------------------------- */
/* Auction moves read as a thread rather than as events, so they are indented
   under the deal they belong to. */
.logline.wys-logmove { padding-left: 14px; opacity: 0.9; }
@media (max-width: 760px) {
    .wys-exposedbox,
    .wys-trickbox { flex: 1 1 auto; }
    .wys-stocknote,
    .wys-stockdone { max-width: none; }
}
@media (max-width: 560px) {
    .wys-pair .card.wys-prize { --cw: 48px; }
    .wys-cell .card { --cw: 44px; }
    .wys-knownpile .card { --cw: 38px; }
    .wys-deal { margin-left: 0; }
}

/* The Examples section's 13-card hand (pages/wysiwyg.mjs). Beside its text it
   squeezes the text into a narrow column even on a desktop, so the row stacks
   at every width, as the shared .reflist rule already stacks a long example on
   a phone; and on a phone the card is smaller, so the whole hand (13 cards and
   3 gaps) fits a 390px screen on one line. */
.cheatsheet ul.reflist.wys-examples > li:has(> .cardex > :nth-child(6)) {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: 'name' 'what' 'ex';
    row-gap: 3px;
}
.cheatsheet ul.reflist.wys-examples > li:has(> .cardex > :nth-child(6)) > .reflist-name { grid-row: auto; }
.cheatsheet ul.reflist.wys-examples > li:has(> .cardex > :nth-child(6)) > .cardex {
    justify-self: start;
    margin-top: 2px;
}
@media (max-width: 560px) {
    .cheatsheet .wys-examples .cardex { --cw: 20px; }
}

/* --- centering ------------------------------------------------------------ */
/* Everything on the board sits on one axis. */
.wys-contract { justify-content: center; }
.wys-deal { margin-left: 0; }
.wys-playrow { justify-content: center; }
.wys-boxtitle,
.wys-stocknote,
.wys-stockdone,
.wys-caption { text-align: center; }
.wys-exposedbox,
.wys-trickbox { display: flex; flex-direction: column; align-items: center; }
.wys-pair { justify-content: center; }
.wys-trick-grid { justify-content: center; }
.wys-track-head { justify-content: center; }
.wys-pile { justify-content: center; }
.wys-knownrow { justify-content: center; }
.wys-knownpile { justify-content: center; }
.wys-known-none { text-align: center; }
.wys-actions { justify-content: center; }

/* --- the working behind the contract: a modal ------------------------------ */
.wys-whybtn { margin-left: 4px; }
/* z-index 500 keeps it under the pass-and-play handoff gate (600) and the
   card-flight layers (900/1000). */
.wys-whymodal {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: rgba(0, 0, 0, 0.6);
}
.wys-whycard {
    background: var(--felt-dark);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
    padding: 16px 20px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-width: min(520px, calc(100vw - 32px));
    max-height: calc(100vh - 48px);
    overflow-y: auto;
}
.wys-whyhead {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    font-weight: 700;
}
.wys-why { font-size: 14px; line-height: 1.5; }
.wys-why-line { margin: 0 0 8px; }
.wys-why-line:last-child { margin-bottom: 0; }
/* The bid to beat, while the auction runs. */
.wys-curbid {
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    padding: 2px 9px;
    border-radius: 999px;
    background: var(--accent);
    color: #1a1a1a;
    white-space: nowrap;
}

/* --- suits -------------------------------------------------------------- */
/* A very small gap where the suit changes in a row sorted by suit. */
.card.wys-suitbreak { margin-left: 6px; }

/* --- the hand: one row on a wide screen ------------------------------------ */
/* Thirteen cards at the site size run wider than the panel, so the tray used
   to wrap to two rows and back as the page gained or lost a scrollbar. Size
   the cards from the tray's own width instead: 12 gaps of 4px plus up to 3
   suit breaks of 6px. */
.wys-handpanel { --wys-handcw: var(--cw); }
@media (min-width: 561px) {
    .wys-handpanel #hand-cards { container-type: inline-size; gap: 4px; flex-wrap: nowrap; }
    .wys-handpanel #hand-cards .card {
        --cw: min(var(--wys-handcw), calc((100cqw - 66px) / 13));
    }
}

/* --- the unseen cards ------------------------------------------------------ */
.wys-unseen {
    margin-bottom: 12px;
    padding: 6px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
.wys-unseentoggle {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: 100%;
    background: none;
    border: 0;
    padding: 4px 0;
    color: inherit;
    font: inherit;
    cursor: pointer;
}
.wys-unseentoggle .wys-boxtitle { margin: 0; }
.wys-unseencaret { font-size: 12px; opacity: 0.6; }
/* 4 suits by 13 ranks. A card that has gone leaves its place empty, so each
   column is one rank. The cards are sized from the box's own width. */
.wys-unseengrid {
    display: grid;
    grid-template-columns: repeat(13, var(--ucw, 28px));
    justify-content: center;
    gap: 2px;
    margin: 6px 0 4px;
}
.wys-unseengrid { --ucw: 28px; }
.wys-unseengrid > .card { --cw: var(--ucw); }
.wys-unseenslot {
    width: var(--ucw);
    height: calc(var(--ucw) * 89 / 64);
    border-radius: 4px;
    border: 1px dashed rgba(255, 255, 255, 0.10);
}
/* Known to be in the other hand: outlined in gold. */
.card.wys-heldcard {
    outline: 2px solid var(--accent);
    outline-offset: -1px;
    border-radius: 4px;
}
.wys-unseenkey { font-size: 12px; opacity: 0.7; text-align: center; margin-bottom: 4px; }
@media (max-width: 560px) {
    .wys-unseengrid { --ucw: 22px; gap: 1px; }
}

/* On a phone the hand is two rows of seven and six, whatever the page width,
   so it never flips between three rows and two. The 30px holds the 6 gaps of
   3px and up to 3 suit breaks of 4px. */
@media (max-width: 560px) {
    .wys-handpanel #hand-cards { container-type: inline-size; gap: 3px; }
    .wys-handpanel #hand-cards .card {
        --cw: min(var(--wys-handcw), calc((100cqw - 30px) / 7));
    }
    .card.wys-suitbreak { margin-left: 4px; }
}
/* ---------- optrita (Optrita: Lines, solitaire) ---------- */

/* The status line carries a recap of the trick just played and the prompt, so
   it is reserved at two lines (three on a phone) and never walks the board
   down the screen. */
.status.otl-status { min-height: 2.8em; }

/* ---- the score: two bars toward each side's target ---- */
.otl-score {
    display: flex;
    flex-direction: column;
    gap: 4px;
    width: 100%;
}
.otl-scorerow {
    display: grid;
    grid-template-columns: 44px 1fr 64px;
    align-items: center;
    gap: 8px;
    font-size: 14px;
    white-space: nowrap;
}
.otl-scorewho { font-weight: 700; }
.otl-scorebar {
    height: 10px;
    border-radius: 5px;
    background: rgba(0, 0, 0, 0.3);
    overflow: hidden;
}
.otl-scorefill { display: block; height: 100%; background: var(--accent); }
.otl-scorerow.grid .otl-scorefill { background: #6fd3e8; }
.otl-scorenum { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }

/* ---- the table: the grid, the score column beside it, the hand below ---- */
/* A grid rather than a row so the phone layout below can reorder the score
   column's three blocks around the hand without a second copy of any of them.
   The outer 1fr tracks centre the grid and the score column, and let the hand
   run the full width underneath. */
.otl-table {
    display: grid;
    grid-template-columns: 1fr auto auto 1fr;
    grid-template-areas: ". board side ." "hand hand hand hand";
    align-items: center;
    column-gap: 26px;
    row-gap: 12px;
    margin: 0 auto 8px;
}
.otl-table > .otl-board { grid-area: board; }
.otl-table > .otl-side { grid-area: side; }
.otl-table > .otl-handpanel { grid-area: hand; margin: 0; }
/* Score, trump, trick, and this round's tricks, stacked beside the grid. */
.otl-side {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    width: 236px;
    flex: none;
}
.otl-info {
    --otl-slot-cw: 60px;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: 22px;
}
.otl-slot { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.otl-slot-title {
    font-size: 12px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    opacity: 0.75;
    white-space: nowrap;
}
.otl-slot-cards {
    display: flex;
    gap: 8px;
    justify-content: center;
    min-height: calc(var(--otl-slot-cw) * 89 / 64);
}
.otl-slot-cards .card { --cw: var(--otl-slot-cw); }
/* The card that took the trick. */
.otl-slot-cards .card.otl-winner { outline: 3px solid var(--accent); outline-offset: 2px; }
.otl-slot-caption {
    /* A fixed line, filled or empty, so the grid under it on a phone never
       shifts by the pixel a bold line box adds. */
    height: 18px;
    line-height: 18px;
    font-size: 13px;
    font-weight: 700;
    white-space: nowrap;
}
.otl-slot-caption.won { color: var(--accent); }
.otl-slot-caption.lost { color: var(--red); }

/* ---- the grid, ringed by one arrow per line end ---- */
.otl-board {
    --otl-cw: 58px;
    --otl-arrow: 28px;
    display: grid;
    grid-template-columns: var(--otl-arrow) repeat(6, var(--otl-cw)) var(--otl-arrow);
    grid-template-rows: var(--otl-arrow) repeat(6, calc(var(--otl-cw) * 89 / 64)) var(--otl-arrow);
    gap: 4px;
    width: fit-content;
    flex: none;
}
.otl-cell {
    position: relative;
    display: block;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
}
.otl-cell .card { --cw: var(--otl-cw); }
.otl-cell.act { cursor: pointer; }
/* The line an arrow picked, before a card is chosen for it. */
.otl-cell.inpath .card { outline: 2px dashed var(--accent); outline-offset: 1px; }
/* The cells the last challenge walked, and the ones it turned face up. */
.otl-cell.walked .card { box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.45); }
.otl-cell .card.otl-turned { box-shadow: 0 0 0 2px #6fd3e8; }
/* The reveal: the card just turned, and at the end the one that decided it. */
.otl-cell.otl-current .card { outline: 3px solid #6fd3e8; outline-offset: 1px; }
.otl-cell.otl-decided .card { outline: 3px solid var(--accent); outline-offset: 2px; }
/* A line the cleanup will send back into the deck (no face-down card left). */
.otl-cell.clearing .card { outline: 2px solid var(--accent); outline-offset: 1px; opacity: 0.8; }
/* A face-down card put there this round: a card seen last round can only be
   under one of these, or still in the deck. */
.otl-cell .card.back { position: relative; }
.otl-fresh {
    position: absolute;
    top: 5px;
    right: 5px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6);
    pointer-events: none;
}
.otl-corner { }

.otl-arrow {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.2);
    color: inherit;
    font: inherit;
    line-height: 1;
    cursor: pointer;
}
.otl-arrow-glyph { font-size: 15px; }
.otl-arrow:disabled { cursor: default; opacity: 0.55; }
/* A line missing a card cannot be challenged again this round. */
.otl-arrow.closed { opacity: 0.18; }
.otl-arrow.act { border-color: var(--accent); color: var(--accent); background: rgba(255, 209, 102, 0.12); }
.otl-arrow.sel { border-color: var(--accent); background: var(--accent); color: var(--accent-ink); }
.otl-arrow.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}

/* ---- this round's tricks: six fixed slots ---- */
.otl-round { width: fit-content; max-width: 100%; }
.otl-round-title {
    font-size: 12px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    opacity: 0.75;
    text-align: center;
    margin-bottom: 6px;
}
.otl-round-row {
    --otl-pair-cw: 30px;
    display: grid;
    grid-template-columns: repeat(3, auto);
    gap: 12px 12px;
    justify-content: center;
}
.otl-pair {
    position: relative;
    display: flex;
    gap: 2px;
    width: calc(var(--otl-pair-cw) * 2 + 2px);
    height: calc(var(--otl-pair-cw) * 89 / 64);
    border-radius: 5px;
}
.otl-pair .card.mini { --cw: var(--otl-pair-cw); }
.otl-pair.empty { border: 1px dashed rgba(255, 255, 255, 0.2); box-sizing: border-box; }
.otl-pair.won { outline: 2px solid var(--accent); outline-offset: 2px; }
.otl-pair.lost { outline: 2px solid rgba(255, 255, 255, 0.25); outline-offset: 2px; }
.otl-pair-mark {
    position: absolute;
    top: -8px;
    right: -8px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    font-size: 11px;
    font-weight: 800;
    background: var(--accent);
    color: var(--accent-ink);
}
.otl-pair.lost .otl-pair-mark { background: #b8483c; color: #fff; }

/* ---- the hidden-card tracker ---- */
.otl-hidden { width: fit-content; max-width: 100%; margin: 10px auto; }
.otl-hidden-note {
    font-size: 13px;
    opacity: 0.8;
    text-align: center;
    max-width: 34em;
    margin: 0 auto 8px;
}
.otl-hidden-grid {
    --otl-hid-cw: 26px;
    display: grid;
    grid-template-columns: 18px repeat(13, var(--otl-hid-cw));
    gap: 3px;
    align-items: center;
    width: fit-content;
    margin: 0 auto;
}
/* The placeholders are full-size .card elements, so they are sized here too. */
.otl-hidden-grid .card,
.otl-hidden-grid .card.mini { --cw: var(--otl-hid-cw); }
.otl-hidden-rank { font-size: 11px; opacity: 0.7; text-align: center; }
.otl-hidden-suit { font-size: 15px; text-align: center; }
/* The platform's suit inks are scoped to the log and to .cardlabel, so a bare
   glyph here needs its own. */
.otl-hidden-suit.red { color: var(--red); }
.otl-hidden-suit.red.diamond { color: var(--red-diamond); }
.otl-hidden-suit.club { color: var(--blue-club); }
.otl-hidden-gone { opacity: 0.35; }

/* ---- the result panel ---- */
/* Centered as a block; the cells stay aligned so the columns read down. */
.otl-scoretable { margin: 10px auto; border-collapse: collapse; font-size: 15px; }
.otl-scoretable th, .otl-scoretable td { padding: 3px 14px; text-align: right; white-space: nowrap; }
.otl-scoretable th { font-size: 12px; font-weight: 600; opacity: 0.75; }
.otl-scoretable td:first-child, .otl-scoretable th:first-child { text-align: left; }
.otl-scoretable tr.otl-reached td { color: var(--accent); font-weight: 700; }

/* ---- narrower screens: the trump and the trick go above the grid ---- */
@media (max-width: 720px) {
    /* The score column dissolves into the grid: totals and the trump and trick
       above the board, this round's tricks under the hand. */
    .otl-table {
        /* minmax(0, ...) so the track never grows past the screen to fit its
           content: a plain 1fr is at least as wide as the widest block. */
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "score" "info" "board" "hand" "round";
        justify-items: center;
        row-gap: 10px;
    }
    .otl-side { display: contents; }
    .otl-table .otl-score { grid-area: score; width: min(420px, 100%); }
    .otl-table .otl-info { grid-area: info; }
    .otl-table .otl-round { grid-area: round; }
    .otl-table > .otl-handpanel { justify-self: stretch; }
    .otl-info { --otl-slot-cw: 46px; gap: 26px; }
    .otl-round-row { grid-template-columns: repeat(6, auto); }
}
@media (max-width: 560px) {
    .status.otl-status { min-height: 4.2em; }
    .otl-board {
        /* Six cards and two arrow tracks across the phone's width. */
        --otl-cw: clamp(36px, calc((100vw - 92px) / 6), 58px);
        --otl-arrow: 22px;
        gap: 3px;
    }
    .otl-arrow-glyph { font-size: 13px; }
    /* Six slots in one row: 12 cards and five gaps across the screen. */
    .otl-round-row { --otl-pair-cw: clamp(20px, calc((100vw - 90px) / 12), 25px); gap: 7px; }
    /* Seven cards in one row, so the fresh hand of each round does not wrap and
       push this round's tricks down under it. Scoped through the game's own
       hand-panel class: #hand-cards is on every page. 94px is the page and
       panel padding plus six 5px gaps. */
    .otl-handpanel #hand-cards { gap: 5px; }
    .otl-handpanel #hand-cards .card { --cw: clamp(38px, calc((100vw - 94px) / 7), 56px); }
    .otl-hidden-grid { --otl-hid-cw: 19px; gap: 2px; grid-template-columns: 16px repeat(13, var(--otl-hid-cw)); }
}
/* Snapped to a whole pixel where round() exists, for the seam reason the
   platform's :root curve gives. Guarded, because an unsupported round() inside
   a custom property blanks every card that reads it. */
@supports (width: round(down, 1.5px, 1px)) {
    @media (max-width: 560px) {
        .otl-board { --otl-cw: round(down, clamp(36px, calc((100vw - 92px) / 6), 58px), 1px); }
    }
}
/* =========================================================================
 * Caravan (src/caravan). Every class is prefixed cvn-.
 *
 * Three lanes across, three bands down: the other side's caravans, the lane
 * chips, the viewer's caravans. The two sides are a point reflection of each
 * other, as if the other player sat across the table: the oldest card is next
 * to the middle and each new card is laid over the one before, further from
 * it, with face cards stepping out to the viewer's right on the viewer's side
 * and to the left on the other. So a buried card on the viewer's side shows
 * its top-left index, and one on the other side its bottom-right one (the
 * card art prints the index in both corners). Every
 * caravan is reserved at five rows, and a longer one tightens its overlap to
 * the same height, so nothing below the table moves as caravans grow. Every
 * turn control is an overlay on the caravan it acts on (.cvn-drop) or a card
 * that becomes a button, never something inserted beside it.
 * ========================================================================= */

.cvn-board {
    /* One knob for every card on the table: three cells of a card plus three
       face-card offsets, and the gaps, have to fit a phone. */
    --cvn-cw: clamp(42px, calc((100vw - 60px) / 6.4), 62px);
    --cvn-ch: calc(var(--cvn-cw) * 89 / 64);
    /* How much of an overlapped row shows: the index and a little more. */
    --cvn-step0: calc(var(--cvn-cw) * 0.44);
    --cvn-rows: 5;
    --cvn-h: calc(var(--cvn-ch) + (var(--cvn-rows) - 1) * var(--cvn-step0));
    /* Whose face card: the viewer's side and the other side. */
    --cvn-me: #7cc0ff;
    --cvn-them: #ffa26b;
    max-width: 720px;
    margin: 0 auto;
}

/* --- the seat strips -------------------------------------------------- */

.cvn-seat {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: nowrap;
    gap: 8px;
    /* Reserved at the height of the discarded card, so a discard appearing
       beside a seat never moves the table. */
    min-height: 40px;
    font-size: 14px;
    white-space: nowrap;
}
.cvn-seatname { font-weight: 700; padding: 1px 8px; border-radius: 999px; }
#cvn-top .cvn-seatname { color: var(--cvn-them); }
#cvn-bottom .cvn-seatname { color: var(--cvn-me); }
.cvn-seat.cvn-acting .cvn-seatname { background: var(--accent); color: var(--accent-ink); }
.cvn-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 1px 7px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.22);
    font-variant-numeric: tabular-nums;
}
.cvn-chipword { opacity: 0.8; font-size: 13px; }
.cvn-chipnum { font-weight: 700; }
.cvn-chip.cvn-spent .cvn-chipnum { color: #e2685f; }
.cvn-seat .card.cvn-tinyback { --cw: 14px; box-shadow: none; }
.cvn-seat .card.cvn-throwncard { --cw: 26px; }

/* --- the table -------------------------------------------------------- */

.cvn-table { display: flex; flex-direction: column; gap: 4px; }
.cvn-band {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
}
.cvn-cell { display: flex; justify-content: center; min-width: 0; }
.cvn-bandmid .cvn-cell { align-items: center; }

/* The caravan box is its number-card column, centered in the cell: that column
   is the lane's axis, and the lane chip, the Play overlay and the hint ring are
   centered on it. Face cards hang out of the box, to the right on the viewer's
   side and to the left on the other. */
.cvn-caravan {
    --cvn-step: min(var(--cvn-step0),
        calc((var(--cvn-h) - var(--cvn-ch)) / max(1, var(--cvn-n) - 1)));
    position: relative;
    height: var(--cvn-h);
    width: var(--cvn-cw);
    border-radius: 8px;
}
.cvn-slot {
    position: absolute;
    left: 0;
    width: var(--cvn-cw);
    height: var(--cvn-ch);
    box-sizing: border-box;
    border: 2px dashed rgba(255, 255, 255, 0.18);
    border-radius: 6%;
}
.cvn-down .cvn-slot { top: 0; }
.cvn-up .cvn-slot { bottom: 0; }

/* A number card and the face cards on it. The row box is transparent to the
   pointer, so it never swallows a tap meant for the row underneath. */
.cvn-row {
    /* Each face card's step out from the one before: a third of a card, closed
       up when the row's face cards (--cvn-nf, def.ts) would run past the cell,
       down to a fifth, which still shows the index. The faces hang OUT of the
       caravan box, so without the container query below the step stays at a
       third and three faces on one card overlap the next lane on a phone
       (they do not clip, and every engine this runs on has the query). */
    --cvn-fstep: calc(var(--cvn-cw) * 0.34);
    position: absolute;
    left: 0;
    width: 100%;
    height: var(--cvn-ch);
    pointer-events: none;
}
@supports (container-type: inline-size) {
    .cvn-cell { container-type: inline-size; }
    .cvn-row {
        --cvn-fstep: clamp(calc(var(--cvn-cw) * 0.2),
            calc((50cqw - var(--cvn-cw) / 2) / var(--cvn-nf, 1)), calc(var(--cvn-cw) * 0.34));
    }
}
/* The viewer's side hangs down from the middle; the other side stands on it,
   and its newest card is the one furthest up. */
.cvn-down .cvn-row { top: calc(var(--cvn-i) * var(--cvn-step)); }
.cvn-up .cvn-row { bottom: calc(var(--cvn-i) * var(--cvn-step)); }
.cvn-row > .card {
    --cw: var(--cvn-cw);
    position: absolute;
    top: 0;
    left: 0;
    pointer-events: auto;
}
.cvn-down .cvn-row > .card.cvn-face { left: calc(var(--cvn-f) * var(--cvn-fstep)); top: 3px; }
.cvn-up .cvn-row > .card.cvn-face { left: calc(-1 * var(--cvn-f) * var(--cvn-fstep)); top: -3px; }
/* While a card is being aimed, a face card never takes the tap meant for the
   number card under it. */
#cvn-table.cvn-choosing .cvn-row > .card.cvn-face { pointer-events: none; }

/* Who put a face card on the other side's caravan: a colored edge. */
.cvn-row > .card.cvn-byme {
    box-shadow: inset 0 0 0 2px var(--cvn-me), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.cvn-row > .card.cvn-bythem {
    box-shadow: inset 0 0 0 2px var(--cvn-them), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}

/* Targets: a card a face card may go on. (A caravan a number card may join is
   marked by its Play overlay, .cvn-drop below.) */
.cvn-row > .card.cvn-target { outline: 3px solid var(--accent); outline-offset: 1px; }
.cvn-row > .card.cvn-target.clickable:hover { transform: none; filter: brightness(1.12); }
#cvn-table.cvn-aiming .cvn-row > .card.cvn-num:not(.cvn-target) {
    filter: brightness(0.62) saturate(0.55);
}
/* The Play and Disband overlays, and a hinted caravan's ring, sit over the
   caravan box, which is the number-card column: the lane's axis. */
.cvn-drop,
.cvn-caravan.hinted::before {
    position: absolute;
    inset: -3px -6px;
    border-radius: 8px;
}
.cvn-caravan.hinted::before {
    content: '';
    z-index: 45;
    outline: 3px solid var(--accent);
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    pointer-events: none;
}

/* A removal held on the table for a moment (def.ts, "Holding a removal"): the
   cards on their way out are ringed red and dimmed, the Jack that removes them
   is not. The ring survives into the ghost that carries them away. */
.cvn-row.cvn-doomed > .card:not(.cvn-remover) {
    outline: 2px solid #e2685f;
    outline-offset: -1px;
    filter: saturate(0.5) brightness(0.8);
}

/* What a face card does: ×2 on a King, and so on. Top right, because that is
   the corner of the last face card in a row that nothing covers (bottom left
   on the other side's caravans, which are the same layout turned round). A
   card in a caravan is already absolutely positioned; a hand card needs a
   positioned box for the badge, and every hand card gets one so none paints
   above its neighbors by being the only positioned one. */
.cvn-handpanel #hand-cards .card { position: relative; }
.cvn-badge {
    position: absolute;
    top: 4%;
    right: 5%;
    min-width: 1.4em;
    padding: 0 3px;
    border-radius: 6px;
    background: rgba(20, 20, 20, 0.82);
    color: var(--accent);
    font-size: max(10px, calc(var(--cw) * 0.19));
    font-weight: 800;
    line-height: 1.35;
    text-align: center;
    white-space: nowrap;
    pointer-events: none;
}
/* The same marks in the Card abilities legend (pages/caravan.mjs), in the
   badge's own colors, so the rules and the table read alike. */
.cvn-legend { list-style: none; padding-left: 0; }
.cvn-legendmark {
    display: inline-block;
    min-width: 1.6em;
    margin-right: 4px;
    padding: 0 3px;
    border-radius: 6px;
    background: rgba(20, 20, 20, 0.82);
    color: var(--accent);
    font-weight: 700;
    text-align: center;
}
.cvn-up .cvn-badge { top: auto; right: auto; bottom: 4%; left: 5%; }

.cvn-drop {
    z-index: 40;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    margin: 0;
    padding: 0 0 6px;
    border: 0;
    outline: 2px dashed var(--accent);
    background: rgba(255, 209, 102, 0.1);
    color: inherit;
    font: inherit;
    cursor: pointer;
}
.cvn-droplabel {
    padding: 1px 9px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-ink);
    font-size: 12px;
    font-weight: 700;
    line-height: 17px;
    white-space: nowrap;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
}
.cvn-drop:hover { background: rgba(255, 209, 102, 0.2); }
.cvn-drop:focus-visible { outline: 3px solid var(--accent); }

/* --- the lane chips --------------------------------------------------- */

.cvn-lanechip {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    min-width: 62px;
    padding: 2px 8px;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.25);
}
.cvn-valline {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 3px;
    /* A fixed height: an arrow, a suit glyph or the lead pill appearing must
       not change the chip's height (it moved the board 3px on a phone). */
    height: 20px;
    line-height: 20px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.cvn-val { font-weight: 700; font-size: 16px; min-width: 2.2ch; text-align: center; border-radius: 6px; }
.cvn-dir { display: inline-block; width: 1ch; opacity: 0.8; }
.cvn-suit { font-size: 13px; }
.cvn-valline.cvn-sold .cvn-val { color: #8fe0a0; }
.cvn-valline.cvn-lead .cvn-val { background: #3a9d57; color: #fff; }
.cvn-valline.cvn-over .cvn-val { color: #ff8a80; }
.cvn-lanerule { height: 1px; margin: 1px 0; background: rgba(255, 255, 255, 0.2); }

/* --- the action row and the status line -------------------------------- */

.cvn-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px;
    /* One button's height, so a button coming or going never moves the hand. */
    min-height: 38px;
    margin: 6px 0 8px;
}
/* The arrow in a recap or a log line ("Bot: 6&hearts; -> left = 9"), centered on
   the line rather than wherever the fallback font's own metrics put it. A bare
   U+2192 centers on the CAP band in the stack's fallback here, about 2px above
   the lowercase band the words around it read on, and a phone picks a different
   fallback again. `vertical-align: middle` puts the glyph's box center on the
   baseline plus half the x-height, which is the middle of the words whatever
   font supplies the arrow. def.ts also asks for the text presentation. */
.cvn-arrow { display: inline-block; vertical-align: middle; line-height: 1; }

.status.cvn-turn { border-left: 3px solid var(--accent); padding-left: 8px; }
.status.cvn-warn { color: var(--bad, #d97a7a); }
#status-line.cvn-status { line-height: 1.35; }

/* --- the hand --------------------------------------------------------- */

/* A card with nowhere to go but the discard. filter, not opacity, so an
   overlapped hand does not show the card beneath through it. */
#hand-cards .card.cvn-dead { filter: brightness(0.62) saturate(0.55); }
#hand-cards .card.cvn-dead.sel { filter: none; }
.cvn-handpanel.cvn-myturn { border-color: var(--accent); }

/* --- the result -------------------------------------------------------- */

/* Laid out like the board (def.ts renderResult): a column a caravan, the other
   side's row on top. Centered as a block in the lifted dialog. */
.cvn-result {
    margin: 0 auto 12px;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
}
.cvn-result th, .cvn-result td { padding: 4px 14px; text-align: center; }
.cvn-result tr:first-child th { border-bottom: 1px solid var(--panel-border); }
.cvn-result th.cvn-resultseat { text-align: left; }
.cvn-result td.cvn-wonlane { font-weight: 700; color: #8fe0a0; }

@media (max-width: 560px) {
    .cvn-band { gap: 4px; }
    .cvn-seat { gap: 6px; font-size: 13px; }
    .cvn-lanechip { min-width: 54px; padding: 1px 6px; }
    .cvn-val { font-size: 15px; }
    /* Two lines at a phone's width, so a recap plus a prompt never pushes the
       table down. */
    #status-line.cvn-status { min-height: calc(2 * 1.35em); }
}

body.motion-off .cvn-caravan.hinted::before { animation: none; }
/* ---------- minigolf (solitaire) ---------- */

/* ---- Pick up, and the note under it ---- */

/* Above the board, not below it: the note under the button changes on every
   hover, so it must never sit where it could push the cards down. Its line
   count is reserved for the same reason. */
.mg-actions { margin: 4px 0 8px; }
.mg-actions-row {
    display: flex;
    gap: 10px;
    justify-content: center;
    align-items: center;
    min-height: 38px;
}
/* Two lines reserved: a hover preview names the hit, then the two side cards
   and their arithmetic, which is two lines on a phone. */
.mg-note {
    font-size: 13px;
    opacity: 0.85;
    text-align: center;
    max-width: 44em;
    margin: 4px auto 0;
    min-height: 2.6em;
}
.mg-note.warn { color: var(--accent); opacity: 1; }
/* The note's card names are suits that matter (the holes are diamonds and
   spades). The platform's own suit colors are scoped to #log-entries, so this
   line opts in for itself. */
.mg-note .red, .mg-status .red { color: var(--red); }
.mg-note .red.diamond, .mg-status .red.diamond { color: var(--red-diamond); }
.mg-note .club, .mg-status .club { color: var(--blue-club); }

/* ---- the course ---- */

.mg-board {
    --mg-cw: 58px;
    display: grid;
    grid-template-columns: repeat(7, var(--mg-cw));
    grid-auto-rows: calc(var(--mg-cw) * 89 / 64);
    gap: 5px;
    /* Fixed tracks and centered, so the course is one constant size all round. */
    width: fit-content;
    margin: 0 auto 12px;
    /* Its own stacking context, so the ball, flag and dots stack among the
       cards and nowhere else. Without it the ball painted over the tutorial's
       coach panel where that docks over the board on a phone. */
    position: relative;
    isolation: isolate;
}
.mg-cell {
    position: relative;
    padding: 0;
    border: 0;
    background: none;
    cursor: default;
    color: inherit;
}
.mg-cell .card { --cw: var(--mg-cw); width: 100%; height: 100%; }
.mg-cell.act { cursor: pointer; }

/* The hole: a red ring on the card, and the flag as a dark badge on its free
   top-right corner (the rank indexes sit top-left and bottom-right). The badge
   is near-black with a white rim because the card under it is white, and the
   hole is always a diamond or a spade, so a red flag drawn straight on the face
   sat among red pips and was hard to find. It overhangs the corner a little so
   it breaks the card's outline, which is what the eye catches first. A shadow
   ring rather than an outline, so a lit hole keeps the gold `.lit` outline. */
.mg-hole .card { box-shadow: 0 0 0 3px #ff3b3b; }
/* Sized and placed to clear the ink of every hole card (A to 9) on the
   simplified atlas: measured off cards-simple.png, a 0.38 badge does once it
   sits 0.05 card widths out past the corner, which stays inside the gap. */
.mg-flag {
    position: absolute;
    top: calc(var(--mg-cw) * -0.05);
    right: calc(var(--mg-cw) * -0.05);
    width: calc(var(--mg-cw) * 0.38);
    height: calc(var(--mg-cw) * 0.38);
    box-sizing: border-box;
    border-radius: 50%;
    background: #14161b;
    border: 2px solid #ffffff;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.6);
    pointer-events: none;
    z-index: 5;
}
/* The pole. */
.mg-flag::before {
    content: '';
    position: absolute;
    left: 34%;
    top: 16%;
    bottom: 14%;
    width: 2px;
    background: #ffffff;
}
/* The pennant, flying right from the top of the pole. */
.mg-flag::after {
    content: '';
    position: absolute;
    left: calc(34% + 2px);
    top: 16%;
    border-left: calc(var(--mg-cw) * 0.18) solid #ff3b3b;
    border-top: calc(var(--mg-cw) * 0.07) solid transparent;
    border-bottom: calc(var(--mg-cw) * 0.07) solid transparent;
}

/* WHERE A PIECE SITS ON A CARD. The board is on the simplified atlas, which
   puts the rank and the suit in the MIDDLE of the face, so nothing may sit
   there: the ball, the ghost balls and the track dots all rest on the card's
   LEFT EDGE, in the blank band between the rank and the suit. Measured off
   cards-simple.png: a ball 0.32 card widths across, centered 0.72 card widths
   down, clears the ink of all 52 faces once it sits 0.03 out past the edge,
   which is under 2px and inside the board's gap at every width. A corner was
   tried first and is worse: a corner is where four cards meet, so a ghost
   there could belong to any of them. All four pieces share these properties,
   so they always land on the same spot of a card. */
.mg-board {
    --mg-ball: calc(var(--mg-cw) * 0.32);
    --mg-ball-left: calc(var(--mg-cw) * -0.03);
    --mg-ball-top: calc(var(--mg-cw) * 0.56);
}

/* The ball. Placed with left/bottom rather than a transform, because the roll
   animates `transform` (see roll() in def.ts). Every card has its ball spot
   at the same offset, so rolling between card centers moves it spot to spot. */
.mg-ball {
    position: absolute;
    left: var(--mg-ball-left);
    top: var(--mg-ball-top);
    width: var(--mg-ball);
    height: var(--mg-ball);
    border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, #ffffff 0%, #f0f0f0 38%, #b9b9b9 100%);
    /* A dark rim, because the ball sits on white card faces. */
    border: 1.5px solid #2b2b2b;
    box-sizing: border-box;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.6), inset 0 -2px 3px rgba(0, 0, 0, 0.15);
    pointer-events: none;
    z-index: 4;
}

/* The last hit's track: a small dot on every card the ball crossed, and on the
   card it started from, until the next hit. Centered on the ball's own spot,
   so the track reads as where the ball has been. */
.mg-dot {
    position: absolute;
    left: calc(var(--mg-ball-left) + var(--mg-ball) / 2);
    top: calc(var(--mg-ball-top) + var(--mg-ball) / 2);
    width: 10px;
    height: 10px;
    margin: -5px 0 0 -5px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.7);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45);
    pointer-events: none;
    z-index: 3;
}

/* The hover preview: a blue ring on the two side cards that set the hit's
   distance, and a GHOST BALL on every card the ball could reach from the
   hovered card on the stroke after. The ghost is the ball's own size and shape,
   hollow and dashed, so it reads as "the ball could be here" rather than as
   another mark. The cards the ball rolls over are not marked. */
.mg-cell.mg-side .card { box-shadow: 0 0 0 3px #6fd3e8; }
.mg-cell.mg-next::after {
    content: '';
    position: absolute;
    left: var(--mg-ball-left);
    top: var(--mg-ball-top);
    width: var(--mg-ball);
    height: var(--mg-ball);
    box-sizing: border-box;
    border-radius: 50%;
    border: 2px dashed #16181d;
    background: rgba(255, 255, 255, 0.55);
    /* A white halo, so the dashed rim still shows on a dark picture card. */
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.85);
    pointer-events: none;
    z-index: 3;
}

/* ---- the scorecard ---- */

.mg-card-panel { width: fit-content; max-width: 100%; margin: 0 auto 12px; }
.mg-card-head {
    display: flex;
    gap: 10px;
    align-items: baseline;
    justify-content: center;
    margin-bottom: 8px;
}
.mg-card-head h3 { margin: 0; }
.mg-card-topar { font-size: 13px; opacity: 0.85; white-space: nowrap; }
.mg-scorecard {
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: center;
    max-width: 100%;
    margin: 0 auto;
}
/* Fixed column widths, and the front nine carries an empty last column, so
   the two nines line up cell for cell. */
.mg-card {
    border-collapse: collapse;
    table-layout: fixed;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
}
.mg-card th, .mg-card td {
    width: 28px;
    height: 24px;
    padding: 2px;
    text-align: center;
    border: 1px solid var(--panel-border);
    white-space: nowrap;
}
.mg-card th:first-child {
    width: 62px;
    text-align: left;
    padding-left: 6px;
    font-weight: 600;
}
.mg-card .sum { width: 38px; font-weight: 700; }
.mg-card-hole td, .mg-card-hole th { opacity: 0.7; font-size: 12px; }
.mg-card-flag .cardlabel { font-size: 12px; }
.mg-card-flag .red { color: var(--red); }
.mg-card-flag .red.diamond { color: var(--red-diamond); }
.mg-card-par td, .mg-card-par th { opacity: 0.8; }
.mg-card td.now { background: rgba(255, 209, 102, 0.16); }
.mg-card td.under { color: #7bd88f; font-weight: 700; }
.mg-card td.over { color: var(--red); }
.mg-card td.live { opacity: 0.6; font-style: italic; }
.mg-card-player.you th:first-child { color: var(--accent); }

/* ---- the result ---- */

/* The four comparison chips on ONE row: the scorecard under them is five rows
   a nine, and with the chips wrapping to two rows the lifted panel ran past its
   own height. A chip's two phrases then break between them (see summarize in
   ai.ts), so each chip reads as two short lines. */
/* Four equal columns, the bots group dissolved into them (display: contents)
   so You and the three bots share the one grid row. */
.mg-compare .replay-scores { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.mg-compare .replay-bots { display: contents; }
.mg-compare .replay-score { min-width: 0; max-width: none; overflow-wrap: normal; }
/* The result's copy of the card, which gains a row per bot. */
.mg-compare + .mg-scorecard { margin-top: 12px; }

@media (max-width: 560px) {
    /* Seven cards across a phone: whatever width is left, between 38px and 50px.
       Never below 35px, so the board keeps the full art it is played on. */
    .mg-board {
        --mg-cw: clamp(38px, calc((100vw - 60px) / 7), 50px);
        gap: 3px;
    }
    .mg-note { min-height: 3.9em; }
    /* The recap plus the hole takes two lines on a phone and one on the first
       hole, so two are reserved or the board steps down 25px after the first
       hit (measured at 390px and 360px). Desktop measured one line throughout. */
    .mg-status { min-height: calc(2 * 1.4em); }
    .mg-card { font-size: 11px; }
    .mg-card th, .mg-card td { width: 21px; height: 21px; padding: 1px; }
    .mg-card th:first-child { width: 52px; padding-left: 3px; }
    .mg-card .sum { width: 28px; }
    .mg-card-flag .cardlabel, .mg-card-hole td, .mg-card-hole th { font-size: 10px; }
}
/* =========================================================================
 * Ocean's Eleven (src/oceanseleven). Every class is prefixed o11-.
 *
 * Two sides, the other player's above and the viewer's below, each a head
 * line (name, wins, deck), a hand-name line, and a row of face-up cards. The
 * row never wraps: its cards overlap more as it grows, down to the index
 * corner, so the table keeps one height from the first card of a hand to the
 * last. Every line is reserved at its tallest, so nothing below the table
 * moves as a hand runs.
 * ========================================================================= */

.o11-board {
    /* One knob for every card on the table. */
    --o11-cw: clamp(44px, calc(20px + 4vw), 70px);
    --o11-ch: calc(var(--o11-cw) * 89 / 64);
    max-width: 760px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

/* --- one side ------------------------------------------------------------ */

.o11-side {
    padding: 6px 8px 8px;
    border: 2px solid transparent;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.12);
}
.o11-side.o11-acting { border-color: var(--accent); }

.o11-head {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    min-height: 24px;
    font-size: 14px;
    white-space: nowrap;
}
.o11-name { font-weight: 700; }
.o11-pips { display: inline-flex; gap: 3px; }
.o11-pip {
    width: 11px;
    height: 11px;
    border-radius: 50%;
    border: 1.5px solid rgba(255, 255, 255, 0.45);
    box-sizing: border-box;
}
.o11-pip.on { background: var(--accent); border-color: var(--accent); }
.o11-deckchip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 1px 7px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.22);
    font-variant-numeric: tabular-nums;
}
.o11-head .card.o11-tinyback { --cw: 14px; box-shadow: none; }
.o11-decknum { font-weight: 700; min-width: 2ch; text-align: right; }
.o11-deckchip.o11-spent .o11-decknum { color: #e2685f; }
.o11-turnchip {
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-ink);
    font-size: 12px;
    font-weight: 700;
}

.o11-handline {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 8px;
    min-height: 22px;
    font-size: 15px;
    white-space: nowrap;
}
.o11-handname { font-weight: 600; }
.o11-tag {
    padding: 0 7px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 700;
    line-height: 18px;
}
.o11-tag.o11-lead { background: #3a9d57; color: #fff; }
.o11-tag.o11-won { background: var(--accent); color: var(--accent-ink); }
.o11-tag.o11-lost { background: rgba(255, 255, 255, 0.16); }

/* --- the row -------------------------------------------------------------

   EVERY CARD HAS A FIXED PLACE, so a card already on the table never moves when
   the next one is turned. (A centered flex row slid every card half a card
   sideways on every deal, which read as the table jittering.) The first five
   places are centered in the side, and the ones a short row has not filled show
   as empty slots, the worthless cards the rules count them as. A sixth card and
   beyond go on to the right; only a row too long for the side closes up its
   spacing, and only a very long one moves its start.

   The highlight classes below are TWO-CLASS rules on the card itself, never
   `.o11-row > .card...`: a flight ghost is a clone appended to the body, so a
   rule that needs the row as parent drops off the ghost and the card flashes
   undimmed, or loses its ring, for the length of the flight. */
.o11-row {
    container-type: inline-size;
    position: relative;
    height: calc(var(--o11-ch) + 4px);
    margin-top: 2px;
    --o11-pitch0: calc(var(--o11-cw) + 6px);
    /* The narrowest spacing that still shows each card's index corner. */
    --o11-pitch-min: calc(var(--o11-cw) * 0.3);
    --o11-start: max(0px, min(calc((100cqw - var(--o11-cw) - 4 * var(--o11-pitch0)) / 2),
        calc(100cqw - var(--o11-cw) - (var(--o11-n) - 1) * var(--o11-pitch-min))));
    /* Spaced to fit --o11-cap cards, a step count def.ts sets (layoutRow), not
       the cards on the table: a long row closes up at a few steps rather than
       at every card. Without it (the app tests' fakes have no geometry) the
       row fits exactly the cards it holds. */
    --o11-pitch: min(var(--o11-pitch0), max(var(--o11-pitch-min),
        calc((100cqw - var(--o11-start) - var(--o11-cw)) / max(1, var(--o11-cap, var(--o11-n)) - 1))));
}
.o11-row > .card,
.o11-row > .o11-xslot {
    --cw: var(--o11-cw);
    position: absolute;
    top: 2px;
    left: calc(var(--o11-start) + var(--o11-i) * var(--o11-pitch));
}
.o11-xslot {
    width: var(--o11-cw);
    height: var(--o11-ch);
    box-sizing: border-box;
    border: 2px dashed rgba(255, 255, 255, 0.16);
    border-radius: 6%;
}
/* The five cards that make the hand: ringed. A ring and not a lift, because a
   lift MOVES the card, and the animator flew every card that joined or left the
   best five up or down a few pixels. */
.card.o11-best {
    box-shadow: 0 0 0 2px var(--accent), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
/* Spare cards that broke a tie between two level best fives. */
.card.o11-second {
    outline: 2px dashed rgba(255, 255, 255, 0.75);
    outline-offset: -2px;
}
/* The rest: filter, not opacity, so an overlapped card does not show the one
   beneath through it. */
.card.o11-spare { filter: brightness(0.55) saturate(0.6); }

/* What a wild stands for, under its index where an overlap never covers it.
   A fixed size, not one derived from --cw: the ghost that carries a card in
   flight is not inside the row, so it would inherit the page's larger --cw and
   the badge would swell in mid-air. */
.o11-wildbadge {
    position: absolute;
    left: 3%;
    top: 40%;
    padding: 0 3px;
    border-radius: 5px;
    background: rgba(20, 20, 20, 0.85);
    color: var(--accent);
    font-size: 12px;
    font-weight: 800;
    line-height: 1.3;
    white-space: nowrap;
    pointer-events: none;
}

/* --- the score between the sides ------------------------------------------- */

.o11-mid {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 12px;
    min-height: 24px;
    font-size: 14px;
    white-space: nowrap;
}
.o11-handno { opacity: 0.8; }
.o11-wildrule { font-size: 13px; opacity: 0.75; }
.o11-wildrule.o11-wild-twos, .o11-wildrule.o11-wild-jokers { color: var(--accent); opacity: 0.9; }

/* --- the buttons ----------------------------------------------------------- */

.o11-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    /* One button's height, so buttons coming and going never move the decks. */
    min-height: 40px;
    margin: 6px 0 10px;
}
.o11-actions .btn.hinted {
    outline: 3px solid var(--accent);
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}
.status.o11-turn { border-left: 3px solid var(--accent); padding-left: 8px; }
/* Two lines at every width: a recap plus the outs is one sentence too many for
   one line, and a line appearing would push the whole table down. */
#status-line.o11-status { line-height: 1.35; min-height: calc(2 * 1.35em); }

/* --- the decks ------------------------------------------------------------- */

.o11-decks {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px 22px;
    margin-bottom: 8px;
}
.o11-deck { min-width: 0; }
.o11-decks .deck-track-grid.o11-grid13 { grid-template-columns: 1.4em repeat(13, var(--dtcw)); }
.o11-decks .card.o11-trackwild { box-shadow: 0 0 0 1.5px rgba(255, 209, 102, 0.55); }
.o11-wildsleft { color: var(--accent); }
/* The title on one line and the legend on the next, always, so a key coming or
   going never re-wraps the head and moves the grid under it. */
.o11-deck .deck-track-head { flex-direction: column; align-items: center; gap: 0; }
.o11-deck .deck-track-legend { white-space: nowrap; min-height: 1.4em; }
/* The outs of the player behind: lit, and every other card still to come
   stepped back so they stand out. */
.o11-outskey { color: var(--accent); font-weight: 700; }
.o11-tiekey { opacity: 0.8; }
.o11-decks .deck-track-grid .card.o11-out {
    box-shadow: 0 0 0 2px var(--accent);
    filter: none;
}
.o11-decks .deck-track-grid .card.o11-tieout {
    outline: 1.5px dashed rgba(255, 255, 255, 0.85);
    outline-offset: -1px;
}
.o11-decks .o11-hasouts .deck-track-grid .card:not(.o11-out):not(.o11-tieout):not(.deck-track-out) {
    filter: brightness(0.5) saturate(0.5);
}

/* --- the history of hands, and the summary --------------------------------- */

.o11-history h3.o11-histhead { margin: 0 0 6px; }
/* Centered as a block and capped, so a hand's name sits near its cards; the
   rows inside stay left-aligned so the columns read down the list. */
.o11-hist { display: flex; flex-direction: column; gap: 8px; max-width: 640px; margin: 0 auto; }
.o11-histempty { margin: 0; text-align: center; opacity: 0.7; }
.o11-histhand {
    padding: 4px 6px 6px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.14);
}
.o11-histtitle {
    display: flex;
    align-items: baseline;
    justify-content: flex-start;
    gap: 10px;
    font-size: 13px;
    margin-bottom: 2px;
    text-align: left;
}
.o11-histno { font-weight: 700; }
.o11-histwhy { opacity: 0.75; }
/* Seat, cards, and the hand's name on one line; the columns line up down the
   list, so the whole list reads as a table (left-aligned, also in the lifted
   summary, which otherwise centers its text). */
.o11-histrow {
    display: grid;
    grid-template-columns: 4.5em minmax(0, 1fr) 14em;
    align-items: center;
    column-gap: 8px;
    font-size: 13px;
    text-align: left;
}
.o11-histrow.o11-histwin .o11-histseat { font-weight: 700; color: var(--accent); }
.o11-histseat { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.o11-histcards {
    container-type: inline-size;
    display: flex;
    flex-wrap: nowrap;
    align-items: flex-end;
    min-height: calc(26px * 89 / 64 + 5px);
    padding-top: 3px;
}
.o11-histcards > .card {
    --cw: 26px;
    position: relative;
}
.o11-histcards > .card + .card {
    margin-left: max(-18px, min(2px, calc((100cqw - var(--o11-n) * 26px) / (var(--o11-n) - 1))));
}
.o11-histcards > .card.o11-best { box-shadow: 0 0 0 1.5px var(--accent); }
.o11-histcards > .card.o11-second { outline: 1.5px dashed rgba(255, 255, 255, 0.75); outline-offset: -1px; }
.o11-histcards > .card.o11-spare { filter: brightness(0.55) saturate(0.6); }
/* At 26px the floating badge read as a dot, so here it is a tab along the
   card's foot, left-aligned so an overlapping neighbor never covers it. */
.o11-histcards .o11-wildbadge {
    top: auto;
    bottom: 0;
    left: 0;
    padding: 0 3px;
    border-radius: 0 3px 0 3px;
    font-size: 10px;
    line-height: 12px;
}
.o11-histname { font-size: 12px; opacity: 0.85; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.o11-histwin .o11-histname { opacity: 1; }
/* In the summary: the history is the table. */
#result-panel .o11-hist { margin-bottom: 12px; }

@media (max-width: 560px) {
    .o11-wildbadge { font-size: 11px; }
    .o11-head { gap: 7px; font-size: 13px; }
    .o11-handline { font-size: 14px; }
    /* The name drops under the cards. */
    .o11-histrow { grid-template-columns: 4.2em minmax(0, 1fr); }
    .o11-histname { grid-column: 2; }
}

body.motion-off .o11-actions .btn.hinted { animation: none; }
/* ============ Jack and Jill Reclaim the Hill (jnj-) ============ */

/* ---- the Hill ----
   A fixed 5x3 grid: the twelve spots round the edge, the Jokers, the Well and
   the discard pile in the middle row. Fixed tracks, centered, so the board is
   one constant size for the whole game and nothing under the pointer moves.
   The board card is capped below the hand's --cw: the Hill is three rows of
   cards, and at the desktop --cw it would push the hand off a laptop screen. */
.jnj-board {
    --jnj-cw: min(var(--cw), 76px);
    display: grid;
    grid-template-columns: repeat(5, var(--jnj-cw));
    grid-template-rows: repeat(3, calc(var(--jnj-cw) * 89 / 64));
    gap: 8px;
    width: fit-content;
    margin: 4px auto 12px;
    /* Pawns and ghosts are positioned inside the spots; keep them below the
       tutorial coach panel. */
    isolation: isolate;
}
.jnj-spot {
    position: relative;
    width: var(--jnj-cw);
    height: calc(var(--jnj-cw) * 89 / 64);
}
.jnj-spot > .card { --cw: var(--jnj-cw); }
.jnj-spot.act { cursor: pointer; }
/* A monster the pick can attack now: tapping it attacks. */
.jnj-spot.lit > .card { outline: 3px solid var(--accent); outline-offset: 1px; }
/* A monster of a rank that cannot be attacked yet: dimmed, never hidden, so
   the player can plan the walk to it. */
.card.jnj-later { opacity: 0.5; filter: saturate(0.6); }

/* How far each live monster is from the character who defeats it, both ways
   round: dark for Jack (his walk), red for Jill (her Aim). */
.jnj-dist {
    position: absolute;
    left: 50%;
    bottom: 3px;
    transform: translateX(-50%);
    padding: 0 5px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.92);
    color: #b3261e;
    font-size: 11px;
    font-weight: 800;
    line-height: 16px;
    white-space: nowrap;
    pointer-events: none;
}
.jnj-dist.jack { background: rgba(29, 29, 31, 0.92); color: #f2efe9; }

/* ---- the pawns ----
   Two slots per spot, Jack left and Jill right, so a spot holding both (or a
   pawn and the other one's ghost) still shows each in its own place. Each pawn
   is a figure with its weapon, as on the rulebook's cover: Jack black with a
   spear, Jill red with a bow. The weapons sit on the OUTER side (Jack's spear
   at his left, Jill's bow at her right), so two pawns on one spot never tangle.
   Drawn as SVG backgrounds, so a flight ghost (a clone) carries them for free
   and the DOM fakes need nothing. Each is a 24x30 SVG, URL-encoded only where
   it has to be (# as %23, < and > as %3C and %3E): decode, edit, re-encode. */
.jnj-tokens {
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-58%);
    display: flex;
    justify-content: center;
    gap: 2px;
    pointer-events: none;
}
.jnj-slot { width: 24px; height: 30px; }
.jnj-pawn {
    display: block;
    width: 24px;
    height: 30px;
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
}
.jnj-pawn.jack { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 30'%3E%3Cpath d='M3.5 6V29' stroke='%23f2efe9' stroke-width='3.2' stroke-linecap='round'/%3E%3Cpath d='M3.5 6V29' stroke='%239a7444' stroke-width='1.5' stroke-linecap='round'/%3E%3Cpath d='M3.5 0.6C5.7 2.7 5.9 5.1 3.5 7.8C1.1 5.1 1.3 2.7 3.5 0.6Z' fill='%23d7dde4' stroke='%231d1d1f' stroke-width='0.9' stroke-linejoin='round'/%3E%3Cpath d='M7.5 29V22A7.5 7.5 0 0 1 22.5 22V29Z' fill='%231d1d1f' stroke='%23f2efe9' stroke-width='1.8' stroke-linejoin='round'/%3E%3Ccircle cx='15' cy='8' r='4.8' fill='%231d1d1f' stroke='%23f2efe9' stroke-width='1.8'/%3E%3C/svg%3E"); }
.jnj-pawn.jill { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 30'%3E%3Cpath d='M19 2.5Q25.6 15 19 27.5' fill='none' stroke='%23fff3f0' stroke-width='3.2' stroke-linecap='round'/%3E%3Cpath d='M19 2.5Q25.6 15 19 27.5' fill='none' stroke='%238a5a2b' stroke-width='1.6' stroke-linecap='round'/%3E%3Cpath d='M19 2.5V27.5' stroke='%23fff3f0' stroke-width='0.8'/%3E%3Cpath d='M1.5 29V22A7.5 7.5 0 0 1 16.5 22V29Z' fill='%23d32f2f' stroke='%23fff3f0' stroke-width='1.8' stroke-linejoin='round'/%3E%3Ccircle cx='9' cy='8' r='4.8' fill='%23d32f2f' stroke='%23fff3f0' stroke-width='1.8'/%3E%3C/svg%3E"); }
/* A ghost: where the picked card can take that character. A real button, the
   only board control a Move needs. */
.jnj-ghost {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 30px;
    padding: 0;
    border: 2px solid var(--accent);
    border-radius: 12px;
    background: rgba(255, 209, 102, 0.85);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
    cursor: pointer;
    pointer-events: auto;
}
/* The figure inside a ghost is drawn smaller and flat, so the gold button is
   what reads as "tap here" and the figure says whose move it is. */
.jnj-pawn.ghost {
    width: 18px;
    height: 23px;
    filter: none;
    opacity: 0.9;
}
.jnj-ghost:hover, .jnj-ghost:focus-visible { background: var(--accent); }
.jnj-ghost.hinted { outline: 3px solid #fff; animation: hintpulse 0.9s ease-in-out infinite alternate; }

/* ---- the middle row ---- */
.jnj-mid {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    min-width: 0;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
}
button.jnj-mid { cursor: pointer; border-radius: 8px; }
button.jnj-mid:hover, button.jnj-mid:focus-visible { background: rgba(255, 255, 255, 0.06); }
.jnj-midlabel { font-size: 11px; opacity: 0.8; white-space: nowrap; }
/* The two Jokers in their full art, fanned so each is big enough to read as a
   Joker: the second overlaps the first by 40%. */
.jnj-jokerrow { display: flex; }
.jnj-jokerrow .card { --cw: calc(var(--jnj-cw) * 0.62); }
.jnj-jokerrow .card + .card { margin-left: calc(var(--jnj-cw) * -0.25); }
/* The Well lies sideways in the middle of the Hill, as in the rulebook. The
   holder is the card's rotated footprint, so the turn cannot spill into the
   neighboring cells. */
.jnj-wellcard {
    --jnj-wellw: calc(var(--jnj-cw) * 0.66);
    width: calc(var(--jnj-wellw) * 89 / 64);
    height: var(--jnj-wellw);
    display: flex;
    align-items: center;
    justify-content: center;
}
.jnj-wellcard > .card { --cw: var(--jnj-wellw); rotate: 90deg; flex: none; }
.jnj-discardslot {
    height: calc(var(--jnj-cw) * 0.6 * 89 / 64);
    display: flex;
    align-items: center;
}
.jnj-discardslot .card.mini { --cw: calc(var(--jnj-cw) * 0.6); }
.jnj-empty {
    width: calc(var(--jnj-cw) * 0.6);
    height: calc(var(--jnj-cw) * 0.6 * 89 / 64);
    border: 1px dashed rgba(255, 255, 255, 0.25);
    border-radius: 6%;
}

/* ---- the hand and its one row of buttons ---- */
.jnj-handpanel #hand-cards { justify-content: center; }
/* Its own row with a reserved height, so a button appearing or going does
   not move the hand above it. */
.jnj-actions {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    min-height: 38px;
    margin-top: 10px;
}
.jnj-actions .btn { white-space: nowrap; }
/* The control the hint's move is made with. */
.jnj-actions .btn.hinted {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}
/* The progress line: the Defense ladder, the rank being fought lit and the
   finished ones struck through. Full opacity, because the lit rank is the one
   thing on it a player reads; the rest keeps the line's usual muted ink. */
.progress.jnj-progress {
    opacity: 1;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 2px 6px;
}
.jnj-progress .jnj-muted { opacity: 0.7; white-space: nowrap; }
.jnj-tier {
    padding: 0 7px;
    border: 1px solid transparent;
    border-radius: 9px;
    line-height: 18px;
    opacity: 0.7;
    white-space: nowrap;
}
.jnj-tier.now { opacity: 1; border-color: var(--accent); color: var(--accent); font-weight: 700; }
.jnj-tier.done { opacity: 0.45; text-decoration: line-through; }
/* A won game's medal, on the progress line and in the result banner. */
.jnj-medal {
    padding: 0 8px;
    border-radius: 9px;
    line-height: 18px;
    font-weight: 800;
    color: #1b1b1b;
}
.jnj-medal.gold { background: #ffd166; }
.jnj-medal.silver { background: #d7dde4; }
.jnj-medal.bronze { background: #d99a66; }
.winner-banner.jnj-medal-banner.gold { color: #ffd166; }
.winner-banner.jnj-medal-banner.silver { color: #dfe5ec; }
.winner-banner.jnj-medal-banner.bronze { color: #e0a674; }
/* Two lines reserved on the status line, the longest a turn's prose runs. */
.status.jnj-status { min-height: 2.6em; }

/* ---- the pile viewer ----
   The Regicide / Dracula modal recipe: z-index 500 keeps it under the
   pass-and-play gate (600) and the card-flight layers (900/1000). */
.jnj-pile {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: rgba(0, 0, 0, 0.6);
}
.jnj-pile-card {
    background: var(--felt-dark);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
    padding: 18px 22px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
    display: flex;
    flex-direction: column;
    gap: 8px;
    max-width: min(640px, calc(100vw - 32px));
    max-height: calc(100vh - 48px);
    overflow-y: auto;
}
.jnj-pile-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    font-weight: 700;
}
.jnj-pile-label {
    font-size: 11px;
    opacity: 0.65;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    margin-top: 6px;
}
.jnj-pile-card .cardrow { gap: 4px; }
.jnj-pile-card .card.mini { --cw: 36px; }
.jnj-pile-note { font-size: 13px; opacity: 0.75; }

.jnj-result-line { text-align: center; }

@media (max-width: 560px) {
    .jnj-board {
        --jnj-cw: clamp(50px, calc((100vw - 56px) / 5.3), 64px);
        gap: 5px;
    }
    .status.jnj-status { min-height: 3.9em; }
    /* One line at 390px: the arrows go (left to right already reads as the
       order) and the chips tighten. */
    .progress.jnj-progress { font-size: 13px; gap: 2px 4px; }
    .jnj-progress .jnj-arrow { display: none; }
    .jnj-tier { padding: 0 5px; }
    .jnj-pile-card .card.mini { --cw: 30px; }
}

/* ---- King of the Hill ----
   The Crown on the crowned King, at the top of its card while it stands. A
   gold SVG crown (16x12), URL-encoded the same way as the pawns. */
.jnj-crown {
    position: absolute;
    left: 50%;
    top: -7px;
    transform: translateX(-50%);
    width: 22px;
    height: 17px;
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 12'%3E%3Cpath d='M1.2 10.8L0.6 3.2L4.6 6.2L8 0.8L11.4 6.2L15.4 3.2L14.8 10.8Z' fill='%23ffd166' stroke='%231d1d1f' stroke-width='1.1' stroke-linejoin='round'/%3E%3C/svg%3E");
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
    pointer-events: none;
}
.jnj-tier.crown.now { color: #ffd166; border-color: #ffd166; }

/* ---- two players: the partner's hand ----
   Above your own; face up in trading range, backs otherwise. Smaller cards,
   since it is read rather than played from (a Trade picks one). */
.jnj-partnerpanel { --jnj-pw: min(var(--cw), 56px); }
.jnj-partnerpanel .cardrow { justify-content: center; gap: 6px; }
/* Through its own variable: `--cw: min(var(--cw), ...)` would be a cycle,
   which makes --cw invalid and paints the cards at no size at all. */
.jnj-partnerpanel .card { --cw: var(--jnj-pw); }
.jnj-partnernote { margin: 6px 0 0; text-align: center; font-size: 12px; opacity: 0.75; }
/* ==== Sold! (sold/) - only what is new lives here; panels, buttons, and
        cards are the shared classes. Every rule that could reach a shared id
        goes through .sld-board or .sld-handpanel. ==== */

/* isolation keeps the board's stacking contexts under the phone-docked
   tutorial coach panel. */
.sld-board {
    display: flex;
    flex-direction: column;
    gap: 12px;
    isolation: isolate;
    /* Small read-only cards (the seat strips and the held results). */
    --sld-small: 28px;
    --sld-strip-h: calc(var(--sld-small) * 89 / 64);
}

/* ---------- the seats ---------- */
.sld-seats {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
}
.sld-seat {
    flex: 1 1 190px;
    max-width: 280px;
    min-width: 0;
    padding: 8px 10px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    display: flex;
    flex-direction: column;
    gap: 5px;
    /* Head, totals, and two strips, whether or not this chip shows both:
       the viewed seat's chip leaves out the row its hand is showing. */
    min-height: calc(2 * var(--sld-strip-h) + 58px);
}
/* From the first sale the money is put away, so a chip is one strip shorter
   (one gap fewer too). It changes once, at the phase change, and not again. */
.sld-board.sld-selling .sld-seat { min-height: calc(var(--sld-strip-h) + 53px); }
.sld-seat.sld-me { outline: 2px solid rgba(255, 209, 102, 0.45); }
.sld-seat.sld-turn {
    border-color: var(--accent);
    box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.35);
}
.sld-seat-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px;
    white-space: nowrap;
}
.sld-seat-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; }
.sld-seat-state {
    font-size: 12px;
    padding: 1px 8px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.08);
    font-variant-numeric: tabular-nums;
}
.sld-seat-state.sld-leading { background: rgba(255, 209, 102, 0.28); font-weight: 700; }
.sld-seat-state.sld-owes { background: rgba(255, 120, 120, 0.25); }
.sld-seat-state.sld-outstate { opacity: 0.7; }
.sld-seat-state.sld-choosing { font-style: italic; opacity: 0.8; }
.sld-seat-state.sld-picked { background: rgba(120, 200, 140, 0.25); }
.sld-seat-state.sld-win { background: rgba(255, 209, 102, 0.35); font-weight: 700; }
.sld-seat-totals { display: flex; gap: 12px; font-size: 13px; white-space: nowrap; }
.sld-total-wrap { display: inline-flex; gap: 4px; align-items: baseline; }
.sld-total-label { opacity: 0.7; }
.sld-total { font-weight: 800; font-variant-numeric: tabular-nums; }
/* One-row strips: they never wrap, so a chip never grows as the game runs.
   Deliberately not .cardrow, which wraps on phones. */
.sld-board .sld-strip {
    display: flex;
    flex-wrap: nowrap;
    overflow: hidden;
    gap: 0;
    min-height: var(--sld-strip-h);
    align-items: center;
}
.sld-board .sld-strip .card { --cw: var(--sld-small); flex: none; }
.sld-board .sld-strip .card + .card { margin-left: -12px; }
.sld-empty { font-size: 12px; opacity: 0.55; font-style: italic; white-space: nowrap; }

/* ---------- the table ---------- */
.sld-table { display: flex; flex-direction: column; gap: 8px; }
/* The held results: one line of groups, scrolled rather than wrapped. A
   scrolling strip stays left-aligned (ui-conventions). */
.sld-held {
    display: flex;
    flex-wrap: nowrap;
    gap: 14px;
    overflow-x: auto;
    min-height: calc(var(--sld-strip-h) + 6px);
    align-items: center;
}
.sld-heldrow { display: flex; gap: 8px; align-items: center; flex: none; }
.sld-heldtitle { font-size: 12px; font-weight: 700; opacity: 0.8; white-space: nowrap; }
.sld-helditem { display: inline-flex; gap: 3px; align-items: center; white-space: nowrap; }
.sld-helditem .card { --cw: var(--sld-small); flex: none; }
.sld-heldname { font-size: 12px; opacity: 0.85; }
.sld-heldpaid { font-size: 12px; opacity: 0.75; font-variant-numeric: tabular-nums; }
.sld-heldempty { align-self: center; }
.sld-board { --sld-offercw: var(--cw); }
/* One row, always: not .cardrow, which wraps on phones, and a second row
   would walk the controls and the hand down the screen. */
.sld-board .sld-offer {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    flex-wrap: nowrap;
    gap: 8px;
    min-height: calc(var(--sld-offercw) * 89 / 64);
}
.sld-board .sld-offer .card { --cw: var(--sld-offercw); flex: none; }
.sld-bidline {
    text-align: center;
    font-size: 14px;
    min-height: 1.4em;
    white-space: nowrap;
}

/* ---------- the sold row (sales only) ---------- */
/* One place per seat under the Checks. It lines up with the Check row
   column for column (same width, same gap, both centered), which is what
   lets a ranked Property sit under the Check it takes. Its height is one
   name line, one card, and the part of a laid Check that shows below the
   card, reserved for the whole of the sales so nothing moves. */
.sld-board { --sld-cardh: calc(var(--sld-offercw) * 89 / 64); }
.sld-board .sld-played { display: none; }
.sld-board.sld-selling .sld-played {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    flex-wrap: nowrap;
    gap: 8px;
    min-height: calc(18px + var(--sld-cardh) * 1.45);
}
/* Nothing is bid or paid in the sales: the hand is the control, so the bid
   line and the action row give their space back, once, as the sales begin. */
.sld-board.sld-selling .sld-bidline,
.sld-board.sld-selling .sld-actions { display: none; }
.sld-playslot {
    width: var(--sld-offercw);
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.sld-playname {
    font-size: 12px;
    line-height: 18px;
    height: 18px;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    opacity: 0.85;
}
.sld-board .sld-played .card { --cw: var(--sld-offercw); flex: none; }
/* A Check laid on its Property: over the lower half, the Property's corner
   still showing. The gap it replaces is the same net height. */
.sld-board .sld-played .card.sld-playcheck { margin-top: calc(var(--sld-cardh) * -0.55); }
.sld-playgap { height: calc(var(--sld-cardh) * 0.45); }
.sld-cardslot { width: var(--sld-offercw); height: var(--sld-cardh); flex: none; border-radius: 6px; }
.sld-cardslot.sld-waiting { border: 2px dashed rgba(255, 255, 255, 0.25); box-sizing: border-box; }
.sld-cardslot.sld-gone { visibility: hidden; }
/* Nothing the shell adds at the end of a game shows until the last sale's
   replay has finished (the summary waits for it too). */
body:has(.sld-board.sld-playing) #endgame-line { display: none; }

/* ---------- controls ---------- */
.sld-actions {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    min-height: 44px;
}
.sld-stepper { display: inline-flex; align-items: center; gap: 6px; }
.sld-stepper .btn.sld-step { min-width: 40px; font-weight: 800; }
.sld-amount {
    min-width: 2.2em;
    text-align: center;
    font-size: 20px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
.sld-actionnote {
    text-align: center;
    font-size: 13px;
    opacity: 0.85;
    min-height: 1.4em;
}

/* ---------- the result ---------- */
/* A score table stays left-aligned cell by cell; the table itself centers. */
.sld-resulttable { margin: 0 auto; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.sld-resulttable th, .sld-resulttable td { padding: 4px 10px; text-align: left; white-space: nowrap; }
.sld-resulttable th { font-size: 13px; opacity: 0.75; font-weight: 600; }
.sld-resulttable .sld-resulttotal { font-weight: 800; }
.sld-resulttable tr.sld-resultwin td { color: var(--accent); }

/* ---------- phones ---------- */
@media (max-width: 720px) {
    /* The offer fits one row at every seat count: 54px is the page and panel
       padding, plus a few px of slack, and the gaps are 6px. */
    .sld-board {
        --sld-small: 24px;
        --sld-n: 4;
        --sld-offercw: min(52px, calc((100vw - 58px - (var(--sld-n) - 1) * 6px) / var(--sld-n)));
        gap: 10px;
    }
    .sld-board.sld-n5 { --sld-n: 5; }
    .sld-board.sld-n6 { --sld-n: 6; }
    .sld-board .sld-offer, .sld-board.sld-selling .sld-played { gap: 6px; }
    .sld-playname { font-size: 11px; }
    /* The stepper, Raise, and Pass stay on one row down to a 320px phone, so
       the hand does not step down a row on the player's own turn. */
    .sld-board .sld-actions { flex-wrap: nowrap; gap: 6px; }
    .sld-board .sld-actions .btn { padding: 7px 9px; white-space: nowrap; }
    .sld-board .sld-stepper { gap: 3px; }
    .sld-board .sld-stepper .btn.sld-step { min-width: 32px; }
    .sld-board .sld-amount { font-size: 18px; min-width: 1.6em; }
    /* Two lines, whatever the phase asks: a one-line status would move the
       whole board by a line when the next one wraps. */
    .status.sld-status { line-height: 1.35; min-height: 2.7em; }
    .sld-resulttable th, .sld-resulttable td { padding: 3px 6px; font-size: 13px; }
    .sld-seat { flex: 1 1 42%; max-width: none; padding: 6px 8px; }
    .sld-board .sld-seat-totals { gap: 8px; }
    /* "Money cards" and "Checks" do not fit side by side in a phone chip, and
       whether they would depends on the digits, so during the sales they
       always stack: one height for the whole phase. */
    .sld-board.sld-selling .sld-seat-totals { flex-direction: column; gap: 0; }
    .sld-board.sld-selling .sld-seat { min-height: calc(var(--sld-strip-h) + 53px + 1.3em); }
    .sld-board .sld-strip .card + .card { margin-left: -11px; }
    .sld-bidline { font-size: 13px; }
}
/* =========================================================================
 * Caribbean Star (src/caribbeanstar). Every class is prefixed cbs-.
 *
 * Three bands down the board: the other magician's practice pile, this turn's
 * two cards, the viewer's pile. Every band is reserved at its tallest ordinary
 * shape (a pile row is one card high and scrolls sideways; a table slot has
 * room for a performed show), so nothing above the hand moves as the game
 * runs. The action bar holds buttons only, at one row.
 * ========================================================================= */
.cbs-board {
    --cbs-pilecw: 44px;
    --cbs-tablecw: 52px;
    max-width: 720px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
/* --- a magician's block ---------------------------------------------- */
.cbs-seatblock {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 6px 10px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.cbs-seatblock.cbs-own { border-color: rgba(255, 209, 102, 0.35); }
.cbs-seathead {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 4px 12px;
    min-height: 24px;
    font-size: 14px;
}
.cbs-seatname { font-weight: 700; white-space: nowrap; }
.cbs-glyphs { display: inline-flex; gap: 1px; }
.cbs-suit.red { color: var(--red); }
.cbs-suit.red.diamond { color: var(--red-diamond); }
.cbs-suit.club { color: var(--blue-club); }
/* The show as it stands, in words, and a button that opens its working.
   The minutes and the value keep a fixed width so the chip never changes
   size as the pile grows. */
.cbs-meter {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 1px 6px 1px 8px;
    border-radius: 8px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.22);
    color: var(--ink);
    font: inherit;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
}
.cbs-meter:hover, .cbs-meter.cbs-open { border-color: var(--accent); }
.cbs-minutes { font-size: 12.5px; opacity: 0.85; min-width: 7.6em; text-align: right; }
.cbs-minutes.cbs-perfect { color: #9be58b; opacity: 1; }
.cbs-minutes.cbs-long { color: #ff8a80; opacity: 1; }
.cbs-worthword { font-size: 12.5px; opacity: 0.85; }
.cbs-worth { font-weight: 700; min-width: 2.1em; text-align: left; }
.cbs-worth.cbs-perfect { color: #9be58b; }
.cbs-worth.cbs-neg { color: #ff8a80; }
/* The "?" that says a figure opens its working. */
.cbs-why {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 15px;
    height: 15px;
    border-radius: 50%;
    border: 1px solid currentColor;
    font-size: 10.5px;
    font-weight: 800;
    line-height: 1;
    opacity: 0.75;
}
/* A performed show's caption on the table, or a farewell show's figure. */
.cbs-showbtn {
    font: inherit;
    color: inherit;
    background: none;
    border: 0;
    border-bottom: 1px dotted currentColor;
    padding: 0;
    cursor: pointer;
    line-height: inherit;
}
.cbs-showbtn:hover, .cbs-showbtn.cbs-open { color: var(--accent); }
/* The working, over the area it belongs to (it never pushes anything down). */
.cbs-seatblock, .cbs-table { position: relative; }
.cbs-explain {
    position: absolute;
    left: 0;
    right: 0;
    z-index: 30;
    max-height: 300px;
    overflow: auto;
    padding: 8px 10px;
    border-radius: 10px;
    border: 1px solid var(--accent);
    background: var(--felt-dark);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}
.cbs-explain-down { top: 0; }
.cbs-explain-up { bottom: 0; }
.cbs-explain-head {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin-bottom: 6px;
    font-weight: 700;
    font-size: 14px;
}
.cbs-explain-inline { margin: 4px auto 0; max-width: 520px; }
/* A real table, so the cards, the words and the running value each keep one
   column down the working (ui-conventions, "Score tables line up by column"). */
.cbs-work { width: 100%; border-collapse: collapse; font-size: 13px; }
.cbs-work td { padding: 4px 6px; vertical-align: middle; text-align: left; }
.cbs-work tr + tr td { border-top: 1px solid var(--panel-border); }
.cbs-work-cards { width: 1%; white-space: nowrap; }
.cbs-work-row { display: inline-flex; gap: 2px; }
.cbs-work-row .card { --cw: 24px; flex: none; }
.cbs-work td.cbs-work-value {
    width: 1%;
    text-align: right;
    white-space: nowrap;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
/* A pile is one row of small cards that scrolls rather than wraps. */
.cbs-pile {
    display: flex;
    justify-content: center;
    flex-wrap: nowrap;
    gap: 4px;
    overflow-x: auto;
    min-height: calc(var(--cbs-pilecw) * 89 / 64 + 4px);
    padding-top: 2px;
}
.cbs-pile .card { --cw: var(--cbs-pilecw); flex: none; }
.cbs-known {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    /* One row of 26px cards, whether or not any are known. */
    min-height: 40px;
    font-size: 12.5px;
    opacity: 0.9;
}
.cbs-known-label { white-space: nowrap; opacity: 0.8; margin-right: 4px; }
.cbs-known .card { --cw: 26px; flex: none; }
/* --- the table ------------------------------------------------------- */
.cbs-table { padding: 6px 10px; }
.cbs-tablehead {
    text-align: center;
    font-size: 13px;
    font-weight: 700;
    opacity: 0.8;
    min-height: 18px;
}
.cbs-slots {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: 22px;
    /* FIXED: two cards, a two-line caption each, and a one-row performed show,
       whatever the turn did (ui-conventions, "Containers must not move"). */
    height: calc(var(--cbs-tablecw) * 89 / 64 + 96px);
}
.cbs-tablenote { align-self: center; max-width: 30em; text-align: center; font-size: 13px; opacity: 0.8; }
.cbs-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    width: 150px;
    padding: 4px;
    border-radius: 8px;
}
.cbs-slot.cbs-next { background: rgba(255, 209, 102, 0.12); outline: 2px solid var(--accent); }
.cbs-slot.cbs-stopped .cbs-slotcard { opacity: 0.5; }
.cbs-slotwho { font-size: 12.5px; font-weight: 700; white-space: nowrap; }
.cbs-slotcard .card { --cw: var(--cbs-tablecw); }
/* A caption is at most two lines, and always reserves both. */
.cbs-slotwhat {
    font-size: 12.5px;
    text-align: center;
    line-height: 16px;
    height: 32px;
    overflow: hidden;
    opacity: 0.92;
}
.cbs-shown {
    display: flex;
    justify-content: center;
    flex-wrap: nowrap;
    gap: 2px;
    max-width: 100%;
    height: calc(20px * 89 / 64);
    overflow: hidden;
}
.cbs-shown .card { --cw: 20px; flex: none; }
/* --- card names in prose and on buttons ------------------------------ */
/* A card named in the status line or on a button, colored by suit the way
   the sprites and the log are. Not .cardlabel: its bold and nowrap would
   re-weight the sentence around it. */
.cbs-cardtok { white-space: nowrap; }
.cbs-cardtok.red { color: var(--red); }
.cbs-cardtok.red.diamond { color: var(--red-diamond); }
.cbs-cardtok.club { color: var(--blue-club); }
/* On the yellow primary button the light inks vanish: darker ones, still the
   same suit per color. */
.btn.primary .cbs-cardtok { font-weight: 800; }
.btn.primary .cbs-cardtok.red { color: #b3261e; }
.btn.primary .cbs-cardtok.red.diamond { color: #b4400c; }
.btn.primary .cbs-cardtok.club { color: #1f4e8c; }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .btn.primary .cbs-cardtok.red.diamond { color: var(--fc-diamond-ink); }
.deck-fourcolor .btn.primary .cbs-cardtok.club { color: var(--fc-club-ink); }
/* The status line narrating a turn as it plays back: the log's own lines,
   run together as one sentence or two. */
.cbs-status .cbs-caption { display: inline; }
.cbs-status .cbs-caption + .cbs-caption::before { content: ' '; }
/* --- the action bar --------------------------------------------------- */
/* One row of buttons at a fixed height; a wide buffet scrolls sideways.
   Centered by auto margins on the end buttons, NOT justify-content: center,
   which clips the left end of an overflowing row behind an edge no scroll
   reaches (ui-conventions, "A scrolling strip must NOT center"). */
.cbs-actions {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 8px;
    height: 52px;
    margin: 6px 0 2px;
}
.cbs-actions > :first-child { margin-left: auto; }
.cbs-actions > :last-child { margin-right: auto; }
.cbs-actions .btn { flex: none; white-space: nowrap; }
.cbs-narrow { display: none; }
/* --- the hand --------------------------------------------------------- */
.panel.cbs-handpanel #hand-cards { gap: 16px; }
.cbs-handgroup { display: flex; flex-wrap: nowrap; gap: 6px; }
.card.cbs-faded { opacity: 0.4; }
/* --- the role badge ---------------------------------------------------- */
.card.cbs-badged { position: relative; }
.cbs-badge {
    position: absolute;
    top: 4%;
    right: 5%;
    display: inline-flex;
    align-items: center;
    gap: 1px;
    padding: 1px 3px;
    border-radius: 6px;
    background: rgba(20, 20, 20, 0.82);
    font-size: max(10px, calc(var(--cw) * 0.17));
    font-weight: 800;
    line-height: 1.1;
    pointer-events: none;
}
.cbs-badge .cbs-icon { width: max(11px, calc(var(--cw) * 0.2)); height: max(11px, calc(var(--cw) * 0.2)); }
/* On a small simplified card the rank fills the top, so the badge sits in the
   empty bottom-right corner and carries the icon alone (the points are in the
   card's tooltip and the pile's meter). */
.cbs-badge.cbs-badge-small {
    top: auto;
    bottom: 3%;
    right: 3%;
    padding: 1px;
    border-radius: 4px;
}
.cbs-badge.cbs-badge-small .cbs-badge-num { display: none; }
.cbs-badge.cbs-badge-small .cbs-icon { width: max(9px, calc(var(--cw) * 0.27)); height: max(9px, calc(var(--cw) * 0.27)); }
.cbs-role-trick { color: #ffd166; }
.cbs-role-showtime { color: #ffe9a8; }
.cbs-role-buffet { color: #8fd3ff; }
.cbs-role-rehearsal { color: #9be58b; }
.cbs-role-nap { color: #c7b8ff; }
.cbs-role-mistake { color: #ff9a8f; }
.cbs-role-stop { color: #ff6b6b; }
/* --- the card key ------------------------------------------------------ */
.cbs-key {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 4px 14px;
    margin: 6px auto 0;
    max-width: 720px;
    font-size: 12.5px;
    opacity: 0.85;
}
.cbs-keyitem { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
.cbs-key .cbs-icon, .cbs-reflist .cbs-icon { width: 15px; height: 15px; vertical-align: -2px; color: var(--accent); }
/* --- the cards you haven't seen (renderDeckView) ----------------------- */
/* The Deck chip in the progress line is the button that opens them. It keeps
   the round label's text and size, plus a border that says it can be pressed;
   disabled (nothing unseen, or the game over) the border goes but its width
   stays, so the line never moves. */
.progress-round.cbs-deck {
    font: inherit;
    font-size: 13px;
    color: inherit;
    background: none;
    margin: 0;
    padding: 1px 9px;
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    opacity: 0.85;
    cursor: pointer;
}
.progress-round.cbs-deck:hover:not(:disabled),
.progress-round.cbs-deck.cbs-open { opacity: 1; border-color: var(--accent); }
.progress-round.cbs-deck:disabled { opacity: 0.6; border-color: transparent; cursor: default; }
/* A dialog over the page: a fixed full-page backdrop that takes every pointer
   event, and a box in the middle. z-index 500, under the handoff gate's 600
   and over the tutorial coach bar (Techu's deck dialog does the same). */
.cbs-deckview {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    padding: 12px;
    background: rgba(0, 0, 0, 0.55);
}
.cbs-deckview[hidden] { display: none; }
.cbs-dvbox {
    position: relative;
    box-sizing: border-box;
    max-width: 100%;
    max-height: 100%;
    overflow: auto;
    padding: 12px 14px 14px;
    background: var(--felt-dark);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
    box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
}
/* The box takes focus only from a press inside it, so it draws no ring. */
.cbs-dvbox:focus { outline: none; }
/* The title centered, Close pinned right and out of the flow
   (ui-conventions, "Center, do not left-align"). */
.cbs-dvhead {
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 31px;
    padding: 0 64px;
}
.cbs-dvtitle { margin: 0; font-size: 15px; text-align: center; }
.cbs-dvclose { position: absolute; right: 0; }
.cbs-dvwhere { margin: 4px 0 10px; font-size: 13.5px; text-align: center; opacity: 0.9; }
/* The shared deck tracker's grid (platform.css), 13 ranks wide and larger:
   the dialog has the room. */
.cbs-deckview .deck-track-grid {
    --dtcw: 30px;
    grid-template-columns: 1.4em repeat(13, var(--dtcw));
    gap: 3px;
}
.deck-track-grid .cbs-dvrank {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    font-size: 12px;
    font-weight: 700;
    opacity: 0.85;
}
.cbs-dvicon { display: flex; }
.cbs-dvicon .cbs-icon { width: 12px; height: 12px; }
/* --- the result -------------------------------------------------------- */
/* A real <table> (not the grid .result-table), so every column lines up:
   names left, every number column right, the header aligned like its column. */
.cbs-result {
    margin: 4px auto 0;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
}
.cbs-result th, .cbs-result td { padding: 3px 12px; text-align: right; }
.cbs-result th:first-child, .cbs-result td:first-child { text-align: left; }
.cbs-result th { font-size: 13px; opacity: 0.8; border-bottom: 1px solid var(--panel-border); }
.cbs-result .cbs-rtotal { font-weight: 800; }
.cbs-farewell { margin-top: 8px; text-align: center; font-size: 14px; }
@media (max-width: 560px) {
    .cbs-board { --cbs-pilecw: 36px; --cbs-tablecw: 46px; }
    .cbs-slot { width: 150px; }
    .cbs-slots { gap: 8px; }
    .cbs-seatblock { padding: 5px 6px; }
    /* The pile's name and its meter always on two lines, so the head never
       changes shape with the minutes or the value. */
    .cbs-seathead { flex-direction: column; gap: 2px; min-height: 46px; }
    /* Turn prose is reserved at two lines on a phone. */
    .status.cbs-status { min-height: 50px; }
    .cbs-actions { gap: 6px; }
    .cbs-actions .btn { padding-left: 9px; padding-right: 9px; }
    /* The short labels, so three buttons fit in 390px. */
    .cbs-wide { display: none; }
    .cbs-narrow { display: inline; }
    /* 13 columns of 22px fit a 360px screen with the dialog's padding. The
       dialog sits in the play area, so the phone fit's zoom draws them
       smaller still (about 18px at 390px). */
    .cbs-deckview .deck-track-grid {
        --dtcw: 22px;
        grid-template-columns: 1.1em repeat(13, var(--dtcw));
        gap: 1px;
    }
    .cbs-deckview { padding: 8px; }
    .cbs-dvbox { padding: 10px; }
    .cbs-dvhead { padding: 0 60px; }
    .cbs-dvicon .cbs-icon { width: 10px; height: 10px; }
}
/* =========================================================================
   Pineapple Open-Face Chinese Poker (pineapple)
   Every seat builds a 13-card Board in three rows (3 top, 5 middle, 5 bottom),
   so the table is a stack of pyramids: the other seats' Boards across the top
   on small simplified cards, this seat's Board at the bottom on the site card
   size, then the hand panel with the dealt cards and the Discards strip.
     the board area       .pin-table (#pin-table), .pin-opps, .pin-own
     one Board            .pin-board, .pin-boardhead, .pin-foulmark
     one Row              .pin-row (+ -top / -middle / -bottom), .pin-cap
     marks                .legal (a Row that takes the picked card), .pin-last
     below the Board      .pin-discards (#pin-discards), .pin-actions
                          (#pin-confirm), .pin-status, .pin-progress
     summaries            .pin-summary in the result panel
   Binding rules: .kiro/steering/ui-conventions.md (centering, containers must
   not move, shared ids need a game class, small-card art).
   RESERVED HEIGHTS. Every Board paints all 13 slots from the first repaint, so
   no Row grows as cards arrive. On top of that:
     a Row          --cw x 1.39 of cards, plus 8px of frame on this seat's Board
                    (the .legal ring) and none on an opponent's
     a caption      one nowrap line (1.4em of 12px; 11px on opponents, 10px on
                    a phone), and it never widens its Board
     a board head   22px (20 on a phone), the foul mark always in its grid
                    column (visibility)
     Discards       one card of --pin-oppcw high, four cards wide
     the controls   2.6em, one row of buttons, before any prose
     status line    1 line at desktop, 2 on a phone; progress line 1 line
   MEASURED in task 7.3 (Playwright, vs AI, a full 1-round Match at 2 and at
   3 seats, offsetTop and offsetHeight on every repaint). Every figure below
   held min = max for the whole Match, summaries included, but for the one
   exception after the table:
                      1280px          390px           360px (3 seats)
     status line      26              50 (2 lines)    50
     progress line    20              20              20
     #pin-table       729             521             492
     opponent Board   284 x 268 wide  207 x 172 wide  182 x 142 wide
       head/row/cap   22 / 64 / 15    20 / 42 / 14    20 / 33 / 14
     own Board        433             306             302
       head/row/cap   22 / 111 / 17   20 / 71 / 17    20 / 69 / 17
     .pin-actions     42              42              42
     hand panel       164 (tray 103)  124 (tray 63)   122 (tray 61)
     Discards strip   64              42              33
   Two opponent Boards take 560px at 1280, 350px of a 370px column at 390,
   and 290px of 340px at 360; the page never scrolls sideways. The one
   height that changes is the hand panel's, before this seat's first cards
   of a Match when a bot acts first (handspace.ts reserves nothing for an
   empty tray): 61px, then the height above. Only the Discards strip, below
   it, moves. Centering, midpoint of the in-flow content against its box:
   every Row, board title (name and total), caption, Board, the opponent
   pair, the controls, the tray, the Discards, and the summary titles and
   tables measured 0.0px off at 1280 and 390. At 360 a 3-seat opponent's
   title sits 5 to 8px left, where its head is too narrow to balance the
   foul mark.
   Card widths. This seat's Board uses the site --cw (72px to 96px on desktop,
   about 45px at 390px). Opponents and Discards read --pin-oppcw: 46px, then
   30px under 720px, where two opponent Boards fit a 390px phone:
   2 x (5 x 30 + 4 x 4 gaps + 6 frame) + 6 between = 350px in a 370px column.
   All of them are on the simplified atlas (a Board is a dense grid); only the
   dealt cards in the hand are full art. That is a class on the element, set in
   def.ts, not a rule here. Task 7.3 read the painted atlas: cards-simple.png
   on every opponent, own Board, and Discard card, full art on the tray
   (cards-mini.png at 390). It also compared the own Board on both atlases at
   1280 and 390: on the simplified one a pair or a run of one suit reads at a
   glance, where full art keeps rank and suit in a corner index.
   No rule names a shared id. #hand-cards gets no rule at all: handspace.ts
   reserves it and the shared rule centers it. The log and every score table
   stay left-aligned; everything on the board axis is centered.
   ========================================================================= */
/* --- sizes -------------------------------------------------------------- */
/* On every Pineapple root that holds small cards, so the opponents and the
   Discards strip (a sibling below the hand panel, outside .pin-table) both
   read it. */
.pin-table,
.pin-discards {
    --pin-oppcw: 46px;
    --pin-oppgap: 6px;
}
/* Beats .card.mini's own 46px / 34px (3 classes against 2). */
.pin-opps .card.mini,
.pin-discards .card.mini { --cw: var(--pin-oppcw); }
/* --- the status and progress lines --------------------------------------- */
/* One line each at desktop. The status line is the only place that says what
   to do ("Place 2 cards. The one you leave is discarded."). */
.status.pin-status { min-height: 1.45em; }
.progress.pin-progress { min-height: 1.45em; }
/* --- the board area ------------------------------------------------------ */
/* Opponents on top, this seat's Board under them, both centered. */
.pin-table {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    min-width: 0;
    margin-bottom: 10px;
}
/* One Board centered at 2 seats; two side by side at 3 seats, and still side
   by side on a phone (next seat on the left, the one after on the right). */
.pin-opps {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: flex-start;
    gap: 24px;
    max-width: 100%;
}
.pin-own {
    display: flex;
    justify-content: center;
    max-width: 100%;
}
/* --- a Board ------------------------------------------------------------- */
/* A pyramid: the 3-card top Row centered over the two 5-card Rows. Its width
   is set by the Rows alone (the head and captions below cannot widen it), so
   no Board changes width as captions and the foul mark come and go. */
.pin-board {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 6px 8px;
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.14);
}
.pin-own .pin-board { background: rgba(255, 255, 255, 0.04); }
.pin-opps .pin-board { padding: 4px 6px; }
/* Seat name and Match_Total centered, the foul mark to their right. A grid
   rather than a flex row: the mark is hidden by visibility for most of a Hand,
   so as a flex item it kept its width in the unit and pushed the name and total
   ~30px left of center on every Board (measured in task 7.3). The two 1fr
   columns either side balance it instead, and where a head is too narrow to
   balance it (a 3-seat opponent on a phone) the left one gives way first. The
   width: 0 / min-width: 100% pair gives the head the Board's width without
   letting it contribute any, so a long name clips instead of widening it. */
.pin-boardhead {
    box-sizing: border-box;
    display: grid;
    grid-template-columns: 1fr auto auto 1fr;
    align-items: center;
    column-gap: 6px;
    width: 0;
    min-width: 100%;
    min-height: 22px;
    overflow: hidden;
    margin-bottom: 2px;
    font-size: 13px;
    font-weight: 700;
    line-height: 1.4;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.pin-boardhead > .pin-name { grid-row: 1; grid-column: 2; }
.pin-boardhead > .pin-total { grid-row: 1; grid-column: 3; }
.pin-boardhead > .pin-foulmark { grid-row: 1; grid-column: 4; justify-self: start; }
/* "Will foul". Always in the flow and toggled by visibility, so the head is
   the same height with or without it. If def.ts ever reaches for `hidden`,
   the second rule keeps the space reserved anyway. */
.pin-foulmark {
    display: inline-block;
    padding: 1px 7px;
    border: 1px solid rgba(255, 138, 128, 0.9);
    border-radius: 999px;
    color: #ff8a80;
    font-size: 11px;
    font-weight: 700;
    white-space: nowrap;
}
.pin-foulmark[hidden] { display: inline-block; visibility: hidden; }
/* --- a Row --------------------------------------------------------------- */
/* Centered, one line, the height of a card whatever it holds. The frame is
   transparent until .legal colors it, so the highlight never shifts layout.
   A Row may be a <button>, so the resets keep a button Row and a div Row
   pixel-identical. .pin-row-top / -middle / -bottom and the per-target hooks
   (pin-row-<row>-<seat>) carry no style of their own. */
.pin-row {
    appearance: none;
    -webkit-appearance: none;
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 6px;
    min-height: calc(var(--cw) * 1.39);
    margin: 0;
    padding: 3px;
    border: 1px solid transparent;
    border-radius: 10px;
    background: transparent;
    font: inherit;
    color: inherit;
    cursor: default;
}
/* An opponent's Row is never a target, so it has no frame to reserve. */
.pin-opps .pin-row {
    gap: var(--pin-oppgap);
    min-height: calc(var(--pin-oppcw) * 1.39);
    padding: 0;
    border-width: 0;
}
/* A Row with a free slot while this seat is placing: visible, but calm, since
   it is lit for the whole turn rather than only while a card is picked. */
.pin-row.legal {
    border-color: rgba(255, 209, 102, 0.4);
    background: rgba(255, 209, 102, 0.05);
    cursor: pointer;
}
.pin-row.legal .card.empty { border-color: rgba(255, 209, 102, 0.45); }
.pin-row.legal:hover { border-color: rgba(255, 209, 102, 0.75); background: rgba(255, 209, 102, 0.1); }
.pin-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The Row_Hand and its royalty under each Row. One reserved line, empty or
   not, and like the head it takes the Board's width without adding to it. */
.pin-cap {
    box-sizing: border-box;
    width: 0;
    min-width: 100%;
    min-height: 1.4em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    text-align: center;
    font-size: 12px;
    line-height: 1.4;
    opacity: 0.85;
}
.pin-opps .pin-cap { font-size: 11px; }
/* Each seat's latest turn. Blue rather than gold: gold already means picked
   (.card.sel) and legal. */
.pin-board .card.pin-last {
    outline: 2px solid #6aa9e0;
    outline-offset: 1px;
}
/* --- the Discards strip -------------------------------------------------- */
/* This seat's Discards, face up. Four cards reserved, the most a Hand makes. */
.pin-discards {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: var(--pin-oppgap);
    min-width: calc(4 * var(--pin-oppcw) + 3 * var(--pin-oppgap));
    min-height: calc(var(--pin-oppcw) * 1.39);
    max-width: 100%;
    margin-inline: auto;
}
/* --- the controls -------------------------------------------------------- */
/* Controls only, one button high whether or not a button is in it, so the
   hand below never moves between turns. Prose goes in its own container. */
.pin-actions {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 8px;
    min-height: 2.6em;
    margin: 8px 0;
}
#pin-confirm { min-width: 7em; }
/* --- the summaries ------------------------------------------------------- */
/* One block per Pairing, then the totals. Titles centered; each table is
   centered as a block and its cells stay left-aligned so the columns read
   down. It scrolls rather than widening the panel, and an auto margin (unlike
   justify-content) never clips the start of a table wider than the panel. */
.pin-summary {
    max-width: 100%;
    margin: 8px auto 0;
    overflow-x: auto;
}
.pin-summary h3,
.pin-summary h4 {
    margin: 8px 0 4px;
    font-size: 14px;
    text-align: center;
}
.pin-summary table {
    margin: 0 auto 10px;
    border-collapse: collapse;
    font-size: 13px;
}
.pin-summary th,
.pin-summary td {
    padding: 3px 10px 3px 0;
    border-bottom: 1px solid var(--panel-border);
    text-align: left;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.pin-summary .cardrow { justify-content: center; }
/* --- the royalty tables in the rules ------------------------------------- */
/* Inside the Cheat_Sheet's Scoring section (pages/pineapple.mjs), on the setup
   screen, the board, and the rules page. A reference table, so its cells stay
   left-aligned and the numbers right-aligned to read down the columns. */
.pin-royaltable {
    border-collapse: collapse;
    margin: 6px 0 10px;
}
.pin-royaltable th,
.pin-royaltable td {
    padding: 3px 12px 3px 0;
    border-bottom: 1px solid var(--panel-border);
    text-align: left;
    font-size: 13px;
}
.pin-royaltable .pin-royal-value {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
/* --- phones -------------------------------------------------------------- */
/* Every reservation re-derived here. The site --cw drops to its phone curve at
   560px (about 45px at 390px), and the rules above follow it through
   var(--cw); what this block changes is the small-card width, the gaps, the
   frames, and the status line's line count. */
@media (max-width: 719px) {
    .pin-table,
    .pin-discards {
        --pin-oppcw: 30px;
        --pin-oppgap: 4px;
    }
    .pin-table { gap: 8px; }
    .pin-opps { gap: 6px; }
    .pin-board { padding: 4px 5px; }
    .pin-opps .pin-board { padding: 2px; }
    .pin-row { gap: 4px; }
    .pin-opps .pin-cap { font-size: 10px; }
    .pin-boardhead { font-size: 12px; min-height: 20px; }
    .pin-opps .pin-boardhead { font-size: 11px; column-gap: 4px; }
    .pin-foulmark { font-size: 10px; padding: 0 5px; }
    /* A 3-seat opponent head is 166px here: this is what keeps room to
       balance the mark (task 7.3 measured the name 2.7px off without it). */
    .pin-opps .pin-foulmark { padding: 0 4px; }
    /* Two lines: the longest instruction wraps at this width. */
    .status.pin-status { min-height: 2.8em; }
    .progress.pin-progress { min-height: 1.45em; }
    /* If the board area is itself a panel, its padding would take the room
       the two opponent Boards need. */
    .panel.pin-table { padding-left: 4px; padding-right: 4px; }
    .pin-summary table { font-size: 12px; }
    .pin-summary th,
    .pin-summary td { padding: 2px 6px 2px 0; }
}
/* Narrower than a 375px phone (360px Android, 320px), two opponent Boards at
   30px no longer fit, so the small cards step down once more. */
@media (max-width: 369px) {
    .pin-table,
    .pin-discards { --pin-oppcw: 24px; }
    /* A 136px opponent head: the tightest the mark can sit beside a
       centered name. */
    .pin-opps .pin-boardhead { column-gap: 2px; }
    .pin-opps .pin-foulmark { padding: 0 3px; }
}

/* The Examples section's two boards side by side (pages/pineapple.mjs). On a
   phone two 5-card rows and their captions are wider than the sheet, so the
   cards drop to 18px and a caption wraps under its own row of cards, while
   the Winner cell stays on one line. */
@media (max-width: 560px) {
    .cheatsheet .pin-extable .cardex { --cw: 18px; }
    .cheatsheet .pin-extable .exnote { white-space: normal; }
    .cheatsheet .pin-extable td:last-child { white-space: nowrap; }
}
/* =========================================================================
   Meio (meio)
   A trump trick taker where every seat splits its hand into a first half and
   a second half, and scores the DIFFERENCE between the tricks it takes in each.
     the table as a table: West, North and East around the trick, this player
       South, the trick a cross (.mei-board, .mei-trick, .pos-*)
     each seat's tricks laid as the rules lay them, first half horizontal and
       second half upright across it, so a balanced pair is a cross and a lone
       trick is a point (.mei-tricks, .mei-stack, .mei-h, .mei-v)
     the six set aside, face up to their owner, under their own tricks in
       their own panel (.mei-mine, .mei-pilecards)
     a finished trick held in the cross until the next lead (.mei-winning)
   ANTI-JITTER. The trick rows are reserved at one row, the action box at one
   button, the pile row at one row of its cards whatever it holds, and the
   status line at two lines. The hand keeps all thirteen cards in place while
   the six are chosen, so nothing moves under the finger; after the split the
   hand box shrinks (reserveHandSpace: false), which moves nothing but the
   log below it.
   Shared ids are scoped through classes this game owns (.mei-status on the
   status line, .mei-handpanel on the hand panel).
   ========================================================================= */
/* --- the scoreboard ----------------------------------------------------- */
/* LOW score wins here, so `.progress-score.leading` marks the MINIMUM. */
.progress.mei-scoreboard { min-height: 2.6em; align-items: center; }
.mei-refchip {
    font-size: 12px;
    padding: 1px 8px;
    border-radius: 999px;
    border: 1px dashed var(--panel-border);
    opacity: 0.85;
    white-space: nowrap;
}
.mei-refchip.mei-trumpchip { border-style: solid; font-weight: 700; }
/* One width for all four of its words, so a new half cannot rewrap the line. */
.mei-refchip.mei-halfchip {
    border-style: solid;
    display: inline-block;
    min-width: 88px;
    box-sizing: border-box;
    text-align: center;
}
/* --- suit glyphs in prose ---------------------------------------------- */
.mei-suited .red { color: var(--red); }
.mei-suited .red.diamond { color: var(--red-diamond); }
.mei-suited .club { color: var(--blue-club); }
/* --- the status line ---------------------------------------------------- */
.status.mei-status { min-height: 2.6em; }
.status.mei-status.mei-turn { color: var(--accent); font-weight: 700; }
.mei-hintnote { opacity: 0.85; font-weight: 400; }
/* --- the board: the table as a table ------------------------------------ */
.mei-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas:
        ".    up    ."
        "left trick right"
        ".    down  .";
    align-items: center;
    gap: 10px 16px;
    margin-bottom: 6px;
}
.mei-board.seats-3 {
    grid-template-areas:
        "left trick right"
        ".    down  .";
}
.mei-board.seats-2 {
    grid-template-areas:
        ".    up    ."
        ".    trick ."
        ".    down  .";
}
.mei-table { display: contents; }
.mei-seat.pos-left { grid-area: left; justify-self: end; }
.mei-seat.pos-right { grid-area: right; justify-self: start; }
.mei-seat.pos-up { grid-area: up; justify-self: center; }
.mei-seat.pos-down { grid-area: down; justify-self: center; }
.mei-trick { grid-area: trick; justify-self: center; }
/* --- a seat's panel ------------------------------------------------------ */
.mei-seat {
    box-sizing: border-box;
    width: 184px;
    max-width: 100%;
    min-width: 0;
    padding: 6px 9px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.mei-seat.mei-active { outline-color: var(--accent); }
.mei-seat-head {
    display: flex;
    align-items: baseline;
    gap: 5px;
    margin-bottom: 4px;
    min-height: 18px;
}
.mei-toact { color: var(--accent); font-size: 11px; }
.mei-seat-name {
    flex: 1 1 auto;
    min-width: 0;
    font-weight: 700;
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* This round's score so far: the imbalance, low is good. */
.mei-pts {
    font-size: 13px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    color: #ffd479;
}
.mei-pts.zero { color: #7fd18a; }
/* --- a seat's tricks, laid as the rules lay them ------------------------- */
/* One stack per pair. A first-half trick lies horizontal, a second-half one
   upright across it: a CROSS is balanced and scores nothing, and a lone bar is
   a point. Reserved at one row (the most a seat can hold is seven stacks). */
.mei-tricks {
    display: flex;
    flex-wrap: nowrap;
    gap: 2px;
    min-height: 22px;
    align-items: center;
}
.mei-stack {
    position: relative;
    flex: none;
    width: 20px;
    height: 22px;
}
.mei-tk {
    position: absolute;
    box-sizing: border-box;
    border-radius: 2px;
    border: 1px solid rgba(255, 255, 255, 0.85);
    background: #2c5d8f;
}
.mei-tk.mei-h { width: 20px; height: 12px; left: 0; top: 5px; }
.mei-tk.mei-v { width: 12px; height: 22px; left: 4px; top: 0; }
/* A balanced pair: quiet. A lone trick: the point it costs, in the accent. */
.mei-stack.paired .mei-tk { background: #3f7a55; }
.mei-stack.odd .mei-tk { background: #b0473c; }
/* The legend under the board says what the two shapes mean, always. */
.mei-legend {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px 12px;
    font-size: 12px;
    opacity: 0.8;
    margin: 0 0 8px;
    min-height: 22px;
}
.mei-legend .mei-legitem { display: inline-flex; align-items: center; gap: 5px; }
/* --- the trick: a cross ------------------------------------------------- */
.mei-trick {
    --mei-playcw: 56px;
    display: grid;
    grid-template-columns: auto auto auto;
    grid-template-areas:
        ".    up    ."
        "left .     right"
        ".    down  .";
    align-items: center;
    justify-items: center;
    gap: 2px 8px;
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.mei-trick.seats-3 {
    grid-template-areas:
        "left .    right"
        ".    down .";
}
.mei-trick.seats-2 {
    grid-template-columns: auto;
    grid-template-areas:
        "up"
        "down";
    gap: 6px;
}
.mei-play.pos-up { grid-area: up; }
.mei-play.pos-left { grid-area: left; }
.mei-play.pos-right { grid-area: right; }
.mei-play.pos-down { grid-area: down; }
.mei-play .card { --cw: var(--mei-playcw); }
.mei-play .card.empty { border-style: dashed; opacity: 0.35; }
.mei-play.onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
.mei-play .card.mei-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
/* --- the action box ---------------------------------------------------- */
.mei-actions { display: block; min-height: 38px; margin-bottom: 10px; }
.mei-actionrow {
    display: flex;
    justify-content: center;
    gap: 8px;
    min-height: 38px;
}
/* --- the hand --------------------------------------------------------- */
/* One flex group per suit, a wider gap between groups than inside one. */
.panel.mei-handpanel #hand-cards { column-gap: 16px; row-gap: 8px; }
.mei-suitgroup { display: flex; flex-wrap: nowrap; gap: 6px; }
.mei-mute { opacity: 0.4; }
/* Picked to set aside: marked IN PLACE (no transform, which the FLIP animator
   would read as a move), and previewed in the pile panel underneath. */
.card.mei-picked {
    outline: 3px dashed #ffd479;
    outline-offset: 2px;
    opacity: 0.6;
}
/* --- the viewer's own panel, and the six set aside ---------------------- */
/* Wider than the other seats' panels, to hold the six under the tricks. The
   note and the row are always rendered at their height, empty behind the gate
   and in the second half, so the board never changes height. */
.mei-seat.mei-mine { width: 244px; }
.mei-pilenote {
    font-size: 11px;
    opacity: 0.7;
    min-height: 15px;
    line-height: 15px;
    margin-top: 5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.mei-pilecards {
    --mei-pilecw: 32px;
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    gap: 4px;
    margin-top: 3px;
    min-height: calc(var(--mei-pilecw) * 1.39);
}
.mei-pilecards .card { --cw: var(--mei-pilecw); }
.mei-pilecards .card.mei-slot { border-style: dashed; opacity: 0.35; }
/* --- the summary ------------------------------------------------------ */
/* A real table, so every column lines up (ui-conventions.md). */
.mei-result {
    border-collapse: collapse;
    margin: 6px auto 0;
    font-size: 13px;
}
.mei-result th,
.mei-result td {
    padding: 3px 10px;
    border-bottom: 1px solid var(--panel-border);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.mei-result .mei-rlabel { text-align: left; font-size: 12px; opacity: 0.85; }
.mei-result .mei-rnum { text-align: right; }
.mei-result th.mei-rnum { font-size: 12px; }
.mei-result tr.mei-rpoints td { font-weight: 700; }
.mei-result tr.mei-rtotal td { font-size: 16px; font-weight: 800; }
.mei-log-win { color: var(--accent); font-weight: 700; }
/* --- tablets ---------------------------------------------------------- */
/* The board column narrows beside the log, and four seats' chips wrap to a
   second row there, so that row is reserved: the final "Final" label fits on
   one row and would otherwise pull the board up under the result. */
@media (max-width: 1000px) {
    .progress.mei-scoreboard.mei-seats-4 { min-height: 62px; align-items: flex-start; }
}
/* --- phones ----------------------------------------------------------- */
/* The board restacks: the other seats in a row ACROSS THE TOP, still West,
   North and East from left to right, the cross under them, this player under
   that. */
@media (max-width: 720px) {
    .mei-board {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        grid-template-areas:
            "left  up    right"
            "trick trick trick"
            "down  down  down";
        gap: 8px 6px;
        align-items: start;
    }
    .mei-board.seats-3 {
        grid-template-areas:
            "left  .     right"
            "trick trick trick"
            "down  down  down";
    }
    .mei-board.seats-2 {
        grid-template-areas:
            "up    up    up"
            "trick trick trick"
            "down  down  down";
    }
    .mei-board.seats-2 .mei-seat.pos-up { justify-self: center; width: 184px; }
    .mei-seat.pos-left,
    .mei-seat.pos-up,
    .mei-seat.pos-right { width: auto; justify-self: stretch; padding: 5px 6px 6px; }
    .mei-trick { --mei-playcw: 44px; padding: 8px 10px; }
    /* A third of a phone holds four stacks a row, so the row may wrap to two. */
    .mei-seat:not(.pos-down) .mei-tricks { flex-wrap: wrap; row-gap: 2px; min-height: 46px; }
    .progress.mei-scoreboard { min-height: 70px; align-items: flex-start; }
    /* Four seats' chips (Bot West, Bot North, Bot East) wrap to a third row. */
    .progress.mei-scoreboard.mei-seats-4 { min-height: 104px; }
    /* The split's prompt wraps to two lines here: reserve them in px, since the
       2.6em above measured 3px short of two real lines. */
    .status.mei-status { min-height: 50px; }
}
/* ==========================================================================
   Bespoke
   ==========================================================================
   Two-player compendium climbing game: a scoresheet of four contracts, the
   other player's hand as a row of backs, a banner saying which end is high this
   trick, the trick as two lanes, and the hand with its action bar under it.
   Shared classes (.card, .cardrow, .panel, .logline, .result-actions,
   .winner-banner, .opt-help-body) are reused as-is; only Bespoke's own lives
   here, all of it prefixed `bsp-`.

   NOTHING ABOVE THE HAND MAY GROW. The other seat reserves one back's height,
   the banner two lines, the trick two lanes and a scrollbar, and the action bar
   sits UNDER the hand. See ui-conventions.md, "Containers must not move". */

/* Card sizes as variables on the layout, so the reservations below read the
   same numbers (plain values: a custom property may not reference itself). */
.bsp-layout {
    --bsp-trickcw: 46px;
    --bsp-backcw: 24px;
    --bsp-stackcw: 32px;
    --bsp-handcw: 60px;
    --bsp-handgap: 4px;
    display: grid;
    grid-template-columns: minmax(200px, 240px) minmax(0, 1fr);
    grid-template-areas:
        "sheet main"
        "hand hand";
    gap: 10px 14px;
    align-items: start;
    margin-bottom: 12px;
}

/* ---------- the scoresheet ---------- */
.bsp-sheetbox { grid-area: sheet; }
/* A real table with the game's own class, never .result-table, so every column
   is sized once for every row (ui-conventions.md, "Score tables line up by
   column"). */
.bsp-sheet {
    border-collapse: collapse;
    margin: 0 auto;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
}
.bsp-sheet th, .bsp-sheet td { padding: 3px 7px; }
.bsp-sheet th {
    font-size: 11px;
    font-weight: 700;
    opacity: 0.75;
    text-align: right;
    white-space: nowrap;
}
.bsp-sheet th.bsp-sheetname, .bsp-sheet td.bsp-sheetname { text-align: left; white-space: nowrap; }
.bsp-sheet th.bsp-sheetpts, .bsp-sheet td.bsp-sheetpts { text-align: right; opacity: 0.65; }
.bsp-sheet td.bsp-sheetcol, .bsp-sheet th.bsp-sheetcol { text-align: right; min-width: 3.6em; white-space: nowrap; }
.bsp-sheet td.won { color: var(--accent); font-weight: 800; }
.bsp-sheet td.live { color: var(--accent); font-style: italic; }
.bsp-sheet td.token { font-style: italic; }
.bsp-sheet td.open { opacity: 0.45; }
.bsp-sheet tr.current td { background: rgba(255, 209, 102, 0.09); }
.bsp-sheet tr.total td { border-top: 1px solid var(--panel-border); font-weight: 800; }
.bsp-sheet tr.clickable { cursor: pointer; }
/* While you bid, the contracts you have declared are struck through. */
.bsp-sheet tr.closed .bsp-term { text-decoration: line-through; opacity: 0.5; }
.bsp-sheet td.pickable { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25); border-radius: 4px; }
.bsp-sheet td.picked { box-shadow: inset 0 0 0 2px var(--accent); }
/* A contract name opens its definition (#bsp-def-<contract>), dressed as text
   with a dotted underline rather than as a control. */
.bsp-term {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-decoration: underline dotted;
    text-underline-offset: 3px;
    cursor: help;
    white-space: nowrap;
}
.bsp-term:hover { text-decoration-style: solid; }
.bsp-defhelp { max-width: 300px; }

/* ---------- the play area ---------- */
.bsp-main {
    grid-area: main;
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-width: 0;
}

/* The other seat: name, role, and the hand as backs, one per card, on one
   line. The turn chip is always there and only shown on turn, so the head
   never shifts. */
.bsp-opp {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 6px 10px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
    min-height: calc(var(--bsp-backcw) * 1.39 + 12px);
}
.bsp-opp.active { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.bsp-opphead { display: flex; gap: 6px; align-items: baseline; white-space: nowrap; }
.bsp-turnchip { color: var(--accent); font-size: 12px; visibility: hidden; }
.bsp-turnchip.on { visibility: visible; }
.bsp-oppname { font-weight: 700; font-size: 13px; }
.bsp-role {
    font-size: 11px;
    font-weight: 700;
    padding: 1px 7px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-ink);
}
.bsp-role.empty { visibility: hidden; }
.bsp-backs {
    display: flex;
    align-items: center;
    min-height: calc(var(--bsp-backcw) * 1.39);
}
.card.bsp-back {
    --cw: var(--bsp-backcw);
    margin-left: -14px;
    border-radius: 2px;
    box-shadow: -1px 0 0 rgba(0, 0, 0, 0.35);
}
.card.bsp-back:first-child { margin-left: 0; }

/* The banner: the stack, the contract, and this trick's ranking. Two lines
   reserved, because the ranking line is empty during the auction. */
.bsp-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    min-height: calc(var(--bsp-stackcw) * 1.39);
}
.card.bsp-stackcard { --cw: var(--bsp-stackcw); position: relative; }
/* The count sits CENTERED on the back; a corner badge read as clipped. */
.bsp-stackcount {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 800;
    font-size: 13px;
    color: #fff;
    text-shadow: 0 1px 2px #000;
}
.bsp-bannertext { display: flex; flex-direction: column; gap: 3px; text-align: center; min-width: 0; }
.bsp-contract { font-weight: 700; font-size: 14px; white-space: nowrap; min-height: 1.35em; }
.bsp-ranking {
    display: flex;
    gap: 8px;
    justify-content: center;
    align-items: center;
    font-size: 12.5px;
    min-height: 1.6em;
    white-space: nowrap;
}
.bsp-rank { padding: 1px 9px; border-radius: 999px; background: rgba(255, 255, 255, 0.1); }
.bsp-rank.inverted { background: rgba(106, 169, 224, 0.28); }
.bsp-rank.quiet { opacity: 0.7; }
.bsp-rule { opacity: 0.8; font-style: italic; }

/* The trick, as two lanes: the other player's plays on top, the viewer's
   underneath, each in its own column in play order. It scrolls rather than
   wraps, and `safe center` keeps the left end reachable once it overflows. */
.bsp-trick {
    display: grid;
    grid-template-rows: repeat(2, calc(var(--bsp-trickcw) * 1.39 + 8px));
    grid-auto-columns: max-content;
    grid-auto-flow: column;
    justify-content: safe center;
    gap: 4px 6px;
    overflow-x: auto;
    padding: 6px 8px;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.14);
}
.bsp-trickplay {
    display: flex;
    align-items: center;
    padding: 3px;
    border: 1px solid transparent;
    border-radius: 7px;
}
/* Side by side, not overlapped: the simplified atlas prints the rank in the
   middle of the card, so any real overlap hides it. The class is not
   `bsp-play`, which is the Play button's: sharing it gave that button this
   column's 3px padding. */
.bsp-trickplay { gap: 2px; }
.card.bsp-trickcard { --cw: var(--bsp-trickcw); }
/* Whatever has to be beaten, or the play that took the held trick. */
.bsp-trickplay.best { border-color: var(--accent); background: rgba(255, 209, 102, 0.1); }
.bsp-trick.held .bsp-trickplay:not(.best) { filter: brightness(0.75); }

/* ---------- the hand ---------- */
.bsp-handpanel { grid-area: hand; }
/* This device is being asked to act: the panel and the status line light up.
   A box-shadow, not a border, so nothing changes size. */
.panel.bsp-handpanel.active { box-shadow: inset 0 0 0 2px var(--accent); }
.status.bsp-status.lit { box-shadow: inset 3px 0 0 var(--accent); }
/* The cards never overlap: a faded or ringed card has to show its whole edge.
   So the card width follows the tray, sized for the largest hand there is
   (18, the Defender after drawing 2) on one line, between a readable floor and
   the desktop size; below the floor the hand wraps. The tray is its own size
   container so `cqw` reads its width. */
.bsp-handpanel #hand-cards {
    container-type: inline-size;
    gap: 4px var(--bsp-handgap);
}
.bsp-handpanel #hand-cards .card {
    --cw: clamp(40px, (100cqw - 17 * var(--bsp-handgap)) / 18, var(--bsp-handcw));
}
/* No cosmetic transform on a Bespoke card: the FLIP animator reads a lift as a
   move and flies a ghost for it (see the webplay-flip-jitter note). The
   outline stays; hover brightens instead of lifting. */
.bsp-handpanel .card.sel {
    transform: none;
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 0 4px rgba(255, 209, 102, 0.25);
}
.bsp-handpanel .card.clickable:hover { transform: none; filter: brightness(1.14); }
/* Cards that cannot be part of any legal play. A filter, not opacity. */
.card.bsp-faded { filter: brightness(0.5) saturate(0.6); }
/* The Defender's freshly drawn cards, until the discard. */
.card.bsp-drawn { box-shadow: 0 0 0 3px #6aa9e0; }
/* Cards in a bomb: a light orange ring inside the card edge, all deal. Inset,
   so it takes no room and cannot touch a neighbor; combined by hand with the
   other two rings, which are outside the edge. */
.bsp-handpanel .card.bsp-bomb { box-shadow: inset 0 0 0 3px rgba(240, 150, 50, 0.9); }
.bsp-handpanel .card.bsp-bomb.bsp-drawn {
    box-shadow: inset 0 0 0 3px rgba(240, 150, 50, 0.9), 0 0 0 3px #6aa9e0;
}
.bsp-handpanel .card.bsp-bomb.sel {
    box-shadow: inset 0 0 0 3px rgba(240, 150, 50, 0.9), 0 0 0 4px rgba(255, 209, 102, 0.25);
}

/* The action bar, under the hand: controls first, then one note line. */
.bsp-actions {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    margin-top: 8px;
    min-height: 84px;
}
.bsp-picks { justify-content: center; flex-wrap: wrap; }
/* A contract already declared: struck through, dimmed, with its score. The
   shared :disabled dimming alone read like an ordinary unpicked button. */
.bsp-picks .seg.done { text-decoration: line-through; opacity: 0.45; font-style: italic; }
.bsp-buttons { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
/* Play and Pass side by side at one size, whatever the label says. */
.bsp-buttons .btn { min-width: 6.5em; }
.bsp-note { font-size: 12px; opacity: 0.88; min-height: 1.4em; text-align: center; }
.bsp-note.bad { color: var(--red); opacity: 1; }
.bsp-note.hint { color: var(--accent); opacity: 1; }
.bsp-narrow { display: none; }

/* ---------- the deal summary (painted into #result-panel) ---------- */
.bsp-resultnote {
    font-size: 12.5px;
    opacity: 0.88;
    text-align: center;
    max-width: 46ch;
    margin: 0 auto 6px;
    line-height: 1.4;
}
.bsp-resultsheet { margin: 8px auto; }

/* ---------- phone ---------- */
@media (max-width: 720px) {
    .bsp-layout {
        --bsp-trickcw: 36px;
        --bsp-backcw: 18px;
        --bsp-stackcw: 26px;
        --bsp-handcw: 40px;
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas:
            "main"
            "hand"
            "sheet";
    }
    .card.bsp-back { margin-left: -11px; }
    /* A phone wraps the hand. A 2px gap puts eight 40px cards on the 336px
       tray at 390 wide, so a 16-card hand is two rows. */
    .bsp-layout { --bsp-handgap: 2px; }
    /* Two lines reserved: the recap plus the instruction wraps here. */
    .status.bsp-status { min-height: calc(2 * 1.45em); }
    .bsp-contract { font-size: 13px; }
    .bsp-ranking { font-size: 12px; }
}
@media (max-width: 480px) {
    .bsp-wide { display: none; }
    .bsp-narrow { display: inline; }
}
/* =========================================================================
   Dibble (dibble)
   Layout intent: one centered column, read top to bottom the way a turn is
   read: the opponent's head line (name, hand as backs, Joker), the five lanes
   (their row, the neutral row, your row, then the lane names), the sequence on
   the table, your hand, and under it the controls. The lanes grid and the
   table hold a fixed height, so a pull, a swap or a new combination never
   moves anything below them. The acting side carries the accent (a tab on the
   opponent's panel, the hand panel's border), and the status line lights up
   whenever this device is being asked to act.
   ON A PHONE the whole board has to fit one screen (reported: "too much
   vertical space"). Measured at 390 x 844, the hand used to start below the
   fold. So: the controls go UNDER the hand, where they need no reserved rows
   (the hand tray keeps its tallest shape within a deal, handspace.ts, so they
   never move); a dealt hand of 16 fits two rows by overlapping its cards 8px;
   and the opponent's line, the lanes, and the table are tighter. A used Joker
   is not drawn, and a face-up one is small.
   ========================================================================= */

/* Two lines' worth at every width from 340px up: every sentence in def.ts's
   statusText is held to two lines at 360px with the longest seat names, which
   was measured in a browser. Below 340px some run to three. */
.status.dib-status { min-height: 2.9em; }
@media (max-width: 340px) {
    .status.dib-status { min-height: 4.4em; }
}
.status.dib-status.dib-turn {
    border-left: 3px solid var(--accent);
    padding-left: 10px;
    font-weight: 600;
}

/* Desktop: the lanes on the left, and beside them in one column the
   opponent, the table and the controls, so the board fits one screen. Below
   900px the side column dissolves into the one column (display: contents),
   with the opponent moved up above the lanes. */
.dib-board {
    display: grid;
    grid-template-columns: auto minmax(300px, 440px);
    justify-content: center;
    align-items: start;
    column-gap: 20px;
    row-gap: 12px;
    margin-bottom: 12px;
}
.dib-lanespanel { grid-column: 1; grid-row: 1; }
.dib-side {
    grid-column: 2;
    grid-row: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    min-width: 0;
}
.dib-board > .dib-handpanel { grid-column: 1 / -1; grid-row: 2; }
@media (max-width: 900px) {
    .dib-board {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 10px;
    }
    .dib-side { display: contents; }
    .dib-seat { order: -1; }
    .dib-lanes { --cw: 56px; }
    /* The controls and the note go under the hand. Nothing aimed at sits
       below them, so they take only the height they need, and the board's
       gap is the only space between them. */
    .dib-board > .dib-handpanel { order: 1; margin-bottom: 0; }
    .dib-board .dib-actions { order: 2; min-height: 0; margin: 0; }
    .dib-board .dib-note { order: 3; min-height: 0; }
}

/* --- the opponent -------------------------------------------------------- */
.dib-seat {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    min-height: 66px;
    max-width: 100%;
    margin-top: 8px;
    padding: 8px 14px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
.dib-seat.dib-acting { border-color: rgba(232, 112, 58, 0.75); }
.dib-totab {
    position: absolute;
    top: -11px;
    left: 12px;
    padding: 1px 8px;
    border-radius: 999px;
    background: rgba(232, 112, 58, 0.92);
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    white-space: nowrap;
}
.dib-seatname { font-size: 13px; font-weight: 700; white-space: nowrap; }
.dib-backs { display: flex; align-items: center; min-width: 34px; }
.card.dib-oppback { --cw: 34px; }
.card.dib-oppback + .card.dib-oppback { margin-left: -24px; }
.card.mini.dib-joker { --cw: 26px; }

/* --- the lanes ------------------------------------------------------------ */
.dib-lanespanel {
    margin: 0;
    padding: 12px 12px 8px;
}
.dib-lanes {
    --cw: 62px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.dib-band {
    position: relative;
    display: grid;
    grid-template-columns: repeat(5, calc(var(--cw) + 10px));
    justify-content: center;
    gap: 6px;
    padding: 6px 6px 5px;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.12);
}
.dib-band-top { background: rgba(160, 60, 40, 0.14); }
.dib-band-bot { background: rgba(38, 120, 60, 0.16); }
.dib-band-mid { margin: 4px 0; }
.dib-bandtag {
    position: absolute;
    top: -8px;
    left: 8px;
    padding: 0 6px;
    border-radius: 6px;
    background: var(--panel);
    font-size: 10px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.8;
    white-space: nowrap;
    pointer-events: none;
}
.dib-cell {
    display: flex;
    align-items: center;
    justify-content: center;
    height: calc(var(--cw) * 89 / 64 + 6px);
    border-radius: 8px;
}
.dib-slot {
    width: var(--cw);
    height: calc(var(--cw) * 89 / 64);
    border-radius: 6%;
    border: 1px dashed rgba(255, 255, 255, 0.18);
    box-sizing: border-box;
}
/* The lane this sequence is fought over, down its whole column. */
.dib-cell.dib-live { background: rgba(255, 209, 102, 0.12); box-shadow: inset 0 0 0 1px rgba(255, 209, 102, 0.45); }
/* The card the last pull or swap moved: a quiet ring on its cell. */
.dib-cell.dib-moved { box-shadow: inset 0 0 0 2px rgba(106, 169, 224, 0.75); }
.dib-cell.dib-picked { box-shadow: inset 0 0 0 3px var(--accent); }
.dib-cell.dib-naming { box-shadow: inset 0 0 0 2px rgba(255, 209, 102, 0.8); }
/* A known face-down card: its value on the back, for this viewer only. */
.card.dib-point { position: relative; }
.dib-known {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    padding: 2px 6px;
    border-radius: 6px;
    border: 1px solid rgba(0, 0, 0, 0.35);
    background: #f2efe9;
    color: #1a1a1a;
    font-size: 15px;
    font-weight: 800;
    line-height: 1.1;
    white-space: nowrap;
    pointer-events: none;
}
.dib-known .red { color: #c8102e; }
.eye-badge.dib-eye { top: 4%; right: 5%; bottom: auto; width: 18px; height: 18px; }
.eye-badge.dib-eye svg { width: 12px; height: 12px; }
.dib-labels {
    display: grid;
    grid-template-columns: repeat(5, calc(var(--cw) + 10px));
    justify-content: center;
    gap: 6px;
    padding: 0 6px;
}
.dib-label {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 3px 0;
    border-radius: 6px;
    line-height: 1.15;
    text-align: center;
}
.dib-label.dib-live { background: rgba(255, 209, 102, 0.18); }
.dib-labelname { font-size: 12px; font-weight: 700; white-space: nowrap; }
.dib-labelex { font-size: 12px; opacity: 0.65; font-variant-numeric: tabular-nums; }

/* --- the table: the sequence being played -------------------------------- */
.dib-table {
    --cw: 46px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    min-height: calc(var(--cw) * 89 / 64 + 30px);
    max-width: 100%;
}
.dib-combo {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
}
/* Whose combination it is, by height: the viewer's sit a step lower. */
.dib-combo.dib-mine { margin-top: 24px; }
.dib-combo.dib-theirs { margin-bottom: 24px; }
.dib-combocards { display: flex; gap: 3px; }
.dib-combo.dib-latest .dib-combocards,
.dib-combo.dib-won .dib-combocards {
    padding: 3px;
    border-radius: 8px;
    box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.7);
}
.dib-heldtable .dib-combo:not(.dib-won) { opacity: 0.55; }
.dib-jokertag {
    position: absolute;
    top: -9px;
    padding: 0 6px;
    border-radius: 6px;
    background: rgba(120, 60, 160, 0.95);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
}
.dib-earlier { font-size: 12px; opacity: 0.6; }
.dib-empty { font-size: 13px; opacity: 0.6; white-space: nowrap; }

/* --- the controls ---------------------------------------------------------- */
/* Two rows of buttons, always: Pass and up to four Joker options on a phone,
   so an answer's controls never push the hand down. */
.dib-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-content: flex-start;
    gap: 8px;
    min-height: 86px;
}
.dib-note {
    min-height: 1.4em;
    line-height: 1.4;
    font-size: 13px;
    opacity: 0.8;
    text-align: center;
}
.dib-nameline {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.14);
}
.dib-namelabel { font-size: 13px; margin-right: 4px; white-space: nowrap; }
.btn.dib-namebtn { min-width: 34px; }

/* --- the hand -------------------------------------------------------------- */
.dib-handpanel { width: 100%; box-sizing: border-box; }
.dib-handpanel.dib-acting { border-color: rgba(232, 112, 58, 0.75); }
.dib-handpanel #hand-title {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
}
/* Dimmed with a filter, not opacity: on a phone the hand overlaps, and a
   see-through card shows its neighbor's edge through it. */
.card.dib-faded { filter: brightness(0.5) saturate(0.4); }
/* The title keeps the Joker's height when the Joker is gone, so the hand does
   not move up when it is used. */
.dib-handpanel #hand-title { min-height: calc(26px * 89 / 64); }

/* --- the summary ------------------------------------------------------------ */
.dib-resulttitle { text-align: center; }
.dib-result, .dib-match {
    border-collapse: collapse;
    margin: 8px auto;
}
.dib-result th, .dib-result td, .dib-match th, .dib-match td {
    padding: 4px 10px;
    text-align: left;
    vertical-align: middle;
}
.dib-result .dib-resultnum, .dib-match .dib-resultnum,
.dib-result .dib-resulthead th:nth-child(n+3), .dib-match .dib-resulthead th:nth-child(n+2) {
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.dib-resulthead th { font-size: 12px; opacity: 0.7; font-weight: 600; }
.dib-resultname { white-space: nowrap; }
.dib-resulttotal { font-weight: 800; }
.dib-resultwin .dib-resultname { color: var(--accent); }
.dib-resultcards { display: flex; gap: 8px; min-height: 60px; align-items: center; }
.dib-resultcard { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.dib-resultcard .card { --cw: 34px; }
.dib-resultworth { font-size: 12px; font-variant-numeric: tabular-nums; }
.dib-resultworth.dib-right { color: #7fd48a; font-weight: 700; }
.dib-resultworth.dib-wrong { color: #f08a7a; }
.card.dib-wrong { opacity: 0.6; }
/* A guesser's card that scores 0 because another was named wrong. */
.card.dib-void { opacity: 0.6; }
.dib-resultworth.dib-void { opacity: 0.7; }

@media (max-width: 560px) {
    .dib-board { gap: 8px; }
    /* The opponent's line: name, backs, and Joker at one small size. */
    .dib-seat {
        gap: 8px;
        /* One small card tall, padding and border included, with or without
           a card in it (an empty hand and a used Joker). */
        min-height: calc(26px * 89 / 64 + 10px);
        margin-top: 2px;
        padding: 4px 12px;
        box-sizing: border-box;
    }
    .card.dib-oppback { --cw: 26px; }
    .card.dib-oppback + .card.dib-oppback { margin-left: -18px; }
    /* The lanes: smaller cards, the three bands closer together. */
    .dib-lanes { --cw: 40px; gap: 8px; }
    .dib-band, .dib-labels { gap: 4px; grid-template-columns: repeat(5, calc(var(--cw) + 6px)); }
    .dib-band { padding: 4px 3px 3px; }
    .dib-band-mid { margin: 0; }
    .dib-cell { height: calc(var(--cw) * 89 / 64 + 4px); }
    .dib-lanespanel { padding: 10px 6px 4px; }
    .dib-label { padding: 1px 0; line-height: 1.1; }
    .dib-labelname { font-size: 11px; }
    /* The examples (22, 333) are left to the label's tooltip here: the names
       say the same, and the line cost the board a row. */
    .dib-labelex { display: none; }
    .dib-known { font-size: 13px; padding: 1px 4px; }
    /* The table: a smaller step between the two players' combinations. */
    .dib-table { --cw: 34px; gap: 8px; min-height: calc(var(--cw) * 89 / 64 + 18px); }
    .dib-combo.dib-mine { margin-top: 12px; }
    .dib-combo.dib-theirs { margin-bottom: 12px; }
    /* The hand: a dealt 16 fits two rows. Each card overlaps the next by 8px
       (4px a side, which the tray's padding absorbs), and every card is
       positioned so the later card always paints on top, lifted or dimmed. */
    .panel.dib-handpanel { padding: 10px 12px; }
    .dib-handpanel .panel-head h3 { margin-bottom: 6px; }
    .dib-handpanel .card.mini.dib-joker { --cw: 22px; }
    .dib-handpanel #hand-title { min-height: calc(22px * 89 / 64); }
    .dib-handpanel #hand-cards { column-gap: 0; padding: 0 4px; }
    .dib-handpanel #hand-cards .card { position: relative; margin: 0 -4px; }
}
/* =========================================================================
   Prime Time (primetime)
   Layout intent: the track on the left and, beside it in one column, this
   turn's cards, the action bar, and every player's kept cards, so the board
   fits one desktop screen. Below 900px the side column dissolves into one
   centered column under the track (display: contents). The track is a hundred
   chart climbing upward, Start below and 101 above, with only the primes
   accented: the keep spaces (primes 11 to 97) filled, 2, 3, 5 and 7 outlined.
   Every box that changes during a turn reserves its tallest shape.
   ========================================================================= */
.status.ptm-status { min-height: 2.9em; }
/* The tallest status on a phone is five lines: a bot's turn that bumped you,
   then the keep instruction (measured at 390px, 101px). */
@media (max-width: 620px) {
    .status.ptm-status { min-height: 5.7em; }
}
.status.ptm-status.ptm-myturn {
    border-left: 3px solid var(--accent);
    padding-left: 10px;
    font-weight: 600;
}
.ptm-recap { font-weight: 400; opacity: 0.85; }
.ptm-hintnote { color: var(--accent); }

/* Seat colors: tokens, dots, and the acting seat's border. */
.ptm-seat0 { --ptm-seat: #5fb8ff; }
.ptm-seat1 { --ptm-seat: #ff7a59; }
.ptm-seat2 { --ptm-seat: #c39bff; }
.ptm-seat3 { --ptm-seat: #f1f1f1; }

.ptm-main {
    display: grid;
    grid-template-columns: auto minmax(300px, 380px);
    justify-content: center;
    align-items: start;
    column-gap: 20px;
    row-gap: 12px;
    margin-bottom: 12px;
}
.ptm-side {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    min-width: 0;
}
@media (max-width: 900px) {
    .ptm-main {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 10px;
    }
    .ptm-side { display: contents; }
    .ptm-turn, .ptm-actions, .ptm-seats { width: min(100%, 460px); }
}

/* --- the track ------------------------------------------------------------ */
.ptm-boardpanel {
    --ptm-cell: 42px;
    padding: 10px;
    margin: 0;
}
@media (max-width: 520px) {
    .ptm-boardpanel {
        --ptm-cell: calc((100vw - 60px) / 10);
        padding: 6px;
    }
}
.ptm-track {
    display: grid;
    grid-template-columns: repeat(10, var(--ptm-cell));
    gap: 3px;
    justify-content: center;
}
.ptm-cell {
    position: relative;
    box-sizing: border-box;
    width: var(--ptm-cell);
    height: var(--ptm-cell);
    margin: 0;
    padding: 0;
    border-radius: 6px;
    border: 1px solid rgba(255, 255, 255, 0.09);
    background: rgba(255, 255, 255, 0.05);
    color: rgba(242, 239, 233, 0.5);
    font: inherit;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: space-between;
    overflow: hidden;
}
button.ptm-cell { cursor: pointer; appearance: none; -webkit-appearance: none; }
.ptm-num {
    font-size: calc(var(--ptm-cell) * 0.3);
    line-height: 1;
    margin-top: 3px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.ptm-cell.ptm-prime {
    border-color: rgba(255, 209, 102, 0.5);
    color: rgba(255, 209, 102, 0.85);
}
.ptm-cell.ptm-keepsq {
    border-color: rgba(255, 209, 102, 0.8);
    background: rgba(255, 209, 102, 0.2);
    color: var(--accent);
    font-weight: 700;
}
/* Start and 101 span the row, so every pawn fits in one line. */
.ptm-cell.ptm-endsq {
    grid-column: 1 / -1;
    width: auto;
    height: calc(var(--ptm-cell) * 0.9);
    flex-direction: row;
    justify-content: center;
    gap: 10px;
}
.ptm-endsq .ptm-num { margin: 0; font-size: calc(var(--ptm-cell) * 0.34); }
.ptm-cell.ptm-goalsq {
    border-color: var(--accent);
    background: rgba(255, 209, 102, 0.3);
    color: var(--accent);
    font-weight: 700;
}
.ptm-cell.ptm-startsq { color: rgba(242, 239, 233, 0.7); }
.ptm-cell.lit {
    box-shadow: inset 0 0 0 2px var(--ink);
    color: var(--ink);
    background-color: rgba(255, 255, 255, 0.24);
}
.ptm-cell.lit:hover, .ptm-cell.lit:focus-visible { background-color: rgba(255, 255, 255, 0.3); }
/* Reachable before anything is picked: marked, quieter than lit. */
.ptm-cell.ptm-reach {
    box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.4);
    color: rgba(242, 239, 233, 0.9);
    background-color: rgba(255, 255, 255, 0.1);
}
.ptm-cell.ptm-reach:hover, .ptm-cell.ptm-reach:focus-visible { background-color: rgba(255, 255, 255, 0.24); }
/* A space several moves reach, tapped: the choices are in the action bar. */
.ptm-cell.ptm-target { box-shadow: inset 0 0 0 3px var(--accent); }
.ptm-cell.ptm-from { box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.55); }
.ptm-cell.ptm-recent { outline: 1px dashed rgba(255, 255, 255, 0.55); outline-offset: -4px; }
.ptm-cell.hinted { box-shadow: inset 0 0 0 3px var(--accent); }

.ptm-tokens {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 1px;
    margin-bottom: 3px;
    min-height: 1px;
}
.ptm-endsq .ptm-tokens { margin: 0; gap: 4px; }
.ptm-pawn {
    display: inline-block;
    box-sizing: border-box;
    width: calc(var(--ptm-cell) * 0.36);
    height: calc(var(--ptm-cell) * 0.36);
    border-radius: 50%;
    background: var(--ptm-seat);
    border: 2px solid rgba(0, 0, 0, 0.6);
}
.ptm-endsq .ptm-pawn {
    width: calc(var(--ptm-cell) * 0.46);
    height: calc(var(--ptm-cell) * 0.46);
}
.ptm-pawn.ptm-picked { outline: 2px solid var(--ink); outline-offset: 1px; }

/* --- this turn's cards ------------------------------------------------------ */
.ptm-turn {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    margin: 0;
}
.ptm-turn h3.ptm-turnhead { margin: 0; text-align: center; white-space: nowrap; }
.ptm-cards {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: 12px;
    min-height: calc(var(--cw) * 1.39 + 22px);
}
.ptm-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
}
.ptm-slot.ptm-spent .card { opacity: 0.4; }
.ptm-slot.ptm-replaced .card { opacity: 0.35; }
.ptm-slot.ptm-replaced .card.ptm-keepable { opacity: 1; }
.ptm-card.sel { outline: 3px solid var(--accent); outline-offset: 2px; box-shadow: 0 0 0 6px rgba(255, 209, 102, 0.35); }
.ptm-card.ptm-keepable { outline: 3px solid var(--accent); outline-offset: 2px; opacity: 1; }
.ptm-slot.ptm-spent .card.ptm-keepable { opacity: 1; }
.ptm-slotfoot {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
    height: 12px;
}
.ptm-pips { display: flex; gap: 4px; }
.ptm-pip {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--ink);
}
.ptm-pip.spent { background: transparent; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.45); }
.ptm-cardtag { font-size: 11px; line-height: 12px; opacity: 0.7; white-space: nowrap; }
.ptm-empty { font-size: 13px; opacity: 0.6; align-self: center; white-space: nowrap; }

.ptm-pawnrow {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px;
    min-height: 32px;
}
.ptm-pawnbtn { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.ptm-pawnbtn.sel { outline: 2px solid var(--accent); outline-offset: 1px; }
.ptm-dot {
    display: inline-block;
    width: 11px;
    height: 11px;
    border-radius: 50%;
    background: var(--ptm-seat);
    border: 1px solid rgba(0, 0, 0, 0.5);
    flex: none;
}

.ptm-deckline {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 3px;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.ptm-decklabel { margin-right: 6px; opacity: 0.75; }
.ptm-deckcell {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    min-width: 20px;
    padding: 1px 2px;
    border-radius: 4px;
    background: rgba(255, 255, 255, 0.06);
    line-height: 1.15;
}
.ptm-deckrank { font-weight: 700; }
.ptm-deckcount { opacity: 0.75; }
.ptm-deckcell.ptm-gone { opacity: 0.35; }

/* --- the action bar: two reserved rows, controls then a note ----------------- */
/* Up to four sums wrap to two rows in the side column, so two rows are held. */
.ptm-actions {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    min-height: 110px;
}
.ptm-controls {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    min-height: 36px;
}
.ptm-opbtn { font-variant-numeric: tabular-nums; white-space: nowrap; }
.ptm-note { min-height: 1.4em; font-size: 13px; opacity: 0.8; text-align: center; }

/* --- the seats ---------------------------------------------------------------- */
.ptm-seats { display: flex; flex-direction: column; gap: 8px; }
.ptm-seat {
    position: relative;
    padding: 6px 10px 8px;
    border-radius: 10px;
    border: 1px solid var(--panel-border);
    background: rgba(0, 0, 0, 0.16);
}
.ptm-seat.ptm-acting { border-color: var(--ptm-seat); }
.ptm-seathead {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    height: 22px;
    line-height: 22px;
    white-space: nowrap;
}
.ptm-seatname { font-weight: 600; }
.ptm-totab { color: var(--ptm-seat); font-size: 12px; }
.ptm-hometag { font-size: 12px; opacity: 0.75; }
/* A fixed height, not a minimum: a scroll strip's own scrollbar or a card's
   outline must not make the panel grow a few pixels mid-game. */
.ptm-keptrow {
    height: calc(46px * 1.39 + 12px);
    box-sizing: border-box;
    align-items: center;
    gap: 6px;
    margin-top: 4px;
    padding: 4px 4px;
    overflow-y: hidden;
}
/* Centered with auto margins on the ends, not justify-content: a scroll strip
   centered that way clips its left overflow out of reach (ui-conventions.md).
   Auto margins take the free space when there is some and collapse to 0 when
   the row overflows, so a long row still scrolls from its first card. */
.ptm-keptrow > :first-child { margin-left: auto; }
.ptm-keptrow > :last-child { margin-right: auto; }
.ptm-kept.sel { outline: 3px solid var(--accent); outline-offset: 2px; }

/* --- the result ---------------------------------------------------------------- */
table.ptm-result {
    border-collapse: collapse;
    margin: 8px auto 0;
    font-variant-numeric: tabular-nums;
}
table.ptm-result th, table.ptm-result td {
    padding: 4px 10px;
    text-align: left;
    border-bottom: 1px solid var(--panel-border);
}
/* The dot and the name sit in a span: a th with display: flex stops being a cell. */
.ptm-namebox { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
table.ptm-result tr.ptm-won th, table.ptm-result tr.ptm-won td { color: var(--accent); }

/* --- the log ---------------------------------------------------------------------- */
#log-entries .logline.ptm-logover { font-weight: 700; }
#log-entries .logline.ptm-logdraw { opacity: 0.75; }
/* =========================================================================
   Chai Tea Jazz (chaiteajazz)
   A contract trick-taker on a shared sheet: every side declares Trump, Under,
   Slalom, and the Joker contract once, and each row pays its best and worst.
     the table as a table: the other seats clockwise round the middle of the
       table (.ctj-board, .pos-*); a ring of five at 6 Seats
     the middle of the table: the picking controls while picking, the trick
       while playing, stacked in one cell                     (.ctj-center)
     a small panel per other seat: name, the Barrel, the Side, Declarer,
       "passed" (.ctj-seat, .ctj-chip); at 2 Seats the opponent's straw-pile.
       This seat's chips sit beside "Your hand"               (.ctj-mychips)
     the Bet Debt, the Declaration, and Call the game in one strip (.ctj-readout)
     the sheet as a real table, column by column              (.ctj-sheet)
     a finished trick held until the next lead, winner ringed (.ctj-winning)
   ANTI-JITTER. The readout, the middle of the table, the chip rows, and the
   straw rows are reserved at their tallest ordinary shape at every breakpoint,
   and every panel stays rendered all round, so nothing above the hand moves as
   a round runs. The picking panel and the trick are both laid out all the time
   and the idle one is hidden with visibility, so the middle keeps the taller
   one's height. Rules on a SHARED id go through a class this game owns
   (.ctj-status on the status line, .ctj-handpanel on the hand panel).
   ========================================================================= */

/* --- suit glyphs in prose ------------------------------------------------ */
.ctj-suited .red { color: var(--red); }
.ctj-suited .red.diamond { color: var(--red-diamond); }
.ctj-suited .club { color: var(--blue-club); }

/* --- the status line and the round chip ---------------------------------- */
.status.ctj-status { min-height: 2.6em; }
.status.ctj-status.ctj-turn { color: var(--accent); font-weight: 700; }
.ctj-roundchip {
    font-size: 12px;
    padding: 1px 8px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    white-space: nowrap;
}

/* --- the readout ---------------------------------------------------------- */
.ctj-readout {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-content: flex-start;
    gap: 5px 6px;
    min-height: 54px;
    margin-bottom: 8px;
}
.ctj-chip {
    display: inline-block;
    font-size: 12px;
    padding: 1px 8px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    white-space: nowrap;
    line-height: 1.6;
}
.ctj-chip.ctj-debt { border-color: #ffd479; color: #ffd479; font-weight: 700; }
.ctj-chip.ctj-decl { border-color: var(--accent); font-weight: 700; }
.ctj-chip.ctj-dir { border-style: dashed; }

/* --- the board: the table as a table -------------------------------------- */
/* The middle column is sized for the widest picking row (four suit buttons),
   and the side columns keep room for a seat panel. */
.ctj-board {
    display: grid;
    grid-template-columns: minmax(136px, 1fr) minmax(0, 470px) minmax(136px, 1fr);
    align-items: center;
    justify-items: center;
    gap: 10px 14px;
    margin-bottom: 10px;
}
.ctj-board.ctj-seats-2 { grid-template-areas: ". n ." ". trick ."; }
.ctj-board.ctj-seats-3 { grid-template-areas: "w trick e"; }
.ctj-board.ctj-seats-4 { grid-template-areas: ". n ." "w trick e"; }
.ctj-board.ctj-seats-6 { grid-template-areas: "ul n ur" "ll trick lr"; }
.ctj-table { display: contents; }
.ctj-seat.pos-n { grid-area: n; }

/* One cell, two layers: whichever is not in use keeps its box but is hidden,
   so the cell is always the taller of the two. */
.ctj-center {
    grid-area: trick;
    display: grid;
    justify-items: center;
    align-items: center;
    width: 100%;
}
.ctj-center > * { grid-area: 1 / 1; }
.ctj-center > .ctj-off { visibility: hidden; }
.ctj-seat.pos-w { grid-area: w; justify-self: end; }
.ctj-seat.pos-e { grid-area: e; justify-self: start; }
.ctj-seat.pos-ul { grid-area: ul; justify-self: end; }
.ctj-seat.pos-ll { grid-area: ll; justify-self: end; }
.ctj-seat.pos-ur { grid-area: ur; justify-self: start; }
.ctj-seat.pos-lr { grid-area: lr; justify-self: start; }

/* --- a seat's panel -------------------------------------------------------- */
.ctj-seat {
    box-sizing: border-box;
    width: 176px;
    max-width: 100%;
    min-width: 0;
    padding: 6px 9px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.ctj-board.ctj-seats-2 .ctj-seat.pos-n { width: 300px; }
.ctj-seat.ctj-active { outline-color: var(--accent); }
.ctj-seat-head { display: flex; align-items: baseline; justify-content: center; gap: 5px; }
.ctj-toact { color: var(--accent); font-size: 11px; }
.ctj-seat-name {
    min-width: 0;
    font-weight: 700;
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Always rendered at one chip row, empty or not. */
.ctj-seat-chips {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: 4px;
    min-height: 22px;
    margin-top: 3px;
}
.ctj-seat-chips .ctj-chip { font-size: 11px; padding: 0 6px; }
.ctj-barrelchip { border-color: #c79a5a; color: #e6c28c; font-weight: 700; }
.ctj-side-0 { border-color: #7fb3ff; color: #a9ccff; }
.ctj-side-1 { border-color: #ff9f7f; color: #ffc0aa; }
.ctj-role { border-color: var(--accent); color: var(--accent); }
.ctj-passed { opacity: 0.7; font-style: italic; }

/* --- straw-piles (2 Seats) ------------------------------------------------- */
/* Each slot: its face-up card over the face-down card beneath, offset so the
   back peeks out. Reserved at one card's height plus the offset. */
.ctj-straws {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: 6px;
    margin-top: 5px;
}
.ctj-straw { position: relative; }
.ctj-straw .ctj-strawback { position: absolute; top: 6px; left: 0; }
.ctj-straw .card:not(.ctj-strawback) { position: relative; }
.ctj-straws:not(.ctj-mystraws-row) .ctj-straw { width: var(--cw, 46px); min-height: calc(46px * 1.39 + 6px); }
.ctj-straws:not(.ctj-mystraws-row) .card { --cw: 46px; }
.ctj-mystraws { min-height: 0; }
.ctj-mystraws .ctj-straws { margin: 0 0 6px; }
.ctj-mystraws .ctj-straw { min-height: calc(var(--cw) * 1.39 + 8px); min-width: var(--cw); }
.ctj-strawgone { opacity: 0.2; border-style: dashed; }

/* --- the trick -------------------------------------------------------------- */
/* The felt spans the middle of the table, so each side seat's card sits at the
   edge next to that seat's panel. */
.ctj-trick {
    --ctj-playcw: 56px;
    box-sizing: border-box;
    width: 100%;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    justify-items: center;
    gap: 6px 8px;
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.ctj-seats-2 .ctj-trick { grid-template-areas: ". n aside" ". s ."; }
.ctj-seats-3 .ctj-trick { grid-template-areas: "w . e" ". s ."; }
.ctj-seats-4 .ctj-trick { grid-template-areas: ". n ." "w . e" ". s ."; }
.ctj-seats-6 .ctj-trick { --ctj-playcw: 44px; grid-template-areas: "ul n ur" "ll . lr" ". s ."; }
.ctj-play.pos-n { grid-area: n; }
.ctj-play.pos-s { grid-area: s; }
.ctj-play.pos-w { grid-area: w; justify-self: start; }
.ctj-play.pos-e { grid-area: e; justify-self: end; }
.ctj-play.pos-ul { grid-area: ul; justify-self: start; }
.ctj-play.pos-ll { grid-area: ll; justify-self: start; }
.ctj-play.pos-ur { grid-area: ur; justify-self: end; }
.ctj-play.pos-lr { grid-area: lr; justify-self: end; }
.ctj-play .card { --cw: var(--ctj-playcw); }
.ctj-play .card.empty { border-style: dashed; opacity: 0.35; }
.ctj-play.onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
.ctj-play .card.ctj-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
.ctj-play .card.ctj-leading { outline: 2px dashed var(--accent); outline-offset: 2px; }
.ctj-aside { grid-area: aside; display: flex; gap: 3px; }
.ctj-aside .card { --cw: 34px; }

/* --- the picking panel -------------------------------------------------------- */
/* Four reserved rows of controls, then one line of prose, so no control moves
   when the prose changes or a detail row fills. */
.ctj-picking { width: 100%; }
.ctj-pickrow {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 6px;
    /* One .btn row: a .btn is 38.4px tall, so 38px let a filled row grow. */
    min-height: 38.4px;
    margin-bottom: 4px;
}
.ctj-pickrow .btn { white-space: nowrap; }
.ctj-pickrow .btn.selected { outline: 2px solid var(--accent); }
.ctj-amount {
    min-width: 2.4em;
    text-align: center;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
.ctj-steplabel { font-size: 12px; opacity: 0.8; }
/* Two lines reserved: 2.6em at a 1.3 line-height. */
.ctj-picknote { min-height: 2.6em; line-height: 1.3; text-align: center; font-size: 13px; opacity: 0.85; }
/* Call the game sits in the chip rows, so it is exactly a chip's height:
   12px x 1.6, plus 1px of padding and 1px of border on each side (23.2px). */
.ctj-readout .ctj-call { font-size: 12px; line-height: 1.6; padding: 1px 10px; }

/* --- the hand ------------------------------------------------------------------ */
.ctj-mute { opacity: 0.4; }
/* This seat's chips, a caption on the hand's title line: normal weight, one line. */
.ctj-handpanel .ctj-mychips {
    display: inline-flex;
    gap: 4px;
    margin-left: 8px;
    vertical-align: middle;
    font-weight: 400;
    white-space: nowrap;
}
.ctj-handpanel .ctj-mychips:empty { display: none; }
/* Twelve cards at 3 Seats: on a desktop they fit one row, shrinking from the
   site's card size to at most a twelfth of the tray (never under 56px), so the
   hand is one row tall instead of eleven cards and a straggler. The tray is a
   size container for the cqi unit; the site's --cw is captured on the panel. */
.ctj-handpanel { --ctj-handcw: var(--cw); }
@media (min-width: 721px) {
    .ctj-handpanel.ctj-twelve #hand-cards { container-type: inline-size; }
    .ctj-handpanel.ctj-twelve #hand-cards .card {
        --cw: clamp(56px, calc((100cqi - 11 * 8px) / 12), var(--ctj-handcw));
    }
    @supports (width: round(down, 1px, 1px)) {
        .ctj-handpanel.ctj-twelve #hand-cards .card {
            --cw: round(down, clamp(56px, (100cqi - 11 * 8px) / 12, var(--ctj-handcw)), 1px);
        }
    }
}
.ctj-handpanel .ctj-mychips .ctj-chip { font-size: 11px; padding: 0 6px; line-height: 1.5; }

/* --- the sheet ------------------------------------------------------------------- */
/* A real table, so each column is sized once for every row and the columns line
   up (ui-conventions, "Score tables line up by column"). */
.ctj-sheetpanel { margin-top: 10px; }
.ctj-sheet { border-collapse: collapse; margin: 0 auto; font-size: 13px; }
.ctj-sheet th,
.ctj-sheet td { padding: 4px 10px; border-bottom: 1px solid var(--panel-border); }
.ctj-sheet .ctj-rowlabel { text-align: left; white-space: nowrap; font-weight: 600; }
.ctj-sheet .ctj-num,
.ctj-parts .ctj-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ctj-sheet thead .ctj-num { font-size: 12px; }
.ctj-sheet .ctj-score { font-weight: 700; }
.ctj-sheet .ctj-place { font-size: 11px; opacity: 0.75; }
.ctj-sheet .ctj-empty { opacity: 0.5; }
.ctj-sheet .ctj-top .ctj-place { color: var(--accent); opacity: 1; }
.ctj-sheet .ctj-bottom .ctj-place { color: #ff8a80; opacity: 1; }
.ctj-sheet .ctj-totals td,
.ctj-sheet .ctj-totals th { font-weight: 800; font-size: 14px; }
.ctj-parts { border-collapse: collapse; margin: 6px auto; font-size: 13px; }
.ctj-parts th, .ctj-parts td { padding: 2px 10px; text-align: left; }
.ctj-parts .ctj-totals th, .ctj-parts .ctj-totals td { font-weight: 800; border-top: 1px solid var(--panel-border); }
.ctj-rowline { color: var(--accent); }

/* --- phones ------------------------------------------------------------------------ */
/* Every reservation re-derived. The other seats go in one row across the top,
   still in clockwise order, and the middle of the table under them. */
@media (max-width: 720px) {
    .ctj-board { display: flex; flex-direction: column; align-items: center; gap: 8px; }
    .ctj-table {
        display: flex;
        flex-wrap: nowrap;
        justify-content: center;
        gap: 4px;
        width: 100%;
    }
    .ctj-table .ctj-seat { width: auto; flex: 1 1 0; padding: 4px 5px 5px; }
    .ctj-board.ctj-seats-2 .ctj-seat.pos-n { width: 100%; }
    /* A chip is 11px x 1.6 plus its 2px border (19.6px), 4px apart. Two rows
       here; at 6 Seats the five panels are ~71px wide, so Barrel, Side, and a
       role chip stack three rows high. */
    .ctj-seat-chips { flex-wrap: wrap; min-height: calc(2 * (11px * 1.6 + 2px) + 4px); }
    .ctj-board.ctj-seats-6 .ctj-seat-chips { min-height: calc(3 * (11px * 1.6 + 2px) + 2 * 4px); }
    .ctj-trick { --ctj-playcw: 44px; padding: 8px 10px; }
    .ctj-seats-6 .ctj-trick { --ctj-playcw: 36px; }
    .ctj-straws:not(.ctj-mystraws-row) .card { --cw: 34px; }
    .ctj-straws:not(.ctj-mystraws-row) .ctj-straw { min-height: calc(34px * 1.39 + 6px); }
    /* Three chip rows: a long declaration, its companions, and Call the game. */
    .ctj-readout { min-height: calc(3 * (12px * 1.6 + 4px) + 2 * 5px); }
    /* Two lines of the status wrap here (2 x 1.4 line-height); measured 50.4px. */
    .status.ctj-status { min-height: 2.8em; }
    /* Two wrapped .btn rows and the 6px gap between them. */
    .ctj-pickrow { flex-wrap: wrap; min-height: calc(2 * 38.4px + 6px); }
    .ctj-picknote { min-height: 3.9em; }
    .ctj-sheet th, .ctj-sheet td { padding: 3px 6px; }
}
/* ==========================================================================
   Of What's Left
   ==========================================================================
   Two-to-four-player climbing with singles, sets and suited runs, where every
   Seat also holds two Wilds. Shared classes (.card, .panel, .panel-head,
   .card.simple.back, .card.sel, .card.hinted, .card.clickable, .logline,
   .btn, .result-actions, .winner-banner, .result-note) are reused as they are;
   only this game's own rules live here, all prefixed `owl-`.

   WILDS ARE LABELED BACKS. A Wild has no face, so it is the card back with a
   gold edge and a "Wild" tag, and on the table a caption with the rank it
   stands for. The tags are absolutely placed, so the card needs a containing
   block: `.card.owl-wild:not(.card-fly)` gives it one without out-specifying
   the flight ghost's `position: fixed` (the Crew and Casino precedent). A fixed
   ghost is a containing block itself, so the tags ride along in flight.

   NOTHING ABOVE THE HAND MOVES. The seats strip, the pile panel and the three
   action rows (buttons, rank pickers, notes) each reserve their height, so the
   hand never slides under a finger while cards are being picked. */

/* ---------- the seats ----------
   A grid, not a wrapping flex row: every seat sits in one row of equal columns
   (two by two for 4 seats on a phone), and min-width 0 keeps a long name from
   widening its column. Each seat has a head and two fixed rows. */
.owl-seats {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 220px);
    justify-content: center;
    gap: 8px;
    margin-bottom: 10px;
}
.owl-seat {
    min-width: 0;
    padding: 6px 9px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
}
.owl-seat-active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
/* The bonus zone: 5 or fewer cards. Color AND the star mark, words in the tooltip. */
.owl-seat-zone { background: rgba(255, 209, 102, 0.1); }
.owl-seat-passed { opacity: 0.72; }
.owl-seat-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.owl-seat-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 700;
    font-size: 14px;
}
.owl-seat-total {
    font-size: 18px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
.owl-seat-total.leading { color: var(--accent); }
.owl-seat-line {
    display: flex;
    align-items: center;
    gap: 8px;
    height: 18px;
    overflow: hidden;
    white-space: nowrap;
    font-size: 12px;
}
.owl-seat-count { opacity: 0.8; }
.owl-pips { display: inline-flex; gap: 3px; }
.owl-pip {
    width: 8px;
    height: 11px;
    border-radius: 2px;
    border: 1px solid rgba(255, 209, 102, 0.6);
}
.owl-pip.on { background: #d9a520; }
.owl-zonemark { color: var(--accent); font-size: 14px; line-height: 1; }
.owl-passmark {
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.7;
}

/* ---------- the pile ---------- */
.panel.owl-pilepanel { margin: 0; min-height: 168px; text-align: center; }
.owl-pilepanel .panel-head { justify-content: center; }
/* One row, always. A meld too long for the row FANS instead of wrapping: each
   card after the first steps by 6px of gap, or by whatever fits, which is
   (row width - one card) / (n - 1). The margin's 100% is the row's width, and
   --owl-n is the meld's card count, set by meldRow. */
.owl-meld {
    display: flex;
    justify-content: center;
    flex-wrap: nowrap;
    padding: 4px 0;
    min-height: 90px;
}
.owl-meld .card { --cw: 58px; }
.owl-meld .card + .card {
    margin-left: min(6px, calc((100% - var(--owl-n, 1) * var(--cw)) / (var(--owl-n, 1) - 1)));
}
.owl-cleared-row { opacity: 0.6; }
.owl-empty { font-size: 13px; opacity: 0.7; min-height: 90px; padding-top: 34px; }
.owl-pilecap { font-size: 13px; opacity: 0.9; min-height: 1.45em; }

/* ---------- Wilds ---------- */
.card.owl-wild:not(.card-fly) { position: relative; }
.card.owl-wild {
    box-shadow: 0 0 0 2px #d9a520, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.owl-wildtag,
.owl-wildcap {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    white-space: nowrap;
    pointer-events: none;
    font-weight: 800;
    line-height: 1;
    border-radius: 4px;
}
.owl-wildtag {
    top: 12%;
    padding: 2px 4px;
    font-size: 10px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: #2a1d00;
    background: #f2c14e;
}
.owl-wildcap {
    bottom: 12%;
    padding: 2px 5px;
    font-size: 13px;
    color: #17212b;
    background: #f7f3e8;
    border: 1px solid rgba(0, 0, 0, 0.35);
}
/* The suit glyph on a light chip: a dark ink per color family, not the site's
   palette, which is picked to read against the dark table. */
.owl-wildcap .owl-suit { color: #17212b; }
.owl-wildcap .owl-suit.red { color: #b3261e; }
.owl-wildcap .owl-suit.red.diamond { color: #c2410c; }
.owl-wildcap .owl-suit.club { color: #1d4ed8; }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .owl-wildcap .owl-suit.red.diamond { color: var(--fc-diamond-ink); }
.deck-fourcolor .owl-wildcap .owl-suit.club { color: var(--fc-club-ink); }
.card.owl-mini .owl-wildtag { display: none; }
.card.owl-mini .owl-wildcap { font-size: 9px; padding: 1px 2px; bottom: 6%; }

/* ---------- the action rows ---------- */
.owl-actions { margin: 10px 0; }
/* The reservations are a row of buttons (38.4px) with room to spare, and on a
   phone two rows and a gap. */
.owl-controls,
.owl-ranks {
    display: flex;
    justify-content: center;
    gap: 8px;
    flex-wrap: wrap;
    align-items: center;
    align-content: center;
    min-height: 40px;
}
.owl-shape { min-width: 104px; }
.owl-rank { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; }
.owl-rank-label { opacity: 0.8; }
.owl-rank-value {
    display: inline-block;
    min-width: 54px;
    text-align: center;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
.owl-rankstep { min-width: 30px; }
/* Two lines (a note and a hint), three on a phone, at the notes' own 1.45. */
.owl-notes {
    min-height: calc(2 * 1.45em);
    font-size: 13px;
    line-height: 1.45;
    text-align: center;
}
.owl-note { font-size: 13px; opacity: 0.85; }
.owl-note.bad { color: #ff9a8a; opacity: 1; }
.owl-note.owl-hint { color: var(--accent); opacity: 1; font-style: italic; }

/* ---------- the hand ---------- */
/* The score-if-it-ends-now caption: a second, fixed line of the title, so it
   centers with it and never reaches the Sort button at the head's right. */
.owl-handpanel #hand-title { text-align: center; }
.owl-handpanel #hand-title .owl-handscore {
    display: block;
    min-height: 1.35em;
    margin-top: 2px;
    font-size: 13px;
    font-weight: 600;
    line-height: 1.35;
    opacity: 0.85;
    font-variant-numeric: tabular-nums;
}
.owl-handpanel #hand-cards { justify-content: center; }
.owl-handpanel #hand-cards .card { --cw: 58px; }
.owl-handpanel #hand-cards .card.owl-dead { opacity: 0.34; filter: grayscale(0.85); }
.owl-handpanel #hand-cards .card.owl-dead.sel { opacity: 0.75; filter: none; }

/* ---------- played this round ---------- */
.panel.owl-discardpanel { margin-top: 10px; }
.owl-discardrow { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; padding: 4px 0; }
.owl-discardmeld { display: flex; gap: 2px; }
.card.owl-mini { --cw: 30px; }

/* ---------- progress ---------- */
.progress.owl-progress {
    opacity: 1;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 12px;
    min-height: 30px;
}
.owl-threshold {
    font-size: 13px;
    font-weight: 700;
    padding: 2px 8px;
    border-radius: 999px;
    border: 1px solid var(--accent);
    color: var(--accent);
}

/* ---------- summaries ---------- */
table.owl-summary,
table.owl-final {
    border-collapse: collapse;
    margin: 8px auto;
    font-variant-numeric: tabular-nums;
}
table.owl-summary th,
table.owl-summary td,
table.owl-final td { padding: 3px 8px; vertical-align: middle; }
table.owl-summary th { font-size: 12px; opacity: 0.7; font-weight: 600; }
.owl-summary .owl-l,
.owl-final .owl-l { text-align: left; }
.owl-summary .owl-n,
.owl-final .owl-n { text-align: right; }
.owl-summary .owl-total,
.owl-final .owl-total { font-weight: 800; }
.owl-summary .owl-left { display: flex; gap: 2px; flex-wrap: wrap; max-width: 280px; }
.owl-summary tr.owl-shedder td { color: var(--accent); }
.owl-final tr.owl-winner td { background: rgba(255, 209, 102, 0.14); }
.owl-summary-title { text-align: center; margin: 10px 0 0; font-size: 14px; }

/* ---------- the log ---------- */
.logline.owl-logout { color: var(--accent); font-weight: 700; }
.logline.owl-logquiet { opacity: 0.8; }
/* Suits matter in a run, so a run line colors its glyph (per suit, the site's
   palette, which reads against the dark table). */
#log-entries .owl-suit.red,
.owl-pilecap .owl-suit.red { color: var(--red); }
#log-entries .owl-suit.red.diamond,
.owl-pilecap .owl-suit.red.diamond { color: var(--red-diamond); }
#log-entries .owl-suit.club,
.owl-pilecap .owl-suit.club { color: var(--blue-club); }
/* A finished round's picture (def.ts roundPicture) draws a Wild as a back, the
   only back it ever holds, so the back takes a Wild's gold ring here. */
.owl-logpic .card.logpic-card.back { box-shadow: 0 0 0 2px #d9a520; }

/* ---------- rules lists ---------- */
.owl-rules ol,
.owl-rules ul,
ol.owl-list,
ul.owl-list { margin: 6px 0 0 18px; padding: 0; }
.owl-rules li,
ol.owl-list li,
ul.owl-list li { margin-bottom: 5px; }

/* ---------- the status line ----------
   Scoped through the class render() puts on it: #status-line is on every page.
   A prompt naming a run can take two lines below desktop widths, so two are
   reserved there and the board under it never moves. */
#status-line.owl-status { line-height: 1.35; }
@media (max-width: 900px) {
    #status-line.owl-status { min-height: calc(2 * 1.35em); }
}

/* ---------- narrow screens ---------- */
@media (max-width: 620px) {
    .owl-seats { grid-auto-columns: minmax(0, 1fr); }
    .owl-seats.owl-n4 { grid-auto-flow: row; grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .owl-meld .card { --cw: 46px; }
    .owl-handpanel #hand-cards .card { --cw: 46px; }
    .owl-shape { min-width: 92px; }
    .owl-controls,
    .owl-ranks { min-height: 86px; }
    .owl-notes { min-height: calc(3 * 1.45em); }
}
/* ------------------------------------------------------------------------- */
/* Garbo                                                                     */
/* ------------------------------------------------------------------------- */
/*
 * Prefix policy: every Garbo-specific class is `gbo-`. Shared classes are
 * reused as they are: hand cards select with `.card.sel`, arm with
 * `.clickable`, flash with `.card.hinted`; backs are `.card.simple.back`; the
 * outcome uses `.winner-banner`, `.result-note` and `.result-actions`.
 *
 * THE BOARD HOLDS ITS SIZE ALL GAME. Level One draws a fixed 5x5 window (see
 * src/garbo/view.ts) and Level Two the 4x4 square, centered on the same mat,
 * so the hand below never moves. Squares are as wide as a card is TALL, so a
 * covered card's sideways back fits under the card on top of it, the way the
 * sheet has players turn it.
 *
 * ONE LOOK PER MEANING on the board, and nothing dashed:
 *   an open square        a card-shaped recess in the mat
 *   ... with a card picked the recess warms, and carries the cost coin
 *   the card played last  a blue ring on its square
 *   a card it matched     an orange ring and a tag saying what it paid
 *   a card it WOULD match a gold ring and tag, while hovering a square
 */
.gbo-board {
    --gbo-cell: min(84px, calc((100vw - 64px) / 5));
    --gbo-gap: 4px;
    --gbo-pad: 12px;
    --gbo-cw: calc((var(--gbo-cell) - 2px) * 64 / 89);
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    width: calc(5 * var(--gbo-cell) + 4 * var(--gbo-gap) + 2 * var(--gbo-pad));
    height: calc(5 * var(--gbo-cell) + 4 * var(--gbo-gap) + 2 * var(--gbo-pad));
    margin: 6px auto 10px;
    border-radius: 18px;
    background:
        radial-gradient(120% 90% at 50% 35%, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0) 60%),
        linear-gradient(180deg, #154528, #0f3a21);
    border: 1px solid rgba(255, 255, 255, 0.08);
    box-shadow:
        inset 0 2px 10px rgba(0, 0, 0, 0.45),
        inset 0 0 0 4px rgba(0, 0, 0, 0.08),
        0 1px 0 rgba(255, 255, 255, 0.05);
}
.gbo-grid {
    display: grid;
    gap: var(--gbo-gap);
    grid-auto-rows: var(--gbo-cell);
    justify-content: center;
    transition: opacity 160ms ease;
}
/* While the ledger is open, the board steps back behind it. */
.gbo-grid.gbo-dim { opacity: 0.22; }
/* Cards fill a square's height, less a hair. */
.gbo-grid .card { --cw: var(--gbo-cw); }
.gbo-cell {
    position: relative;
    width: var(--gbo-cell);
    height: var(--gbo-cell);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 9px;
}
.gbo-grid .gbo-cell > .card { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35), 0 6px 14px rgba(0, 0, 0, 0.25); }
/* A square nothing can reach: kept for the window's shape, drawn as nothing. */
.gbo-cell.void { visibility: hidden; }
/* An open square: a card-shaped recess where the next card would lie. */
.gbo-cell.empty.legal::before {
    content: '';
    width: var(--gbo-cw);
    height: calc(var(--gbo-cw) * 89 / 64);
    border-radius: 7px;
    background: rgba(0, 0, 0, 0.2);
    box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.1);
    transition: background 120ms ease, box-shadow 120ms ease;
}
.gbo-cell.empty.legal.armed::before {
    background: rgba(255, 209, 102, 0.1);
    box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 209, 102, 0.55);
}
.gbo-cell.legal.armed { cursor: pointer; outline: none; }
.gbo-cell.empty.legal.armed:hover::before,
.gbo-cell.empty.legal.armed:focus-visible::before {
    background: rgba(255, 209, 102, 0.2);
    box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.3), 0 0 0 2px var(--accent);
}
/* Level Two: a coverable card lifts a little under the pointer. */
.gbo-cell.legal.armed:not(.empty) > .card:not(.gbo-under) { transition: transform 120ms ease; }
.gbo-cell.legal.armed:not(.empty):hover > .card:not(.gbo-under),
.gbo-cell.legal.armed:not(.empty):focus-visible > .card:not(.gbo-under) {
    transform: translateY(-3px);
    box-shadow: 0 0 0 2px var(--accent), 0 8px 16px rgba(0, 0, 0, 0.4);
}
/* The card on top of a stack paints above the sideways back under it. */
.gbo-cell > .card:not(.gbo-under) { position: relative; z-index: 1; }
.gbo-cell > .gbo-under {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 0;
    /* `translate` and `rotate`, never `transform` (ui-conventions, "A
       sideways card turns with rotate"). */
    translate: -50% -50%;
    rotate: 90deg;
    filter: brightness(0.8);
}
/* --- numbers on the board ------------------------------------------------ */
/* What the picked card would give the opponent on this square: a coin. */
.gbo-cost, .gbo-coin {
    position: absolute;
    z-index: 3;
    box-sizing: border-box;
    min-width: 26px;
    height: 22px;
    padding: 0 6px;
    border-radius: 11px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    pointer-events: none;
    color: #fff4ef;
    background: linear-gradient(180deg, #c4553a, #8f2f1e);
    border: 1px solid rgba(255, 210, 190, 0.55);
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.45);
    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.35);
}
.gbo-cost.free, .gbo-coin.free {
    color: #effff2;
    background: linear-gradient(180deg, #3f9f61, #24693d);
    border-color: rgba(190, 255, 205, 0.55);
}
/* On an empty square the coin sits in the middle of the recess, larger. On a
   card (Level Two) it sits on the bottom edge, clear of the flat art's rank at
   the top and its suit in the middle. */
.gbo-cell.empty .gbo-cost {
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    height: 26px;
    min-width: 32px;
    border-radius: 13px;
    font-size: 15px;
}
/* On a card (Level Two) the coin sits in the square's bottom-right corner,
   overhanging into the gap, so it never covers the suit in the middle. */
.gbo-cell:not(.empty) .gbo-cost { right: -3px; bottom: -3px; }
/*
 * What a matched card paid, and why: "+12 Q" for a rank match, "+4 ♥" for a
 * suit match. Each tag sits over the half of the card it NAMES, a rank tag
 * over the rank at the top and a suit tag over the suit at the bottom, so the
 * tag replaces exactly what it hides and the card still reads in full.
 */
.gbo-tag {
    position: absolute;
    left: 50%;
    top: -4px;
    z-index: 4;
    transform: translateX(-50%);
    display: inline-flex;
    align-items: baseline;
    gap: 3px;
    padding: 1px 6px;
    border-radius: 9px;
    font-size: 12px;
    font-weight: 800;
    line-height: 1.35;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    color: #ffe3d6;
    background: rgba(28, 16, 10, 0.94);
    border: 1px solid #f08a5d;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
    pointer-events: none;
}
.gbo-tag.suit { top: auto; bottom: 6%; }
.gbo-tag.would { color: #fff1c7; border-color: var(--accent); }
.gbo-tag.cover { border-color: #ff6b5a; }
.gbo-tagwhy { font-weight: 700; opacity: 0.9; }
/* A rank reason is drawn as a tiny card index, so "+5" next to a 5 reads as
   "5 points, for the 5" and not as a stray number. */
.gbo-tag.rank .gbo-tagwhy {
    opacity: 1;
    padding: 0 3px;
    border-radius: 3px;
    color: #1d2230;
    background: #f4efe6;
    font-size: 0.9em;
    line-height: 1.25;
}
.gbo-tagwhy.red { color: var(--red); }
.gbo-tagwhy.red.diamond { color: var(--red-diamond); }
.gbo-tagwhy.club { color: var(--blue-club); }
/* The card played last, until the next one: cool blue on the SQUARE (the
   duel-poker / Sono hue), never on the card, which rides the flight ghost. */
.gbo-cell.gbo-last { box-shadow: 0 0 0 3px #6aa9e0, 0 0 12px rgba(106, 169, 224, 0.6); }
.gbo-cell.gbo-hit { box-shadow: 0 0 0 3px #f08a5d; }
.gbo-cell.gbo-would { box-shadow: 0 0 0 3px var(--accent); }
/* While a square is previewed, the cards it would match show what they would
   be paid, not their own cost as a destination: one number per card. */
.gbo-cell.gbo-would .gbo-cost { visibility: hidden; }
.gbo-cell.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    box-shadow: 0 0 0 3px var(--accent);
}
/* --- the ledger behind a scoreboard chip ---------------------------------- */
/* It OVERLAYS the board instead of inserting a row, so opening it moves
   nothing: opaque, capped to the mat, with its own scroll. */
.gbo-detail {
    position: absolute;
    top: 6px;
    left: 50%;
    z-index: 20;
    transform: translateX(-50%);
    box-sizing: border-box;
    width: min(440px, calc(100vw - 24px));
    max-height: calc(100% - 12px);
    overflow-y: auto;
    margin: 0;
    background: var(--felt-dark);
    border-color: var(--accent);
    box-shadow: 0 10px 28px rgba(0, 0, 0, 0.55);
    text-align: left;
}
.gbo-detailhead { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.gbo-detailhead h3 { margin: 0; font-size: 15px; }
.gbo-detailnote { margin: 4px 0 6px; font-size: 13px; opacity: 0.8; }
.gbo-ledger { width: 100%; border-collapse: collapse; margin: 6px 0 4px; font-size: 13px; }
.gbo-ledger caption {
    text-align: left;
    font-size: 12px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    opacity: 0.65;
    padding: 4px 0 2px;
}
.gbo-ledger td { padding: 4px 4px; vertical-align: middle; border-top: 1px solid rgba(255, 255, 255, 0.07); }
.gbo-ledger th {
    padding: 0 4px 2px;
    text-align: left;
    font-size: 11px;
    font-weight: 600;
    opacity: 0.55;
}
.gbo-ledger th.num { text-align: right; }
.gbo-ledger td.num {
    text-align: right;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    width: 3em;
}
.gbo-ledger tr.total td { font-weight: 700; border-top: 1px solid var(--panel-border); }
.gbo-ledplayed { white-space: nowrap; width: 1%; }
.gbo-ledplayed .card, .gbo-leditem .card { --cw: 26px; display: inline-block; vertical-align: middle; }
.gbo-ledon { margin: 0 4px; font-size: 12px; opacity: 0.7; }
.gbo-ledwhy { display: flex; flex-wrap: wrap; gap: 6px 10px; }
.gbo-leditem { display: inline-flex; align-items: center; gap: 4px; }
.gbo-leditem .gbo-tag { position: static; transform: none; }
/* --- the other hand ------------------------------------------------------- */
/* A FIXED height, not a minimum: backs and faces differ by a few pixels, and
   the panel sits above the board. */
.gbo-opp {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    box-sizing: border-box;
    height: 58px;
    padding: 4px 12px;
    margin: 0 auto 4px;
    width: fit-content;
    max-width: 100%;
}
.gbo-opp .gbo-opp-name { margin: 0; font-size: 14px; white-space: nowrap; }
.gbo-opp-cards { display: flex; gap: 3px; justify-content: center; }
.gbo-opp .card { --cw: 34px; }
/* --- the hand: one group per suit (or rank), a little apart -------------- */
.gbo-handpanel #hand-cards { column-gap: 16px; row-gap: 8px; }
.gbo-suit { display: flex; gap: 6px; flex-wrap: nowrap; }
/* A card and, as its SIBLING, the coin with the least it can give away now.
   The room below is what the coin overhangs, so the next row never meets it. */
.gbo-slot { position: relative; padding-bottom: 12px; }
.gbo-coin { left: 50%; bottom: 0; transform: translateX(-50%); }
/* What every card is worth, always in view under the hand. */
.gbo-legend {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 4px 12px;
    margin-top: 8px;
    padding-top: 7px;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    font-size: 13px;
    color: rgba(255, 255, 255, 0.8);
}
.gbo-legsuits { display: inline-flex; gap: 10px; }
.gbo-legitem { display: inline-flex; align-items: baseline; gap: 2px; font-weight: 700; font-variant-numeric: tabular-nums; }
.gbo-legitem > span { font-size: 15px; }
.gbo-legend .red { color: var(--red); }
.gbo-legend .red.diamond { color: var(--red-diamond); }
.gbo-legend .club { color: var(--blue-club); }
.gbo-legsep { width: 4px; height: 4px; border-radius: 50%; background: rgba(255, 255, 255, 0.3); }
.gbo-legranks, .gbo-legcover { white-space: nowrap; }
/* --- status and scoreboard ---------------------------------------------- */
.status.gbo-status { line-height: 1.35; min-height: calc(2 * 1.35em); }
.gbo-scoreboard .gbo-scorebtn { font: inherit; color: inherit; cursor: pointer; }
.gbo-scoreboard .gbo-scorebtn:hover { border-color: rgba(255, 255, 255, 0.5); }
.gbo-scoreboard .gbo-scorebtn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.gbo-scoreboard .gbo-scorebtn.open { border-color: var(--accent); background: rgba(255, 209, 102, 0.14); }
.gbo-scoreboard .gbo-scorebtn:disabled { cursor: default; }
.progress-score.gbo-turn .gbo-scorename::before { content: '\25B6\00a0'; color: var(--accent); }
.gbo-error { margin-top: 4px; font-size: 13px; color: #ff9c8a; }
/* --- the result ---------------------------------------------------------- */
.gbo-result {
    border-collapse: collapse;
    margin: 6px auto 4px;
    font-size: 14px;
}
.gbo-result th, .gbo-result td { padding: 3px 10px; text-align: left; }
.gbo-result th.num, .gbo-result td.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
    min-width: 3.5em;
}
.gbo-result tr.total td { border-top: 1px solid var(--panel-border); font-weight: 800; }
@media (max-width: 560px) {
    .gbo-board { --gbo-gap: 3px; --gbo-pad: 7px; border-radius: 14px; }
    .gbo-cost, .gbo-coin { min-width: 22px; height: 19px; padding: 0 4px; font-size: 11px; border-radius: 10px; }
    .gbo-cell.empty .gbo-cost { height: 22px; min-width: 26px; font-size: 13px; border-radius: 11px; }
    .gbo-tag { font-size: 10px; padding: 0 4px; top: -5px; }
    .gbo-opp { height: 46px; }
    .gbo-opp .card { --cw: 26px; }
    /* Level Two's eight cards on ONE line at 390px: the cards size to the
       tray (eight cards, four group gaps and the card gaps inside them), down
       to a 36px floor below which the row wraps. Scoped to this game's panel
       (an unscoped container on the shared id leaks into every game). */
    .gbo-handpanel #hand-cards { column-gap: 8px; container-type: inline-size; }
    .gbo-handpanel #hand-cards .card { --cw: clamp(36px, calc((100cqw - 40px) / 8), 56px); }
    .gbo-suit { gap: 3px; }
    .gbo-slot { padding-bottom: 10px; }
    /* Two lines on a phone: the suits, then the ranks and the cover rule. */
    .gbo-legend { font-size: 12px; gap: 3px 9px; }
    .gbo-legsuits { flex-basis: 100%; justify-content: center; }
    .gbo-legsuits + .gbo-legsep { display: none; }
    .status.gbo-status { font-size: 16px; line-height: 1.3; min-height: calc(3 * 1.3em); }
}
body.motion-off .gbo-cell.hinted { animation: none; }
/* ==== Rainbow (rainbow/) - only what is new lives here; panels, buttons, and
        cards are the shared classes. Every rule that could reach a shared id
        goes through .rbw-board, .rbw-handpanel, or .rbw-status. ==== */

/* isolation keeps the board's stacking contexts under the phone-docked
   tutorial coach panel. */
.rbw-board {
    display: flex;
    flex-direction: column;
    gap: 12px;
    isolation: isolate;
    /* The middle's piles: above the 46px line, so full art (ui-conventions, small cards). */
    --rbw-pilecw: 52px;
    /* The rows' read-only cards: 30px, simplified art. */
    --rbw-rowcw: 30px;
    --rbw-rowh: calc(var(--rbw-rowcw) * 89 / 64);
}

/* ---------- the middle ---------- */
/* One row, always, centered: not .cardrow, which wraps on phones. Its height
   is one card plus the sum line (2px gap, 14px line), reserved, so a fresh
   layout, a taken pile, or an empty middle moves nothing. */
.rbw-board .rbw-middle {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    flex-wrap: nowrap;
    gap: 10px;
    min-height: calc(var(--rbw-pilecw) * 89 / 64 + 16px);
}
/* Read-only: every claim is the engine's (engine.ts RULING 6), so no pile is a
   control. A pile is a column: its cards, then its sum, centered under them. */
.rbw-pile { display: flex; flex: none; flex-direction: column; align-items: center; gap: 2px; }
.rbw-pilecards { display: flex; align-items: flex-start; }
.rbw-board .rbw-pile .card { --cw: var(--rbw-pilecw); flex: none; }
/* A pair is two cards fanned, the second over the first. */
.rbw-board .rbw-pilecards .card + .card { margin-left: calc(var(--rbw-pilecw) * -0.6); }
/* The pile's sum: a small, quiet number on a fixed line. A taken pile keeps an
   empty line of the same height. */
.rbw-pilesum {
    display: block;
    height: 14px;
    line-height: 14px;
    font-size: 11px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    opacity: 0.6;
}
/* A taken pile leaves its outline, so the piles still to claim stay put. */
.rbw-slot {
    width: var(--rbw-pilecw);
    height: calc(var(--rbw-pilecw) * 89 / 64);
    border: 2px dashed rgba(255, 255, 255, 0.22);
    border-radius: 6px;
    box-sizing: border-box;
}
.rbw-pile.rbw-pair .rbw-slot { width: calc(var(--rbw-pilecw) * 1.4); }

/* ---------- the trick head: which combos are allowed ---------- */
.rbw-tablehead { justify-content: center; gap: 12px; }
.rbw-types { display: inline-flex; gap: 6px; white-space: nowrap; }
.rbw-type {
    font-size: 12px;
    padding: 1px 8px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.1);
    font-weight: 600;
}
.rbw-type.rbw-off { opacity: 0.4; text-decoration: line-through; }

/* ---------- the rows: ONE grid with fixed tracks ---------- */
/* Rows are display: contents, so every row's cells share the grid's tracks
   and the columns line up down the table (ui-conventions, score tables). The
   tracks: seat, combo, claim badge, claimed pile. At 2 players each seat has
   two combo slots, so the last three tracks repeat (in a wide table; a narrow
   one stacks them, see the end of this file). */
.rbw-rows {
    display: grid;
    grid-template-columns: 9em minmax(0, 1fr) 3em 5em;
    column-gap: 8px;
    row-gap: 4px;
    align-items: center;
}
.rbw-rows.rbw-two { grid-template-columns: 8em minmax(0, 1fr) 2.6em 4.4em minmax(0, 1fr) 2.6em 4.4em; }
.rbw-row { display: contents; }
.rbw-row > * {
    min-height: calc(var(--rbw-rowh) + 6px);
    display: flex;
    align-items: center;
    min-width: 0;
    padding: 2px 4px;
    border-radius: 6px;
}
/* The turn tint goes on the seat cell only. The row is display: contents, so a
   `> *` tint painted every cell of the acting row separately, and its empty
   combo, badge, and claimed cells showed as up to six empty gold boxes
   (measured, 2026-10-01 playtest). Those cells stay like every other seat's
   empty cells. A background takes no layout, so nothing moves. */
.rbw-row.rbw-turn > .rbw-seat { background: rgba(255, 209, 102, 0.12); }
.rbw-row.rbw-me > .rbw-seat,
.rbw-row.rbw-viewed > .rbw-seat { box-shadow: inset 0 0 0 2px rgba(255, 209, 102, 0.4); }
.rbw-row.rbw-isout > * { opacity: 0.55; }
.rbw-row > .rbw-seat { flex-direction: column; align-items: flex-start; justify-content: center; gap: 1px; }
.rbw-seatname {
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}
.rbw-seatmeta {
    display: flex;
    gap: 8px;
    align-items: center;
    font-size: 12px;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.rbw-score { font-weight: 800; font-size: 14px; }
.rbw-count { opacity: 0.75; }
.rbw-reserve { display: inline-flex; align-items: center; gap: 3px; }
.rbw-board .rbw-reserve .card { --cw: 14px; flex: none; }
.rbw-reserve.rbw-used { opacity: 0.5; text-decoration: line-through; }
.rbw-outtag {
    font-size: 11px;
    padding: 0 6px;
    border-radius: 8px;
    background: rgba(255, 120, 120, 0.25);
}
/* A combo is one row of overlapping small cards: never wraps, so a 10-card
   set fits and no row grows. */
.rbw-combo { gap: 3px; overflow: hidden; }
/* The strip keeps its width (flex: none) and the Led tag sits on its right. */
.rbw-strip { display: flex; flex: none; flex-wrap: nowrap; align-items: center; }
.rbw-board .rbw-strip .card,
.rbw-board .rbw-claimed .card { --cw: var(--rbw-rowcw); flex: none; }
.rbw-board .rbw-strip .card + .card { margin-left: calc(var(--rbw-rowcw) * -0.45); }
/* A combo of 6 or more cards (def.ts LONG_COMBO) overlaps tighter, so a
   10-card set and the Led tag fit the 124px cell of a 390px phone. */
.rbw-board .rbw-strip.rbw-long .card + .card { margin-left: calc(var(--rbw-rowcw) * -0.72); }
.rbw-board .rbw-claimed .card + .card { margin-left: calc(var(--rbw-rowcw) * -0.55); }
.rbw-led {
    flex: none;
    font-size: 11px;
    padding: 0 4px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.12);
    white-space: nowrap;
}
.rbw-order {
    justify-content: flex-end;
    font-weight: 800;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.rbw-claimed { gap: 0; }
.rbw-empty { font-size: 12px; opacity: 0.55; font-style: italic; white-space: nowrap; }

/* ---------- controls and lines ---------- */
.rbw-actions {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 44px;
}
.rbw-actions .btn { white-space: nowrap; }
/* The shared reservation (handspace.ts) keeps #hand-cards at its tallest, and
   a wrapping flex row STRETCHES its lines into that height, so on a phone the
   second row of cards dropped 34px the moment a 14-card hand fell to two rows
   (measured). Packing the lines at the top keeps every card row where it was. */
.rbw-handpanel #hand-cards { align-content: flex-start; }
.status.rbw-status { line-height: 1.35; min-height: 1.35em; }
.progress.rbw-progress { white-space: nowrap; }
/* Nothing the shell adds at the end of a game shows until the last trick's
   replay has finished (the summary waits for it too). */
body:has(.rbw-board.rbw-replaying) #endgame-line { display: none; }

/* ---------- the result ---------- */
/* A real table with the game's own class: it sizes each column once, for
   every row. The table centers; the cells keep their own alignment. */
.rbw-result { margin: 0 auto; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.rbw-result th, .rbw-result td { padding: 4px 10px; white-space: nowrap; }
.rbw-result th { font-size: 13px; opacity: 0.75; font-weight: 600; }
.rbw-result .rbw-left { text-align: left; }
.rbw-result .rbw-num { text-align: right; }
.rbw-result .rbw-total { font-weight: 800; }
.rbw-result tr.rbw-resultwin td { color: var(--accent); }

/* ---------- phones ---------- */
@media (max-width: 720px) {
    .rbw-board {
        gap: 10px;
        --rbw-pilecw: 44px;
        --rbw-rowcw: 24px;
    }
    .rbw-board.rbw-n5 { --rbw-pilecw: 40px; }
    .rbw-board.rbw-n6 { --rbw-pilecw: 36px; }
    .rbw-board .rbw-middle { gap: 6px; }
    .rbw-board .rbw-pilecards .card + .card { margin-left: calc(var(--rbw-pilecw) * -0.7); }
    .rbw-pile.rbw-pair .rbw-slot { width: calc(var(--rbw-pilecw) * 1.3); }
    .rbw-rows { grid-template-columns: 6.5em minmax(0, 1fr) 2.4em 3.6em; column-gap: 4px; }
    .rbw-rows.rbw-two { grid-template-columns: 5.5em minmax(0, 1fr) 2.2em 3em minmax(0, 1fr) 2.2em 3em; }
    .rbw-seatname { font-size: 13px; }
    .rbw-seatmeta { gap: 5px; font-size: 11px; }
    .rbw-score { font-size: 13px; }
    /* Two lines, whatever the phase asks: a one-line status would move the
       whole board by a line when the next one wraps. */
    .status.rbw-status { min-height: 2.7em; }
    .rbw-result th, .rbw-result td { padding: 3px 6px; font-size: 13px; }
}

/* ---------- 2 players in a narrow table: stack the two combo slots ---------- */
/* Seven tracks do not fit a narrow table: at 390px each combo cell came out
   29px wide, so the led combo showed no card at all and a pair showed one
   (measured). Under 700px of table the seat cell spans two grid lines and each
   seat's second slot flows onto the second, in the same three tracks as the
   first, so every combo gets the table's spare width. 700, not 620: at a
   1024px window the table is about 640px, and seven tracks left 121px a combo
   cell, which clipped a 5-card combo and its Led tag (measured 2026-10-01);
   from 700px the side-by-side cells are at least 150px, which holds a
   10-card set and the tag. The query reads the table panel itself, which is
   exact whatever the page layout around it; the panel is this game's own
   class, and the shell's flying cards live on <body>, so the containment
   reaches nothing else. */
.rbw-table { container: rbwtable / inline-size; }
@container rbwtable (max-width: 700px) {
    .rbw-rows.rbw-two { grid-template-columns: 6.5em minmax(0, 1fr) 2.4em 3.6em; column-gap: 4px; }
    .rbw-rows.rbw-two .rbw-row > .rbw-seat { grid-row: span 2; }
}
/* ==========================================================================
   Ohio (ohi-)
   Top to bottom: the other seats' open hands, the trick with its running
   value, the Pass button, the hint note, your hand, then the shared summary
   panel. Every box above the hand reserves its tallest ordinary shape, in
   card-width terms, so nothing walks the hand up or down the screen as a deal
   runs (ui-conventions, "Containers must not move"). The shared ids
   (#hand-panel and its children, #status-line, #progress-line) are reached only
   through a class this game puts on them.
   ========================================================================== */
.status.ohi-status { min-height: 26px; }
/* --- the scoreboard ------------------------------------------------------- */
.progress.ohi-scoreboard {
    opacity: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 6px 10px;
    min-height: 34px;
}
.ohi-round {
    font-size: 13px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.6;
    white-space: nowrap;
}
.ohi-score {
    display: inline-flex;
    align-items: baseline;
    gap: 5px;
    padding: 3px 12px;
    border-radius: 999px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    white-space: nowrap;
}
.ohi-score.ohi-mine { border-color: rgba(255, 255, 255, 0.35); }
.ohi-score.ohi-leading { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.ohi-scorename { font-size: 13px; opacity: 0.75; }
.ohi-scorenum { font-size: 16px; font-weight: 800; font-variant-numeric: tabular-nums; }
/* --- suit glyphs and the second-pack dot ---------------------------------- */
/* A seat plays one whole suit, so its glyph is WHO a card belongs to. */
.ohi-suit { font-size: 15px; font-weight: 700; line-height: 1; }
.ohi-suit.red { color: var(--red); }
.ohi-suit.red.diamond { color: var(--red-diamond); }
.ohi-suit.club { color: var(--blue-club); }
/* A filled dot, drawn in CSS: a shape cannot be read as a rank, and a glyph can
   render as a blank box. The same dot sits on that seat's cards in the trick. */
.ohi-deck2mark {
    display: inline-block;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: #2f9ad6;
    border: 1.5px solid #1a1a1a;
    flex: none;
    align-self: center;
}
/* --- the other seats ------------------------------------------------------ */
/* Two seats a row, and a row with one seat (the third of three opponents, or a
   lone opponent at two players) sits in the middle rather than on the left.
   Two-up only where a full hand fits in half a row: eleven 30px cards with ten
   2px gaps, 16px of padding and the border are 368px, so the basis is 370px and
   a narrower block (600 to 770px windows, or 1024px beside the log) puts one
   seat a row. A container query on this game's own block: nothing inside it is
   position: fixed (card-flight ghosts go on <body>). */
.ohi-seats {
    container: ohiseats / inline-size;
    --ohi-oppcw: 30px;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px;
    margin-bottom: 8px;
}
.ohi-seat {
    flex: 1 1 370px;
    max-width: calc(50% - 3px);
    box-sizing: border-box;
    padding: 5px 8px;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.18);
    border: 1px solid transparent;
    min-width: 0;
}
@container ohiseats (max-width: 745.98px) {
    .ohi-seat { flex-basis: 100%; max-width: none; }
}
.ohi-seat.ohi-seat-active { border-color: var(--accent); }
.ohi-seat.ohi-ontop { box-shadow: inset 0 0 0 2px rgba(120, 220, 160, 0.55); }
.ohi-seat-head {
    display: flex;
    align-items: baseline;
    gap: 6px;
    min-height: 20px;
    margin-bottom: 3px;
    white-space: nowrap;
    overflow: hidden;
}
.ohi-seat-name { font-size: 13px; font-weight: 600; }
.ohi-taken,
.ohi-handvalue {
    font-size: 12px;
    opacity: 0.75;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.ohi-seat-head .ohi-taken { margin-left: auto; }
.ohi-passtag,
.ohi-toptag {
    font-size: 11px;
    padding: 0 6px;
    border-radius: 999px;
    white-space: nowrap;
}
.ohi-passtag { background: rgba(255, 255, 255, 0.12); opacity: 0.8; }
.ohi-toptag { background: rgba(120, 220, 160, 0.22); }
/* One row that scrolls inside itself rather than wrapping, one row reserved.
   Centered by auto margins on the end cards, not justify-content: a strip
   that can scroll must not clip its left overflow (ui-conventions), and an
   auto margin falls to 0 once the row is full. */
.ohi-seat-cards {
    display: flex;
    align-items: center;
    gap: 2px;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: thin;
    min-height: calc(var(--ohi-oppcw) * 89 / 64);
}
.ohi-seat-cards > :first-child { margin-left: auto; }
.ohi-seat-cards > :last-child { margin-right: auto; }
.ohi-seat-cards .card { --cw: var(--ohi-oppcw); flex: none; position: relative; }
/* --- the trick ------------------------------------------------------------- */
.panel.ohi-trickpanel { --ohi-trickcw: 46px; padding: 8px 12px; }
.ohi-trick-head {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 10px;
    min-height: 32px;
    white-space: nowrap;
}
.ohi-trick-label { font-size: 14px; opacity: 0.75; }
.ohi-value {
    font-size: 26px;
    font-weight: 800;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    min-width: 2.2em;
    text-align: left;
}
/* The middle of the table: centered by auto margins on the end cards (see
   .ohi-seat-cards), so a long trick still scrolls from its first card. One
   row reserved, the newest card scrolled into view by the renderer. */
.ohi-trick-cards {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: thin;
    padding: 4px;
    min-height: calc(var(--ohi-trickcw) * 89 / 64 + 8px);
    box-sizing: border-box;
}
.ohi-trick-cards > :first-child { margin-left: auto; }
.ohi-trick-cards > :last-child { margin-right: auto; }
.ohi-trick-cards .card { --cw: var(--ohi-trickcw); flex: none; position: relative; }
/* Overlapped by about 30%, so fifteen cards fit at 1280px. */
.ohi-trick-cards .card + .card { margin-left: calc(var(--ohi-trickcw) * -0.3); }
.ohi-trick-cards .card.ohi-won { outline: 3px solid var(--accent); outline-offset: 1px; z-index: 1; }
/* Bottom LEFT: the next card overlaps the right 30% of this one, so a dot on
   the right would be hidden on every card but the newest. */
.ohi-trick-cards .card.ohi-deck2::after {
    content: '';
    position: absolute;
    bottom: 3px;
    left: 3px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: #2f9ad6;
    border: 1.5px solid #1a1a1a;
    pointer-events: none;
}
/* What the last trick's points do. Its line is always there, empty mid-deal,
   so the note appearing never moves the hand (one line here, two on phones). */
.ohi-trick-note {
    min-height: 18px;
    font-size: 13px;
    line-height: 18px;
    text-align: center;
    color: var(--accent);
}
/* --- Pass, and the hint note under it ------------------------------------- */
.ohi-actions {
    display: flex;
    justify-content: center;
    gap: 8px;
    min-height: 40px;
    margin: 8px 0 2px;
}
.ohi-notes { min-height: 20px; text-align: center; }
.ohi-note { font-size: 13px; white-space: nowrap; }
.ohi-note.ohi-hint { color: var(--accent); }
/* --- your hand ------------------------------------------------------------ */
.ohi-handtitle {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    white-space: nowrap;
}
.ohi-handpanel #hand-cards .card { position: relative; }
/* A card that cannot go on this trick, yours or another seat's, fades the same
   way; rank and suit stay readable. */
.ohi-handpanel #hand-cards .card.ohi-dead,
.ohi-seat-cards .card.ohi-dead { opacity: 0.45; filter: saturate(0.55); }
/* --- the Ohio's badge ------------------------------------------------------ */
/* The art shows a K, and the thing to know about the card is its value. */
.ohi-badge {
    position: absolute;
    right: 2px;
    bottom: 2px;
    padding: 0 3px;
    min-width: 18px;
    height: 15px;
    border-radius: 8px;
    background: #1a1a1a;
    border: 1.5px solid #f6efe4;
    font-size: 10px;
    font-weight: 800;
    line-height: 12px;
    text-align: center;
    color: #f6efe4;
    pointer-events: none;
    box-sizing: border-box;
}
.ohi-seat-cards .ohi-badge,
.ohi-trick-cards .ohi-badge {
    right: 1px;
    bottom: 1px;
    min-width: 0;
    height: 11px;
    padding: 0 2px;
    border-width: 1px;
    font-size: 8px;
    line-height: 9px;
}
/* --- the deal summary ------------------------------------------------------ */
.ohi-result-title { margin: 6px 0 2px; font-size: 14px; text-align: center; }
.result-note.ohi-lastline { margin: 6px auto 4px; max-width: 34em; opacity: 0.9; }
.ohi-result {
    margin: 4px auto 0;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
}
.ohi-result th,
.ohi-result td { padding: 3px 12px; text-align: right; white-space: nowrap; }
.ohi-result th.ohi-rname,
.ohi-result td.ohi-rname { text-align: left; }
.ohi-result th { font-size: 13px; opacity: 0.8; border-bottom: 1px solid var(--panel-border); }
.ohi-result .ohi-rtotal { font-weight: 800; }
.ohi-result tr.ohi-rbest td.ohi-rdeal { color: var(--accent); }
/* --- the log --------------------------------------------------------------- */
#log-entries .logline.ohi-logquiet { opacity: 0.7; }
/* --- phones ---------------------------------------------------------------- */
@media (max-width: 560px) {
    /* The longest status line ("Player 3's turn. Play a card lower than the 10
       or the Ohio, or pass.") takes three lines at 390px. */
    .status.ohi-status { font-size: 16px; min-height: 66px; }
    .progress.ohi-scoreboard { gap: 4px 6px; min-height: 56px; }
    .ohi-score { padding: 2px 8px; }
    .ohi-scorename { font-size: 12px; }
    .ohi-scorenum { font-size: 14px; }
    .ohi-seats { --ohi-oppcw: 28px; gap: 4px; }
    .ohi-seat { padding: 4px 6px; }
    .ohi-trick-note { min-height: 36px; }
    .ohi-seat-cards { gap: 1px; }
    .panel.ohi-trickpanel { --ohi-trickcw: 34px; padding: 6px 8px; }
    .ohi-value { font-size: 22px; }
    .ohi-trick-head { min-height: 28px; }
    /* Five rows of "Bot Northeast" and four numbers fit the lifted dialog at 390px. */
    .ohi-result th,
    .ohi-result td { padding: 3px 5px; font-size: 14px; }
    .ohi-result th { font-size: 12px; }
}
/* ------------------------------------------------------------------------- */
/* Techu on the Chair                                                        */
/* ------------------------------------------------------------------------- */
/*
 * Prefix policy: every Techu class is `tch-` and every id `#tch-`. Shared ids
 * are reached only through the game's own classes (`.tch-handpanel`,
 * `.tch-status`, `.tch-scoreboard`). Shared classes are reused as they are:
 * hand cards select with `.card.sel` and arm with `.clickable`; backs are
 * `.card.simple.back`; the remaining-cards grid is the deck tracker's
 * `.deck-track-*`; the outcome uses `.winner-banner`, `.result-note` and
 * `.result-actions`.
 *
 * THE BOARD HOLDS ITS SIZE ALL GAME: 5 cells, 4 gaps and the two home-row
 * bands, with the cell size from the viewport. Everything above it (the
 * status line, the score line, the other seat's panel) reserves its tallest
 * shape, so the board and the hand never move.
 *
 * Seat tints follow the SEAT's color: red #e8665a, black a cool blue
 * #7fa7d6 (the official app's color for Black, and distinct from red under
 * the common color-vision deficiencies).
 */
.tch-board,
.tch-deckview {
    --tch-red: #e8665a;
    --tch-black: #7fa7d6;
}
.tch-board {
    --tch-cell: min(80px, calc((100vw - 48px) / 5));
    --tch-gap: 5px;
    --tch-rowgap: 5px;
    --tch-band: 6px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--tch-rowgap);
    box-sizing: border-box;
    width: fit-content;
    margin: 4px auto 6px;
    padding: 6px;
    border-radius: 16px;
    background: linear-gradient(180deg, #154528, #0f3a21);
    border: 1px solid rgba(255, 255, 255, 0.08);
    box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.45);
}
/* A home-row band: decorative, in the tint of the seat whose home it is. */
.tch-band {
    width: calc(5 * var(--tch-cell) + 4 * var(--tch-gap));
    height: var(--tch-band);
    border-radius: 3px;
    background: rgba(255, 255, 255, 0.15);
}
.tch-band.red { background: var(--tch-red); }
.tch-band.black { background: var(--tch-black); }
/*
 * PAINT ORDER, so a neighbor cell never covers a connection link. A link
 * reaches into the right or lower neighbor's margin, and that neighbor comes
 * later in the DOM. So no cell paints a background of its own: the grid is
 * its own stacking context (isolation), every cell fill is drawn by the
 * cell's ::before at z-index -1, links sit at 0, cards and slivers at 1, and
 * the hidden-card badge at 2. That holds only while .tch-cell creates NO stacking
 * context of its own, so nothing on .tch-cell sets opacity, transform,
 * filter, z-index, isolation or will-change. State classes that need those go
 * on the card (.tch-cut, .tch-flip) or on ::before (.tch-justcut); the ring
 * states are outlines and box shadows, which create none.
 */
.tch-grid {
    display: grid;
    grid-template-columns: repeat(5, var(--tch-cell));
    grid-auto-rows: var(--tch-cell);
    column-gap: var(--tch-gap);
    row-gap: var(--tch-rowgap);
    isolation: isolate;
}
/* A card is 14px shorter than its cell: 7px above and below for the links. */
.tch-grid .card { --cw: calc((var(--tch-cell) - 14px) * 64 / 89); }
.tch-cell {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 8px;
}
.tch-cell::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background: rgba(0, 0, 0, 0.2);
    pointer-events: none;
}
.tch-cell.tch-home-red::before { background: rgba(232, 102, 90, 0.13); }
.tch-cell.tch-home-black::before { background: rgba(127, 167, 214, 0.13); }
/* The network wash: a connected top card's cell, in its owner's tint. */
.tch-cell.tch-net.red::before { background: rgba(232, 102, 90, 0.34); }
.tch-cell.tch-net.black::before { background: rgba(127, 167, 214, 0.34); }
.tch-cell.tch-o-red { --tch-own: var(--tch-red); }
.tch-cell.tch-o-black { --tch-own: var(--tch-black); }
/* A link between two connected cards, center to center, under both cards. */
.tch-link {
    position: absolute;
    z-index: 0;
    border-radius: 3px;
    pointer-events: none;
}
.tch-link.red { background: var(--tch-red); }
.tch-link.black { background: var(--tch-black); }
.tch-link.r {
    left: 50%;
    top: calc(50% - 3px);
    width: calc(var(--tch-cell) + var(--tch-gap));
    height: 6px;
}
.tch-link.d {
    top: 50%;
    left: calc(50% - 3px);
    width: 6px;
    height: calc(var(--tch-cell) + var(--tch-rowgap));
}
/* A home-row card down (or up) to its band. */
.tch-link.b,
.tch-link.t {
    left: calc(50% - 3px);
    width: 6px;
    height: calc(var(--tch-cell) / 2 + var(--tch-rowgap) + var(--tch-band) / 2);
}
.tch-link.b { top: 50%; }
.tch-link.t { bottom: 50%; }
.tch-cell > .card {
    position: relative;
    z-index: 1;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35), 0 5px 12px rgba(0, 0, 0, 0.25);
}
/* Covered cards: up to three backs under the top card, 2px down and right
   per level (6px at most, inside the cell's 7px margin). */
.tch-cell > .card.tch-under {
    position: absolute;
    left: 50%;
    top: 50%;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
    filter: brightness(0.85);
}
.tch-cell > .card.tch-u1 { transform: translate(calc(-50% + 2px), calc(-50% + 2px)); }
.tch-cell > .card.tch-u2 { transform: translate(calc(-50% + 4px), calc(-50% + 4px)); }
.tch-cell > .card.tch-u3 { transform: translate(calc(-50% + 6px), calc(-50% + 6px)); }
/* A card cut off from its owner's home row: faded, still counted. The face
   stays OPAQUE and the fade is a filter, so on a stack the backs underneath
   never show through it (feedback F6). */
.tch-grid .card.tch-cut {
    filter: saturate(0.45) brightness(0.62);
    outline: 1px dashed var(--tch-own, #fff);
    outline-offset: 1px;
}
/* --- where a card may go ------------------------------------------------- */
/* With nothing picked: every space some hand card fits, quietly dashed. */
.tch-cell.legal { cursor: pointer; outline: 2px dashed rgba(255, 255, 255, 0.42); outline-offset: -2px; }
/* A picked card's spaces, bright; a capture and your own lower card read apart. */
.tch-cell.armed { cursor: pointer; outline: 3px solid var(--accent); outline-offset: -3px; }
.tch-cell.armed.capture { outline-color: #ff8a5c; }
.tch-cell.armed.own { outline-style: dashed; outline-color: rgba(255, 209, 102, 0.7); }
/* The space picked first, waiting for a card. */
.tch-cell.tch-picked { cursor: pointer; outline: 3px solid #fff; outline-offset: -3px; }
[data-clickable].tch-cell:focus-visible { outline: 3px solid #fff; outline-offset: -3px; }
/* The card played last: a white ring on its SQUARE (never on the card, which
   may ride a flight ghost). */
.tch-cell.tch-last { box-shadow: 0 0 0 3px #fff, 0 0 12px rgba(255, 255, 255, 0.45); }
.tch-cell.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
    outline-offset: -3px;
}
/* Your face-down opening card, face up to you only, with a closed eye. */
.tch-cell.tch-pending { outline: 2px dashed var(--accent); outline-offset: -2px; }
.tch-hidebadge {
    position: absolute;
    z-index: 2;
    right: 2px;
    bottom: 2px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.72);
    pointer-events: none;
}
.tch-hidebadge::before {
    content: '';
    position: absolute;
    left: 3px;
    right: 3px;
    top: 4px;
    height: 5px;
    border-bottom: 2px solid #ffd76a;
    border-radius: 0 0 8px 8px;
}
/* The reveal: each opening card turns face up. */
.tch-grid .card.tch-flip { animation: tch-flip 500ms ease-out; }
@keyframes tch-flip {
    from { transform: perspective(300px) rotateY(90deg); }
    to { transform: perspective(300px) rotateY(0deg); }
}
/* A play that cuts cards off flashes their cells once. */
.tch-cell.tch-justcut::before { animation: tch-justcut 900ms ease-out; }
@keyframes tch-justcut {
    from { background-color: rgba(255, 255, 255, 0.5); }
}
/* --- the other seat --------------------------------------------------------- */
/* A FIXED height, not a minimum: backs and faces differ by a few pixels, and
   the panel sits above the board. */
.tch-opp {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    box-sizing: border-box;
    height: 58px;
    padding: 4px 12px;
    margin: 0 auto 4px;
    width: fit-content;
    max-width: 100%;
}
/* The name is the one item that gives way: an online name runs to 20
   characters, and at 390px that pushed the row (and the page) past the
   viewport. The cards, the Ready chip and the deck chip keep their widths. */
.tch-opp .tch-opp-name {
    margin: 0;
    font-size: 14px;
    white-space: nowrap;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
.tch-opp-cards {
    display: flex;
    gap: 3px;
    justify-content: center;
    min-width: calc(3 * 34px + 6px);
}
.tch-opp-cards .card { --cw: 34px; }
/* HAND (def.ts renderOpponent): a setting whose largest hand is 4 or 5
   reserves the row for that many cards, so the panel keeps one width all
   game. Hand 3, and Refill with Jokers (3), carry neither class. */
.tch-opp.tch-hand4 .tch-opp-cards { min-width: calc(4 * 34px + 9px); }
.tch-opp.tch-hand5 .tch-opp-cards { min-width: calc(5 * 34px + 12px); }
.tch-opp-note { min-width: 3.4em; display: flex; justify-content: center; }
.tch-ready {
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 700;
    white-space: nowrap;
    color: #1d2230;
    background: var(--accent);
}
.tch-deckchip { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.tch-deckchip .card.tch-chipback { --cw: 13px; box-shadow: none; }
/* The other seat's deck chip with its Discards marker under it, one column
   (X1). The column is the chip's width, so the row keeps its width. */
.tch-seatside { display: flex; flex-direction: column; align-items: center; gap: 2px; }
/*
 * X: a seat's Discards marker, near its deck chip: the last discard as a
 * small face and the count, at one fixed size. With no discard it keeps its
 * space and shows nothing (def.ts renderDiscards). A new arrival pulses once.
 */
.tch-discmark {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    box-sizing: border-box;
    width: 86px;
    height: 14px;
    padding: 0 6px;
    font-size: 11px;
    line-height: 14px;
    white-space: nowrap;
    border-radius: 999px;
}
.tch-discmark.tch-discnone { visibility: hidden; }
.tch-discslot { display: inline-flex; width: 10px; height: 14px; align-items: center; }
.tch-discmark .card { --cw: 10px; box-shadow: none; }
.tch-discmark.tch-discpulse { animation: tch-discpulse 1200ms ease-out; }
@keyframes tch-discpulse {
    from { box-shadow: 0 0 0 2px var(--accent); background: rgba(255, 209, 102, 0.25); }
    to { box-shadow: 0 0 0 2px transparent; background: transparent; }
}
/* X1: the other seat's discard, face up over its hand row for a moment
   (def.ts REVEAL_MS), lifted and ringed. Out of the flow, so the row keeps
   its size; it turns over in place (tch-flip) as it appears. */
.tch-oppcards { position: relative; }
.tch-discreveal {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 3px;
    pointer-events: none;
}
.tch-discreveal .card {
    --cw: 34px;
    translate: 0 -3px;
    box-shadow: 0 0 0 2px var(--accent), 0 4px 10px rgba(0, 0, 0, 0.5);
}
.tch-discreveal .card.tch-flip { animation: tch-flip 500ms ease-out; }
/* The same over your hand row (a discard made for you behind the pass-and-play
   gate): the row under the head, out of the flow, so the box keeps its size. */
.tch-handpanel .tch-myreveal { inset: auto 0; top: calc(6px + 26px); height: calc(var(--cw) * 89 / 64); }
/* --- the hand ---------------------------------------------------------------- */
/*
 * H1: a thin box. 6px of padding above and below and a 26px head (a 22px
 * title line, the 22px deck chip, 4px to the cards) in place of 14px and 31px.
 * H2: ONE FIXED HEIGHT, the head plus one row of cards, for every Hand
 * setting: the largest hand is 5 (Hand 5; 4 under Refill), which is one row
 * at every width down to 320px, so the box never changes size between turns
 * or settings. The shared handspace.ts still holds #hand-cards at that row.
 */
.panel.tch-handpanel {
    position: relative;
    box-sizing: border-box;
    padding: 6px 12px;
    height: calc(2px + 12px + 26px + var(--cw) * 89 / 64);
}
.tch-handpanel .panel-head { min-height: 22px; }
.tch-handpanel .panel-head h3 { margin: 0 0 4px; font-size: 14px; line-height: 22px; }
.tch-handpanel .panel-head > .tch-deckchip { padding-top: 1px; padding-bottom: 1px; }
/* Your Discards marker, out of the flow at the left of the head, level with the chip. */
.tch-handpanel .panel-head > .tch-discmark { position: absolute; left: 0; top: 4px; }
/* Wider than a phone, the head is as wide as a row of 5 cards, so the chip
   sits at the right end of the cards rather than the far edge of the box. */
@media (min-width: 561px) {
    .tch-handpanel .panel-head { max-width: calc(5 * var(--cw) + 32px); margin-inline: auto; }
}
/* L3: "Leads" during your Refill lead, left of your cards, out of the flow.
   A lead always holds a Refill hand (4 at most), which leaves that space free. */
.tch-mynote {
    position: absolute;
    left: 4px;
    top: calc(6px + 26px + var(--cw) * 89 / 128);
    translate: 0 -50%;
}
/* Wider than a phone, against the head's 5-card frame (centered above): its
   right edge 4px left of the frame, so it stays beside the cards. */
@media (min-width: 561px) {
    .tch-mynote { left: auto; right: calc(50% + (5 * var(--cw) + 32px) / 2 + 4px); }
}
/* L4 (def.ts waitsOnLead): your new hand while the other seat's Refill lead
   is still to come. A filter on the opaque face, never opacity, and lighter
   than a cut-off board card's (.tch-cut), so it reads as "not yet" rather
   than "lost". A filter changes no box, so nothing moves. */
.tch-handpanel #hand-cards .card.tch-waitlead { filter: grayscale(0.85) brightness(0.86); }
.tch-handpanel #hand-cards .card.tch-dead { opacity: 0.55; }
.tch-handpanel #hand-cards .card.tch-nofit { opacity: 0.45; }
.tch-handpanel #hand-cards .card.tch-fits { outline: 3px solid var(--accent); outline-offset: 2px; }
/* No card has a space: every card is a discard choice, ringed in the
   capture orange (dashed until picked). With Treasure Jokers the same ring
   marks a card whose only spaces are on your own Joker (def.ts K5). */
.tch-handpanel #hand-cards .card.tch-discardable { outline: 3px dashed #ff8a5c; outline-offset: 2px; }
.tch-handpanel #hand-cards .card.tch-discardable.sel { outline-style: solid; }
/* The Discard button: one button row, reserved at the large form's height so
   the row never grows when Discard becomes the only move. */
.controls.tch-actions { min-height: 46px; margin: 6px 0 4px; align-items: center; }
.controls.tch-actions .btn.tch-discard-only { font-size: 16px; padding: 9px 24px; }
/* --- status and score line -------------------------------------------------- */
/* Reserved at the LONGEST status (a play that cuts and reconnects, then the
   prompt; or a game end with three or four discards made for seats): 2 lines
   on desktop, 3 at 660px and below (measured at the 660px rule below). */
.status.tch-status { line-height: 1.35; min-height: calc(2 * 1.35em); }
.tch-error { margin-top: 4px; font-size: 13px; color: #ff9c8a; }
/* The shared opt-in list in platform.css is left alone; Techu says it here. */
.progress.tch-scoreboard {
    opacity: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    flex-wrap: nowrap;
    min-height: 34px;
    white-space: nowrap;
}
/* A long online name shortens with an ellipsis rather than widening the
   line. Your own chip ("You", or a pass-and-play "Player N") and the phase
   label keep their width. */
.progress.tch-scoreboard > .progress-score { min-width: 0; }
.progress.tch-scoreboard > .progress-score.mine,
.progress.tch-scoreboard > .progress-round { flex-shrink: 0; }
.tch-scorename { font-size: 14px; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.tch-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }
.progress-score.leading .tch-scorenum { color: var(--accent); }
.progress-score.tch-turn .tch-scorename::before { content: '\25B6\00a0'; color: var(--accent); }
/* Visually hidden, still read: who leads, in words. */
.tch-vh {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}
/* --- the remaining-cards dialog ------------------------------------------- */
/* The score-breakdown modal's shape: a fixed full-page anchor that takes every
   pointer event, a translucent backdrop, and a card that is position:
   relative (never transform-centered). z-index 500: under the handoff gate's
   600, over the tutorial coach bar. */
.tch-deckview {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: none;
    box-sizing: border-box;
    padding: 12px;
}
.tch-deckview.tch-dv-open {
    display: flex;
    align-items: center;
    justify-content: center;
}
.tch-deckview::before {
    content: '';
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
}
.tch-dvbox {
    position: relative;
    box-sizing: border-box;
    width: min(440px, 100%);
    max-height: 100%;
    overflow-y: auto;
    padding: 14px;
    background: var(--felt-dark);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
    box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
}
/* The box takes focus only from a press inside it (tabindex -1), never from
   the keyboard, so it draws no ring of its own. */
.tch-dvbox:focus { outline: none; }
.tch-dvhead {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 8px;
}
.tch-dvtitle { margin: 0; font-size: 15px; line-height: 1.35; text-align: left; }
.tch-dvlegend { justify-content: center; flex-wrap: wrap; margin-bottom: 8px; }
/* D1: one section a seat, the tapped chip's first. */
.tch-dvsec + .tch-dvsec { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--panel-border); }
.tch-dvsectitle { margin: 0 0 4px; font-size: 14px; line-height: 1.35; text-align: left; }
.tch-dvdetail { margin: 0 0 6px; font-size: 13px; line-height: 1.35; opacity: 0.85; }
.tch-deckview .deck-track-grid.tch-rankgrid {
    grid-template-columns: 1.4em repeat(var(--tch-ranks), var(--dtcw));
}
/* The Treasure Joker's own slot, under the rank grid: a label and one cell
   in the tracker's sizes. */
.tch-deckview .deck-track-grid.tch-dvjoker {
    grid-template-columns: auto var(--dtcw);
    column-gap: 6px;
    margin-top: 6px;
}
.tch-deckview .card.tch-dv-covered { opacity: 0.4; }
.tch-deckview .deck-track-key.tch-covered::before { opacity: 0.4; }
/* --- the result ------------------------------------------------------------ */
.tch-result {
    border-collapse: collapse;
    margin: 6px auto 4px;
    font-size: 14px;
}
.tch-result th, .tch-result td { padding: 3px 10px; text-align: left; }
.tch-result th.num, .tch-result td.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
    min-width: 3.5em;
}
/* The total under its breakdown (Treasure Jokers), as Garbo's result marks one. */
.tch-result tr.tch-total td { border-top: 1px solid var(--panel-border); font-weight: 800; }
/* Measured in Chromium. The longest name-independent statuses are both pass
   and play:
   - a cut-and-reconnect play, then "No card has a space. Pick one to
     discard." (146 characters), which needs 3 lines below about 610px at
     the desktop size;
   - a game end where the last play is followed by four discards made for
     seats, grouped by seat: "Player 2 plays an Ace on Player 1's King.
     Player 1's two Queens and Player 2's two Queens have no space, so they
     are discarded. Player 1 wins." (142), which needs 3 lines below 650px,
     hence this rule's 660px. Three discards run 138.
   Both fit 3 lines at 390px and 360px. The end form fits only because pass
   and play ends it with the winner alone (def.ts renderStatus): with "13 to
   12" it ran 151 and wrapped a 4th line at 360px. Against the bot the 3- and
   4-discard ends run 123 at most and keep the score. An online name of 20+
   characters can still wrap a 4th line at a phone width, as every
   name-bearing recap could before RULING C. */
@media (max-width: 660px) {
    .status.tch-status { min-height: calc(3 * 1.35em); }
}
@media (max-width: 560px) {
    .tch-board { --tch-gap: 4px; --tch-rowgap: 6px; padding: 4px; border-radius: 12px; }
    .tch-opp { height: 46px; gap: 8px; padding: 4px 8px; }
    .tch-opp .tch-opp-name { font-size: 13px; }
    .tch-opp-cards { min-width: calc(3 * 26px + 6px); }
    .tch-opp-cards .card { --cw: 26px; }
    .status.tch-status { font-size: 15px; line-height: 1.3; min-height: calc(3 * 1.3em); }
    .tch-scorename { font-size: 13px; }
    /* HAND: 4 or 5 cards do not fit in one row beside the name, the Ready
       slot and the deck chip at 360px. So the name is a title row across
       the whole panel, on the board axis, with the deck chip, the cards
       and the Ready slot below it, at one fixed height (16 + 2 + 36 + 6 of
       padding is 60px, inside 62). The two side tracks are equal fr
       tracks in a fit-content grid, so both take the wider of the chip and
       the Ready slot, and the cards sit on the title's axis whether Ready
       shows or not. Their minimum is 0, not auto: at 360px Hand 5's panel
       meets max-width, and an auto minimum kept the chip's track wider and
       put the backs 4px right of the axis. The chip then reaches at most
       4px into the panel's 8px padding. */
    .tch-opp.tch-hand4,
    .tch-opp.tch-hand5 {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
        grid-template-areas: 'name name name' 'chip cards note';
        align-items: center;
        justify-content: center;
        column-gap: 8px;
        row-gap: 2px;
        height: 62px;
        padding: 3px 8px;
    }
    .tch-opp.tch-hand4 .tch-opp-name,
    .tch-opp.tch-hand5 .tch-opp-name { grid-area: name; line-height: 16px; text-align: center; }
    .tch-opp.tch-hand4 .tch-oppcards,
    .tch-opp.tch-hand5 .tch-oppcards { grid-area: cards; }
    .tch-opp.tch-hand4 .tch-opp-cards { min-width: calc(4 * 26px + 9px); }
    .tch-opp.tch-hand5 .tch-opp-cards { min-width: calc(5 * 26px + 12px); }
    .tch-opp.tch-hand4 .tch-opp-note,
    .tch-opp.tch-hand5 .tch-opp-note { grid-area: note; justify-self: start; }
    .tch-opp.tch-hand4 .tch-seatside,
    .tch-opp.tch-hand5 .tch-seatside { grid-area: chip; justify-self: end; }
    /* X1: the chip and the Discards marker share the row's 36px (22 + 14). */
    .tch-opp .tch-seatside { gap: 0; }
    .tch-opp .tch-seatside > .tch-deckchip { padding-top: 1px; padding-bottom: 1px; }
    .tch-discreveal .card { --cw: 26px; }
}
/* On a touch screen a .btn.small is 36px tall (src/duelpoker/style.css), more
   than the phone panel has beside the Discards marker, so the other seat's
   chip keeps its own height there and grows its tap target invisibly, as the
   shared head does for its buttons (platform.css): 4px above it and over the
   Discards marker under it (14px, no handler of its own), 40px in all. */
@media (pointer: coarse) and (max-width: 560px) {
    .tch-opp .tch-seatside > .tch-deckchip { position: relative; min-height: 0; }
    .tch-opp .tch-seatside > .tch-deckchip::after { content: ''; position: absolute; inset: -4px -4px -14px; }
}
/* --- the open board (open-board design 9.1 to 9.3) -------------------------- */
/*
 * Seven tracks reserved from the first render to the last: a gutter, the 5
 * slots, a gutter. The gutter is 24px at every width, so a column-opening
 * space in it is never narrower than a tap. At phone widths the gutters come
 * out of the cells, so the box keeps the fixed board's outer width (368px at
 * 390, 338px at 360); every row is shorter, the same all game. Everything is
 * scoped under .tch-board.tch-open, which def.ts sets from state.openBoard.
 */
.tch-board.tch-open {
    --tch-gutter: 24px;
    --tch-cell: min(80px, calc((100vw - 48px - 2 * (var(--tch-gutter) + var(--tch-gap))) / 5));
    /* How far a gutter's hit area reaches out: the board's padding and border. */
    --tch-hit-out: 7px;
}
.tch-board.tch-open .tch-grid,
.tch-board.tch-open .tch-band {
    grid-template-columns: var(--tch-gutter) repeat(5, var(--tch-cell)) var(--tch-gutter);
}
/* The band: one segment per track, tinted for the board's columns only. */
.tch-board.tch-open .tch-band {
    display: grid;
    column-gap: var(--tch-gap);
    width: auto;
    background: none;
}
.tch-board.tch-open .tch-seg { border-radius: 3px; }
.tch-board.tch-open .tch-seg.red { background: var(--tch-red); }
.tch-board.tch-open .tch-seg.black { background: var(--tch-black); }
/*
 * An open column before the fix: hatched, never outlined (the outline means
 * "you can play here"). Scoped under .tch-board.tch-open (specificity 0,4,1),
 * so it beats the home tint .tch-cell.tch-home-red::before (0,2,1) wherever it
 * sits in the file; def.ts never puts a home tint on an edge cell anyway.
 */
.tch-board.tch-open .tch-cell.tch-edge::before,
.tch-board.tch-open .tch-seg.tch-seg-edge {
    background: repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.07) 0 3px, transparent 3px 8px);
}
/* Further out, and the gutters after the fix: nothing painted. */
.tch-board.tch-open .tch-cell.tch-void::before { background: none; }
/* Your own face-down opening card in a gutter (a replay that opens a
   column) fits the gutter until the reveal moves it onto a slot. */
.tch-board.tch-open .tch-cell.tch-gutter-l > .card,
.tch-board.tch-open .tch-cell.tch-gutter-r > .card { --cw: calc(var(--tch-gutter) - 4px); }
/*
 * A gutter edge cell's hit area reaches out over the board's padding and
 * border (and, on a touch screen, 7px into the page margin), never inward:
 * a play from a gutter is the one that opens a column, and with a card
 * picked one tap commits it. The pseudo-element is the cell's own box for
 * hit testing; it paints nothing and sets no z-index.
 */
.tch-board.tch-open .tch-cell.tch-edge.tch-gutter-l::after,
.tch-board.tch-open .tch-cell.tch-edge.tch-gutter-r::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    width: var(--tch-hit-out);
}
.tch-board.tch-open .tch-cell.tch-edge.tch-gutter-l::after { right: 100%; }
.tch-board.tch-open .tch-cell.tch-edge.tch-gutter-r::after { left: 100%; }
@media (max-width: 560px) {
    /* The board's padding is 4px and its border 1px here. */
    .tch-board.tch-open { --tch-hit-out: 5px; }
}
@media (pointer: coarse) {
    .tch-board.tch-open { --tch-hit-out: 12px; }
}
@media (pointer: coarse) and (min-width: 561px) {
    .tch-board.tch-open { --tch-hit-out: 14px; }
}
/* --- Treasure Hunt's Treasure (src/techu/treasure.ts) ----------------------- */
/*
 * Treasure Hunt calls the Joker the Treasure and puts a badge on its face:
 * the class trh-treasure and a child .trh-chest, both added by
 * src/techu/treasure.ts markTreasure under ruleSet 'treasurehunt' only, so no
 * Techu card ever matches these rules. A dark rounded square with a white rim
 * and a gold chest, at the BOTTOM CENTER of the card (the user, 2026-11-01:
 * "Put the treasure icon on the bottom center of the Joker cards"). A red
 * mark on a white face disappears, so the badge is dark with a white rim.
 * Sized and placed in percentages of the card, never --cw, so a flight ghost
 * (a deep clone, fixed and sized in px on <body>) carries the same badge in
 * the same place. 36% of the card's width, and its bottom 8% of the card's
 * height below the card's (at most 4px), so it sits mostly under the "R" of
 * the simplified face ("J over R") and over the full-art jester's feet:
 * 9.6px wide and 3px of overhang on the 26.8px board card at 360px, against
 * 20px down to the next card. A cut-off Treasure on the board fades with the
 * card's own filter (.tch-grid .card.tch-cut), which its child takes with it.
 */
.card.trh-treasure:not(.card-fly) { position: relative; }
.card.trh-treasure > .trh-chest {
    position: absolute;
    left: 50%;
    translate: -50% 0;
    bottom: max(-8%, -4px);
    width: 36%;
    aspect-ratio: 1;
    pointer-events: none;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='1' y='1' width='22' height='22' rx='5' fill='%231d2230' stroke='%23fff' stroke-width='2'/%3E%3Cpath d='M5 11.5V10c0-2.8 2.6-4.5 7-4.5s7 1.7 7 4.5v1.5z' fill='%23ffcf4a'/%3E%3Crect x='5' y='11.5' width='14' height='7' rx='1' fill='%23e39b17'/%3E%3Crect x='10.4' y='9.8' width='3.2' height='4' rx='.8' fill='%23fff'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}
/*
 * The Discards marker's face is 10px wide, where a half-width badge is a 5px
 * smudge. There the badge covers the face: the marker's simplified Treasure
 * ("J over R") says nothing the chest does not, and the marker's name says
 * "Treasure" in words.
 */
.tch-discmark .card.trh-treasure > .trh-chest { bottom: 14%; width: 100%; }
body.motion-off .tch-grid .card.tch-flip,
body.motion-off .tch-discreveal .card.tch-flip,
body.motion-off .tch-discmark.tch-discpulse,
body.motion-off .tch-cell.tch-justcut::before,
body.motion-off .tch-cell.hinted { animation: none; }
/* ------------------------------------------------------------------------- */
/* Olm                                                                       */
/* ------------------------------------------------------------------------- */
/*
 * Prefix policy: every Olm class is `olm-`. Shared classes are reused as they
 * are (`card`, `simple`, `back`, `empty`, `mini`, `btn`, `status`, `progress`,
 * `controls`, `panel`, `eye-badge`), and no rule here names a shared id: the
 * status and progress lines are reached through `olm-status` and
 * `olm-progress`, which def.ts adds to their className, and the hand panel
 * through `olm-handpanel`. Nothing here touches #hand-cards; handspace.ts owns
 * its height and platform.css its centering.
 *
 * CARD STATES are literal rotations (design.md, "Cards and states"). A passive
 * or blinded card turns with the INDEPENDENT `rotate` property, as Cuttle's
 * `.card.ctl-glasses` does, so a resting card's `transform` stays `none` and the
 * animator's transform ghost composes with the turn. A ghost keeps its state
 * classes, so the rotation rules are deliberately not scoped to the board. A
 * blinded card is a back with a red ring as well, so that it reads apart from
 * a passive face at board size (polish brief item F).
 *
 * GHOST SAFETY. animate.ts clones a card into <body> with the single-class
 * `.card-fly { position: fixed }`. So no rule that declares `position` or
 * `transition` may match a card by its own classes alone: each one names
 * `.olm-board` in an ancestor compound, which a ghost on <body> cannot match.
 * olm_app_test's stylesheet scan enforces this over `.card` and the ten Olm
 * card classes.
 *
 * SIZES (polish brief item 1; the scale run, 2026-10-05). Rune fields, totem
 * slots, and the spirit realm are `--cw` cards in squares of `--cw x 89/64`,
 * so a quarter turn moves nothing (Requirement 27.2). At 720px and below they
 * are 42px, the smallest width that keeps a Covered_Rune chip with ruling 57's
 * eye badge (about 41px at the 11px floor) inside an upright card's painted
 * box (27.1), and the damage bars (`--olm-barcw`), the far hand
 * (`--olm-minicw`), and the deck and discard tops (`--olm-pilecw`) are 34px.
 * From 721px the board grows with the display: `--olm-cw` runs from 42px to
 * 96px on the viewport's width and height, and every secondary size keeps its
 * fraction of it (34/42, and 6/42 for the field gap), so the board keeps its
 * shape ("the board grows with the display", below). Every board face is on
 * the simplified sprite at every size (ruling 56). The hand keeps the site's
 * size.
 *
 * LAYOUT. At 721px and up each side is a 2-column grid, the rune field beside
 * its damage bar tray (the far head spans both). At every width the middle
 * band is one row: the spirit pair (deck and discard under "Spirits"), the
 * realm, and the rune pair (under "Runes"). The DOM order is unchanged
 * (design.md departure). At 720px and below the sides stack in Requirement
 * 27.5's order and use the phone spacing values.
 *
 * PLAYTEST ROUND 3 (.agents/tasks/olm-feedback-2026-10-05). The far hand is a
 * row of cards with the known runes face up at its left end (item A1), which
 * replaces the known-cards strip and the desktop head's 6px scrollbar
 * allowance. The pile labels are two, one over each pair, and a deck's count
 * is a badge on its card (A2). The List buttons are gone: a discard is the
 * button (A3), and its list covers the whole board (A4). Each damage bar's
 * label sits above the bar (A5). The middle band was two rows on phones (the
 * realm, then a 54px pile row); it is one row now, which pays for the bar
 * labels and the build line's fourth line (item 6) at 390px.
 *
 * THE PHONE FOLD (the fold-fix run, 2026-10-06). At 390x844 the hand cards sat
 * 98px below the fold, measured from the PAGE top, almost all of it the 104px
 * of status line and progress line above the board. At 560px and below the two
 * lines are now ONE box: def.ts paints the turn as an `olm-turn` span at the
 * head of the status line, this file hides that span above 560px and hides
 * `#progress-line` at 560px and below, and the turn line no longer repeats the
 * phase name the status sentence already carries (Requirement 29.5). The band
 * is 63px instead of 104px, and the rest of the gap comes out of the spacing
 * above and inside the board, the two margins under it, and the hand panel's
 * head. No card got smaller and nothing was dropped.
 *
 * RESERVATIONS (27.9), so the board and the hand below it never move:
 *  - the far head is fixed at one 34px card (47.3px), whatever the hand holds:
 *    its cards overlap rather than wrap;
 *  - each pile pair is its 10px label over a 34px card (58.3px), inside the
 *    middle band's row, which the realm (58.4px) sets on desktop;
 *  - the build line and the controls share ONE box, `.olm-under`, which
 *    reserves the tallest combination of the two that a real build state
 *    paints, not the sum of each one's tallest (the scale run). Inside it the
 *    build line (the kind, then the combo, or what an unfinished move still
 *    needs and the offered effect's and sacrifice's short forms, up to four
 *    15px lines beside Back) and the controls (44px rows) each take their own
 *    height, stacked from the top, so an idle End button sits right under the
 *    board. The measured tallest, from a census of every selection of every
 *    probe position, the tutorial's chapters, and 40 self-play games (6754
 *    distinct paints, vs-bot and hotseat names):
 *      721px and up, and 561 to 720px: 108px, the four-line attack start (60)
 *        over one row of controls (44), at a 4px gap;
 *      381 to 560px: 141px, the Jack's options (three text lines beside Back,
 *        45) over their three controls on two rows (92);
 *      380px and below: 156px, a K♦'s attack start (four lines, 60) over
 *        "Diamonds effect" and "Sacrifice as a King", which take two rows there.
 *
 * AREA LABELS (item E): a rune field's label stands outside the flow,
 * vertical, left of the field; the realm's likewise left of the realm above
 * 560px, and over it at 560px and below, where the one-row middle band has no
 * margin to spare; a pile pair's sits over the pair; a damage bar's sits in its
 * tray over the bar (item A5). On desktop the bar tray sits beside the much
 * taller rune field, so its label adds no height there.
 */

/* --- the board ------------------------------------------------------------ */

/*
 * The board's sizes are registered lengths, so each one computes to px on the
 * board and descendants inherit the number, not the formula (the probe reads
 * `--cw` and `--olm-barcw` back as px). A browser without @property still
 * substitutes the same formulas, so the layout is the same there.
 */
@property --olm-cw { syntax: '<length>'; inherits: true; initial-value: 42px; }
@property --olm-barcw { syntax: '<length>'; inherits: true; initial-value: 34px; }
@property --olm-minicw { syntax: '<length>'; inherits: true; initial-value: 34px; }
@property --olm-pilecw { syntax: '<length>'; inherits: true; initial-value: 34px; }
@property --olm-gap { syntax: '<length>'; inherits: true; initial-value: 6px; }

.olm-board {
    --olm-cw: 42px;
    --cw: var(--olm-cw);
    --olm-barcw: 34px;
    --olm-minicw: 34px;
    --olm-pilecw: 34px;
    --olm-slot: calc(var(--cw) * 89 / 64);
    --olm-gap: 6px;
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    box-sizing: border-box;
    /* A fixed column, so a row that grows never widens the board and moves its slots sideways. */
    max-width: 640px;
    margin: 4px auto 8px;
    padding: 9px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.14);
    border: 1px solid var(--panel-border);
}

.olm-side {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    align-self: stretch;
}
/* The middle band, one row at every width: the spirit pair, the realm, and the
   rune pair (#olm-piles is display: contents, so its two pairs are this row's
   items, ordered around the realm). */
.olm-mid {
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    gap: 8px;
    align-self: stretch;
    padding: 4px 0;
    border-block: 1px solid rgba(255, 255, 255, 0.08);
}
#olm-piles.olm-piles { display: contents; }
.olm-mid .olm-spiritpair { order: 1; }
.olm-mid > .olm-realmrow { order: 2; margin-inline: 20px; }
.olm-mid .olm-runepair { order: 3; }

/* The far seat's head: its name and its hand center as one unit. Fixed height:
   one 34px card, whatever the hand holds. */
.olm-sidehead {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 0;
    height: calc(var(--olm-minicw) * 89 / 64);
    max-width: 100%;
}
.olm-sidehead h3 {
    flex: none;
    max-width: 45%;
    margin: 0;
    font-size: 15px;
    opacity: 0.9;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The far hand (item A1): one 34px card per card held, the known runes face up
   first. Each card sits in a wrapper that shrinks, so the row stays one line and
   the cards overlap as the hand grows: the card keeps its width and the next
   card paints over the rest of it. The backs shrink first (their flex-shrink is
   1000 times a face's), so the known faces stay whole longest. The last card
   never shrinks, so the row ends on a whole card. */
.olm-farhand {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    flex: 0 1 auto;
    min-width: 0;
    margin-left: 8px;
}
.olm-farcard {
    display: flex;
    flex: 0 1000 calc(var(--olm-minicw) + 4px);
    min-width: 3px;
}
.olm-farcard.olm-farface { flex-shrink: 1; min-width: 14px; }
.olm-farcard:last-child { flex: none; width: var(--olm-minicw); }
.olm-board .olm-farhand .card { --cw: var(--olm-minicw); flex: none; }

/* --- slots ---------------------------------------------------------------- */

.olm-slot {
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
    border-radius: 6px;
}
/* An empty slot is an outline at exactly the slot's size, with no text. */
.olm-slot.olm-slotempty {
    background: rgba(255, 255, 255, 0.03);
    border: 1px dashed rgba(255, 255, 255, 0.22);
}
/* Field, totem, and realm: squares, so a quarter turn moves nothing (27.2). */
.olm-board .olm-field .olm-slot,
.olm-board .olm-totems .olm-slot,
.olm-board .olm-realm .olm-slot {
    width: var(--olm-slot);
    height: var(--olm-slot);
}
/* Damage cards never turn, so their slots are card-shaped. The width rides on a
   property of the bar, not on .card.mini. */
.olm-board .olm-bar .olm-slot {
    width: var(--olm-barcw);
    height: calc(var(--olm-barcw) * 89 / 64);
}
.olm-board .olm-bar .card { --cw: var(--olm-barcw); }
/* The chips' containing block. A body-level ghost is outside .olm-board, so this
   rule cannot out-specify .card-fly { position: fixed } on it. */
.olm-board .olm-slot > .card { position: relative; }

/* The rune field and the totem position center together as one row. The field
   itself is a fixed grid of 2 rows of 3. Its label stands outside the flow. */
.olm-fieldrow {
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: var(--olm-gap);
}
.olm-field {
    display: grid;
    grid-template-columns: repeat(3, var(--olm-slot));
    grid-template-rows: repeat(2, var(--olm-slot));
    gap: var(--olm-gap);
}
.olm-totems { display: flex; }
/* Ruling 8's second totem, offset by a third of a card: the pair is 4/3 of a card
   wide, inside the square's 89/64 side. A margin, not a position. */
.olm-board .olm-totems .olm-slot > .card + .card { margin-left: calc(var(--cw) * -2 / 3); }

/* A damage bar in its tray: a faint red band with the bar's label over the bar
   (item A5), so the bar never reads as a third rune row (issue 6). */
.olm-bartray {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: 3px 8px;
    border-radius: 6px;
    background: rgba(160, 40, 40, 0.16);
}
.olm-bar {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: 4px;
    min-height: calc(var(--olm-barcw) * 89 / 64);
}
.olm-realmrow {
    position: relative;
    display: flex;
    justify-content: center;
}
.olm-realm {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: var(--olm-gap);
    min-height: var(--olm-slot);
}

/* --- area labels (item E) ---------------------------------------------------- */

/* Quiet, small, never interactive. A field's and the realm's run vertically,
   reading upward, in the free margin left of what they name. */
.olm-fieldcap, .olm-realmcap {
    position: absolute;
    right: 100%;
    top: 50%;
    translate: 0 -50%;
    margin-right: 6px;
    writing-mode: vertical-rl;
    rotate: 180deg;
    font-size: 11px;
    line-height: 1.1;
    white-space: nowrap;
    opacity: 0.7;
    pointer-events: none;
}
.olm-realmcap { font-size: 10px; }
/* One line over the bar, at every width (item A5). */
.olm-barcap {
    font-size: 11px;
    line-height: 11px;
    text-align: center;
    white-space: nowrap;
    opacity: 0.8;
}

/* --- piles ---------------------------------------------------------------- */

/* The two pile pairs (item A2): "Spirits" over the spirit deck and the spirit
   discard, and "Runes" over the rune deck and the rune discard. Each pair is its
   one 10px label line over a row of two 34px cards (58.3px). A deck's count is a
   badge on its own card, and a discard is its top card, which is the button that
   opens its list (item A3). */
.olm-pilepair {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
}
.olm-pilerow {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 4px;
}
.olm-pilecap {
    font-size: 10px;
    line-height: 10px;
    text-align: center;
    white-space: nowrap;
    opacity: 0.75;
}
.olm-pile { display: flex; }
.olm-pileslot {
    position: relative;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--olm-pilecw);
    height: calc(var(--olm-pilecw) * 89 / 64);
}
.olm-board .olm-pileslot .card { --cw: var(--olm-pilecw); }
/* The count rides on the deck's card, so it takes no room of its own. */
.olm-count {
    position: absolute;
    right: 1px;
    bottom: 1px;
    padding: 0 3px;
    border-radius: 4px;
    background: rgba(0, 0, 0, 0.72);
    color: #fff;
    font-size: 11px;
    line-height: 14px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    pointer-events: none;
}
.olm-count:empty { display: none; }
/* A discard is a plain button around its top card: no chrome of its own, a ring
   on hover, and the site accent while focused or while its list is open. */
.olm-board .olm-discardbtn {
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
}
.olm-board .olm-discardbtn:hover:not(:disabled) > .card { outline: 2px solid rgba(255, 255, 255, 0.6); outline-offset: 1px; }
.olm-board .olm-discardbtn:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.olm-board .olm-discardbtn[aria-expanded="true"] > .card { outline: 3px solid var(--accent); outline-offset: 1px; }
.olm-board .olm-discardbtn:disabled { cursor: default; }

/* The whole discard (ruling 23; item A4; the scale run). An opaque scrim covers
   the whole board, so no part of the board shows through or around it, and it
   sits inside the board, so it moves nothing. The board's sections are inert
   while it is open (board.ts). On the scrim, the viewer is a panel sized to its
   cards and centered over the board: it wraps them at the board's width, and a
   pile taller than the board scrolls inside it. */
.olm-board .olm-pilepop {
    position: absolute;
    /* Over the board's own 1px border too, so nothing of the board shows around it. */
    inset: -1px;
    z-index: 20;
    box-sizing: border-box;
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 0;
    padding: 8px;
    border: 1px solid var(--panel-border);
    border-radius: inherit;
    background: var(--felt-dark);
    overflow: hidden;
}
.olm-board .olm-pilepop[hidden] { display: none; }
/* The sections behind the open viewer keep their boxes (so nothing moves) but paint nothing,
   not even a card's shadow past the board's edge. */
.olm-board > .olm-covered { visibility: hidden; }
/* As wide as its cards in one row (board.ts writes the count, `--olm-popn`), never narrower than
   its title and Close, and never wider than the scrim, where the cards wrap. The 26px is the
   panel's padding and border. */
.olm-board .olm-poppanel {
    --olm-popcw: 46px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    width: calc(var(--olm-popn, 1) * (var(--olm-popcw) + 4px) - 4px + 26px);
    min-width: max-content;
    max-width: 100%;
    max-height: 100%;
    margin: 0;
    padding: 8px 12px 12px;
    overflow: hidden;
}
/* The title centered over the panel, and Close at its right: the empty first column is as wide
   as Close's, so the title stays on the panel's axis however narrow the panel is. */
.olm-pophead {
    flex: none;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    column-gap: 8px;
    min-height: 44px;
    margin-bottom: 8px;
}
.olm-pophead .olm-poptitle { grid-column: 2; margin: 0; text-align: center; white-space: nowrap; }
.olm-pophead > .olm-popclose {
    grid-column: 3;
    justify-self: end;
    box-sizing: border-box;
    min-height: 44px;
    margin: 0;
    padding: 0 14px;
    font-size: 16px;
    font-weight: 600;
}
/* The list is a grid of card-wide columns, as many as the panel's width holds. A long pile's
   rows line up by column and read from the left, and auto-fit drops the empty columns of a short
   one, so either way the block of cards is centered in the panel. */
.olm-board .olm-poplist {
    flex: 0 1 auto;
    min-height: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, var(--olm-popcw));
    justify-content: center;
    align-content: start;
    gap: 4px;
    overflow-y: auto;
}
/* The same 46px and 34px as .card.mini (platform.css), named here because the columns need it. */
.olm-board .olm-poplist .card { --cw: var(--olm-popcw); }
@media (max-width: 560px) {
    .olm-board .olm-poppanel { --olm-popcw: 34px; }
}

/* --- desktop: the board grows with the display -------------------------------- */

/*
 * From 721px `--olm-cw` is the smaller of a width term and a height term,
 * clamped to 42px (the phone size, so no card is ever smaller than it was) and
 * 96px (the largest hand card, platform.css's `--cw` curve, so the board's runes
 * never outgrow the hand's cards). Both terms come from the board measured in
 * Chromium with every secondary size proportional (the scale run's scan, cw 42
 * to 90): it is 52px plus 8.365 x cw tall, and its widest row, a side's field
 * row and bar tray with the field's label left of them, needs 10.04 x cw plus
 * 102px of width.
 *
 * WIDTH. Above 950px the column is the viewport less 348px (the 300px log
 * beside it, its 16px gap, and the page's 16px padding; platform.css,
 * #screen-game), so the board has room for 10.04 x cw + 102px while cw is at
 * most (100vw - 450px) / 10.04. At 950px and below the log goes under the
 * board and the column is the viewport less 32px, so the bound is
 * (100vw - 134px) / 10.04. Each term takes 20px more off, for a classic
 * scrollbar, which 100vw counts and the column does not.
 *
 * HEIGHT. `--olm-reserve` is everything from the page top to the middle of the
 * first hand card that is not the board's cards: 152.8px above the board, the
 * board's 52px, 124px under it (its 8px margin, the 108px build-line-and-
 * controls box, and that box's 8px margin), 40px from the hand panel's top to
 * its cards, and 66.75px, half the largest hand card. The height term puts the
 * hand card's middle at 95% of the viewport, less what the hand card is short
 * of its largest. So the span from the page top to the hand mid is at most the
 * viewport (the fit gate) and at least 85% of it (the grow gate) wherever the
 * clamp does not bind.
 */
@media (min-width: 721px) {
    .olm-board {
        --olm-reserve: 436px;
        --olm-cw: clamp(42px, min((100vw - 470px) / 10.04, (95vh - var(--olm-reserve)) / 8.365), 96px);
        --olm-barcw: calc(var(--olm-cw) * 34 / 42);
        --olm-minicw: calc(var(--olm-cw) * 34 / 42);
        --olm-pilecw: calc(var(--olm-cw) * 34 / 42);
        --olm-gap: calc(var(--olm-cw) * 6 / 42);
        /* 640px at 42px, as before, so the board keeps its proportions as it grows. */
        max-width: calc(var(--olm-cw) * 640 / 42);
    }
}
@media (min-width: 721px) and (max-width: 950px) {
    .olm-board { --olm-cw: clamp(42px, min((100vw - 154px) / 10.04, (95vh - var(--olm-reserve)) / 8.365), 96px); }
}
/* --- desktop: fields beside their bars ----------------------------------------- */

@media (min-width: 721px) {
    .olm-side {
        display: grid;
        grid-template-columns: auto auto;
        justify-content: center;
        align-items: center;
        column-gap: 14px;
        row-gap: 6px;
    }
    /* The head spans both columns without sizing them: inline-size containment gives it no
       intrinsic width, so a far hand of any size overlaps inside the columns the field and the
       bar set, rather than widening them and moving every slot sideways (item A1). */
    .olm-far > .olm-sidehead { grid-column: 1 / -1; grid-row: 1; justify-self: stretch; contain: inline-size; }
    .olm-far > .olm-fieldrow { grid-column: 1; grid-row: 2; }
    .olm-far > .olm-bartray { grid-column: 2; grid-row: 2; }
    .olm-near > .olm-fieldrow { grid-column: 1; grid-row: 1; }
    .olm-near > .olm-bartray { grid-column: 2; grid-row: 1; }
}

/* --- phones and small tablets: the phone spacing values -------------------- */

@media (max-width: 720px) {
    .olm-board { padding: 4px 6px; gap: 4px; margin: 4px auto 6px; --olm-gap: 4px; }
    .olm-side { gap: 3px; }
    .olm-mid { padding: 0; gap: 4px; }
}

/* --- cards: states and markers --------------------------------------------- */

/* Passive and blinded cards lie sideways. Not scoped to the board: a flight ghost
   keeps its state class and so keeps its turn. */
.card.olm-passive, .card.olm-blinded { rotate: 90deg; }
/* A blinded card is a back with a red ring, so the one-step untap from blinded to
   passive (a back turning into a face, still sideways) reads as progress. */
.card.olm-blinded { box-shadow: 0 0 0 2px #e0787a, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
/* A used Diamonds Drive stays upright and face up (ruling 26), dimmed. */
.card.olm-used { filter: saturate(0.45) brightness(0.8); }
.card.olm-totem { box-shadow: 0 0 0 2px rgba(255, 215, 106, 0.55), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
.card.olm-lit { outline: 3px solid var(--accent); outline-offset: 1px; cursor: pointer; }
.card.olm-sel { box-shadow: 0 0 0 3px #4fb3ff, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
.card.olm-picked { outline: 3px solid #7fd1ff; outline-offset: 2px; }

/* --- chips ----------------------------------------------------------------- */

/* Inset to the card's layout box, so its center is the card's center in either
   orientation and the chips sit at the middle of the painted card. */
.olm-chips {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    pointer-events: none;
}
/* Upright text on a sideways card. One rotation on the shared parent, so a
   blinded spirit's two chips stay stacked. */
.card.olm-passive > .olm-chips, .card.olm-blinded > .olm-chips { rotate: -90deg; }
.olm-chip {
    font-size: 11px;
    line-height: 1.2;
    white-space: nowrap;
    padding: 0 3px;
    border-radius: 4px;
    background: rgba(0, 0, 0, 0.7);
    color: #fff;
}
/* Ruling 57's mark rides inline at the end of the chip. Three classes, and later
   in gen-css's ORDER than platform.css, so it beats .eye-badge. */
.olm-chip > .eye-badge.olm-eye {
    position: static;
    display: inline-flex;
    vertical-align: -2px;
    width: 12px;
    height: 12px;
    margin-left: 2px;
}
.olm-chip > .eye-badge.olm-eye svg { width: 10px; height: 10px; }

/* --- the build line and the controls, between the board and the hand -------- */

/* The status line reserves its tallest ordinary wording: 2 lines, 3 on phones. */
.status.olm-status { min-height: calc(2 * 1.4em); }
.status.olm-status .olm-error { font-size: 13px; font-weight: 400; color: #ff9c8a; }
.progress.olm-progress { white-space: nowrap; }
/* The folded turn (the phone fold above). Above 560px the progress line carries
   these words, so the span stands down; the phone rule below swaps the pair. */
.status.olm-status > .olm-turn { display: none; }

/* The build line over the controls, in one box that reserves their tallest real
   combination (the header's RESERVATIONS). Each takes its own height from the
   top of the box and an empty one takes none, so the idle board's End button
   sits right under the board and the box's spare height is under the controls,
   over the hand. The box itself never changes height, so the hand never moves. */
.olm-under {
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    min-height: 108px;
    margin: 0 0 8px;
}
/* 4px between the two only while the build line has something in it. An empty
   build line stays displayed at no height, so its live region keeps announcing. */
.olm-under > .olm-buildline:not(:empty) { margin-bottom: 4px; }

/* What is being built (Requirement 28.3) and the Back control. The text is a
   column of up to 4 lines: the kind, then the combo, or up to 3 description
   lines (what an unfinished move still needs, item 6, and the short forms of
   the offered effect and sacrifice, item D), each cut with an ellipsis rather
   than widening the page; the column's title carries the whole text. */
.olm-buildline {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 10px;
    margin: 0;
}
.olm-buildtext {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-width: 0;
    flex: 0 1 auto;
    font-size: 12.5px;
    line-height: 15px;
    text-align: center;
}
.olm-buildkind, .olm-buildcombo {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.olm-buildkind { font-weight: 700; }
.olm-buildcombo { color: var(--accent); }
/* The description lines. def.ts gives each one its line budget: a short form
   beside another description takes exactly one line (each is at most 38
   characters, text.ts SHORT_MAX, which fits one line at 360px), and what is
   still needed (item 6) takes the lines left, up to all 3 when it stands alone. */
.olm-buildnote {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
    opacity: 0.92;
}
.olm-buildnote.olm-l3 { -webkit-line-clamp: 3; line-clamp: 3; max-height: 45px; }
.olm-buildnote.olm-l2 { -webkit-line-clamp: 2; line-clamp: 2; max-height: 30px; }
.olm-buildnote.olm-l1 {
    display: block;
    max-height: 15px;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The controls sit right under the build line. Every action button is at
   least 44px tall with an 18px label (item 2). The controls that do something
   take the site's filled accent; End phase takes a light fill. Both are filled
   and high contrast, so the thing to press reads as a button at a glance. */
.controls.olm-controls {
    min-height: 0;
    margin: 0;
    gap: 8px;
    align-items: flex-start;
    align-content: flex-start;
}
.olm-controls .btn, .olm-buildline .btn.olm-back {
    box-sizing: border-box;
    min-height: 44px;
    padding: 0 14px;
    font-size: 18px;
    font-weight: 600;
    line-height: 1.1;
    white-space: nowrap;
}
.olm-controls .btn.olm-ctl, .olm-controls .btn.olm-commit, .olm-controls .btn.olm-direct {
    background: var(--accent);
    color: var(--accent-ink);
    border-color: transparent;
}
.olm-controls .btn.olm-ctl:hover:not(:disabled),
.olm-controls .btn.olm-commit:hover:not(:disabled),
.olm-controls .btn.olm-direct:hover:not(:disabled) { background: #ffdf8e; }
.olm-controls .btn.olm-end {
    background: #ebe7df;
    color: #23261f;
    border-color: transparent;
}
.olm-controls .btn.olm-end:hover:not(:disabled) { background: #ffffff; }
.olm-buildline .btn.olm-back {
    flex: none;
    background: transparent;
    border: 2px solid rgba(255, 255, 255, 0.6);
}
.panel.olm-handpanel { padding-top: 8px; }

@media (max-width: 720px) {
    /* 561 to 720px: every decision's controls still fit one row, so the box is the desktop's 108px. */
    .olm-under { margin: 0 0 6px; }
}

/* --- the phone fold (see the header) --------------------------------------- */

/*
 * One block, so the whole fold reads in one place. It runs at the site's phone
 * breakpoint only: 561 to 720px keeps the spacing the polish run set, and 768px
 * and 1280px are untouched (1280 already fits from the page top).
 *
 * Where the 105px came from, measured at 390x844 in Chromium (98px was the
 * shortfall, so this leaves about 8px of slack):
 *   above the board      46.6px  the progress line (20px and its 8px margin),
 *                                the status line (75.6 -> 63), and 6px of the
 *                                10px of chrome padding over the status line
 *   the board            32px    its padding and row gaps, the bar trays'
 *                                padding-block, the field grid's gap, the
 *                                middle band's two rules, and the known
 *                                strip's scrollbar allowance
 *   the margins          6px     2px each under the board, the build line,
 *                                and the controls
 *   the controls         4px     a 4px row gap instead of 8px
 *   the hand panel head  16.8px  its padding-top, the title's margin and size,
 *                                and a smaller sort control
 * No card is smaller than the polish run set it, nothing is dropped, and every
 * one of these is a FIXED value, so Requirement 27.9's stillness holds.
 */
@media (max-width: 560px) {
    /*
     * The fold. 15px is the largest size at which the longest ordinary wording
     * (a Chain Attack's Queen window) still fits 3 lines with the turn in front
     * of it at 360px; 18px needs 4. The turn sits at normal weight, so the
     * action stays the bold part of the line.
     *
     * The negative margin reclaims 6px of #screen-game's 10px padding, which is
     * on every page and so cannot be scoped to one game; the status line's own
     * class can reach it. 4px of air is left under the 54px topbar, so nothing
     * overlaps it.
     */
    .status.olm-status { font-size: 15px; min-height: calc(3 * 1.4em); margin-top: -6px; }
    .status.olm-status > .olm-turn { display: inline; font-weight: 400; }
    .progress.olm-progress { display: none; }

    /* The board: spacing only. */
    .olm-board { margin: 0 auto 4px; padding: 1px 6px; gap: 2px; --olm-gap: 3px; }
    .olm-side { gap: 2px; }
    /* The one-row middle band (playtest round 3): the realm's label goes over the
       realm, on the pair labels' line, because the row has no margin left for a
       vertical one. The row's widths at 360px: 2 pairs of 69px, the 181px realm,
       and two 2px gaps, 323px inside the board's 326px content box. */
    .olm-mid { gap: 6px; align-items: flex-start; border-block: 0; background: rgba(0, 0, 0, 0.12); border-radius: 8px; }
    .olm-mid > .olm-realmrow { margin-inline: 0; flex-direction: column; align-items: center; gap: 1px; }
    .olm-realmcap {
        position: static;
        translate: none;
        rotate: none;
        writing-mode: horizontal-tb;
        margin: 0;
        line-height: 10px;
    }
    .olm-pilerow { gap: 2px; }
    /* The far bar's label sits under the far hand, whose cards are never lit, so its tray is
       tight. The near bar's label sits between two rows of cards that are lit as targets, and a
       lit card's ring reaches 4px past it, so that tray keeps 3px over the label and 4px under. */
    .olm-board .olm-far .olm-bartray { padding-block: 0; gap: 1px; }
    .olm-board .olm-near .olm-bartray { padding-block: 3px 0; }

    /* Under the board: the build line and the controls' box, which reserves the
       Jack's options here (the header's RESERVATIONS): three text lines beside
       Back over three controls on two 44px rows, at a 4px row gap. */
    .olm-under { min-height: 141px; margin: 0 0 4px; }
    .controls.olm-controls { row-gap: 4px; column-gap: 8px; }

    /* The hand panel's head, down to the sort control's own height. */
    .panel.olm-handpanel { padding-top: 2px; }
    .olm-handpanel .panel-head { min-height: 20px; }
    .olm-handpanel #hand-title { margin-bottom: 2px; font-size: 13px; }
    .olm-handpanel #hand-sort { padding: 2px 8px; font-size: 11px; }
}
@media (max-width: 380px) {
    .olm-controls .btn, .olm-buildline .btn.olm-back { padding-left: 12px; padding-right: 12px; }
    .olm-mid { gap: 2px; }
    .olm-pilerow { gap: 1px; }
    /* A K♦'s attack start: four text lines over "Diamonds effect" and "Sacrifice as a King",
       which no longer share a row at 360px. */
    .olm-under { min-height: 156px; }
}

/*
 * A short desktop screen (1366x768, 1280x800). Even with the board at its 42px
 * floor, 1366x768's span would be 5.6px over its height, so the spacing around
 * it tightens the way the phone fold does: the board's margins (4 and 8px to 2
 * and 4px), the box
 * under it (an 8px margin to 4px), and the hand panel's head (8px of padding to
 * 4px, and the title's 10px margin to 3px, which the head's 24px floor stops
 * at). That is 21px, and the board's reserve drops with it. It comes after
 * every rule it tightens, so it overrides them.
 */
@media (min-width: 721px) and (max-height: 820px) {
    .olm-board { --olm-reserve: 415px; margin: 2px auto 4px; }
    .olm-under { margin-bottom: 4px; }
    .panel.olm-handpanel { padding-top: 4px; }
    .olm-handpanel #hand-title { margin-bottom: 3px; }
}

/* --- the result summary ----------------------------------------------------- */

/* A real table with its own class, so its columns line up (ui-conventions.md,
   "Score tables line up by column"). */
.olm-result {
    border-collapse: collapse;
    margin: 0 auto;
}
.olm-result th, .olm-result td { padding: 4px 12px; }
.olm-result th { font-size: 13px; opacity: 0.85; border-bottom: 1px solid var(--panel-border); }
.olm-result .olm-corner, .olm-result .olm-label { text-align: left; }
.olm-result .olm-num { text-align: right; font-variant-numeric: tabular-nums; }
/* ---------- Grid Poker ----------
   Most of Grid Poker's rules are in platform.css ("Grid Poker's table", "Grid
   Poker's hand on a desktop", .gpk-result); these refine them, and are read
   after it. Ring Poker takes the same treatment in src/ringpoker/style.css. */

/* Beside the grid each opponent's panel is seven cards wide and three rows
   tall (a collection is at most 20), 236px instead of 326px, so it fits the
   left track at 1280px wide and the grid sits on the column's axis exactly.
   Same container test as the row itself (platform.css). */
@container (min-width: 640px) {
    .gpk-tablerow:has(> #board:not([hidden])) .opponent-cards .cardrow.mini {
        width: calc(7 * var(--occw) + 6 * 4px);
        min-height: calc(3 * var(--occw) * 89 / 64 + 2 * 4px);
    }
}

/* ONE ROW OF CARDS, at every width, not only on a desktop. The draft hand grows
   to 18 cards with 2 players and 20 with 3; on a phone, or with 20 cards at
   1280px, it wrapped onto a second and third row, and the fit then shrank the
   whole board mid-draft. def.ts sets --gpk-n to the card count. Cards keep the
   site size until the row is full, then narrow as more arrive, down to 40px;
   past that they overlap, each one covering the right of the one before and
   leaving its corner index showing, so the row never wraps. The row is
   reserved at one full-size card, the tallest it gets. */
.panel.grid-handpanel { --gpk-handcw: var(--cw); --gpk-gap: 6px; }
.panel.grid-handpanel #hand-cards {
    container-type: inline-size;
    flex-wrap: nowrap;
    gap: 0;
    min-height: calc(var(--gpk-handcw) * 89 / 64);
}
.panel.grid-handpanel #hand-cards > .card {
    flex: none;
    --cw: max(40px, min(var(--gpk-handcw),
        calc((100cqw - (var(--gpk-n, 1) - 1) * var(--gpk-gap)) / var(--gpk-n, 1))));
}
.panel.grid-handpanel #hand-cards > .card + .card {
    margin-left: min(var(--gpk-gap),
        calc((100cqw - var(--cw)) / max(1, var(--gpk-n, 1) - 1) - var(--cw)));
}
/* ---------- Ring Poker ----------
   The ring itself (.ring-board, .ring-slot, .pairbtn) is in platform.css; these
   are the rules that fit the whole board to a window, Grid Poker's treatment
   (platform.css, "Grid Poker's table", and src/gridpoker/style.css). */

/* The table: the opponents BESIDE the ring where the column has room for
   both, and above it (as before) where it does not. Stacked, a desktop board
   was the strip (139px) plus the ring (446px) tall, and the hand started below
   a 720px window. The test is the column's own width (.rpk-table is the
   container), not the window's, because the play column is narrower than the
   window by the log beside it.
   THE RING IS WHAT IS CENTERED, not the pair: it is what is being played, and
   the status line above it and the hand below it are on the column's axis.
   Three tracks, `1fr auto 1fr`, the ring in the middle one and the opponents
   right-aligned in the left one, in a column. Beside the ring each opponent's
   panel is seven cards wide and three rows tall (a collection is at most 18),
   236px, so it fits the left track at 1280px wide and the ring sits on the
   axis; where the track is narrower it grows to fit them and the ring moves
   right by that much. 668px is the panel, its gap, and the 420px ring. Only
   while the ring is shown: in the build it is hidden. */
.rpk-table { container-type: inline-size; }
@container (min-width: 668px) {
    .rpk-tablerow:has(> #board:not([hidden])) {
        display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
    }
    .rpk-tablerow:has(> #board:not([hidden])) > #opponents {
        grid-column: 1; justify-self: end;
        flex-direction: column; flex-wrap: nowrap; margin: 0 12px 0 0;
    }
    /* #board is an inline-size container on every page (src/duelpoker/
       style.css), which would give the auto track no width to size by. The
       ring is a fixed 420px square and reads no cqw, so it is sized by its
       content here. */
    .rpk-tablerow:has(> #board:not([hidden])) > #board { container-type: normal; grid-column: 2; }
    .rpk-tablerow:has(> #board:not([hidden])) .opponent-cards .cardrow.mini {
        width: calc(7 * var(--occw) + 6 * 4px);
        min-height: calc(3 * var(--occw) * 89 / 64 + 2 * 4px);
    }
}

/* ONE ROW OF CARDS, at every width: the player's own collection. It grows to 18
   cards, and wrapped onto a second and third row the panel grew a row at a
   time mid-draft, which pushed the hand off a 720px window. The tray is the
   container and def.ts sets --rpk-n to the card count. Cards keep the site
   size until the row is full, then narrow as more arrive, down to 40px; past
   that they overlap, each one covering the right of the one before and leaving
   its corner index showing, so the row never wraps. The row is reserved at one
   full-size card, the tallest it gets. */
.panel.ring-handpanel { --rpk-handcw: var(--cw); --rpk-gap: 6px; }
.panel.ring-handpanel #hand-cards {
    container-type: inline-size;
    flex-wrap: nowrap;
    gap: 0;
    min-height: calc(var(--rpk-handcw) * 89 / 64);
}
.panel.ring-handpanel #hand-cards > .card {
    flex: none;
    --cw: max(40px, min(var(--rpk-handcw),
        calc((100cqw - (var(--rpk-n, 1) - 1) * var(--rpk-gap)) / var(--rpk-n, 1))));
}
.panel.ring-handpanel #hand-cards > .card + .card {
    margin-left: min(var(--rpk-gap),
        calc((100cqw - var(--cw)) / max(1, var(--rpk-n, 1) - 1) - var(--cw)));
}
/* The build tray, one row on a desktop (Grid Poker's rule): sized for the whole
   collection, so its cards keep one size while they are placed. It stays a
   wrapping row below 951px, where every card in it has to stay big enough to
   tap. */
@media (min-width: 951px) {
    .panel.rpk-buildpanel { --rpk-handcw: var(--cw); }
    .panel.rpk-buildpanel #build-tray {
        container-type: inline-size;
        gap: 6px;
        min-height: calc(var(--rpk-handcw) * 89 / 64);
    }
    .panel.rpk-buildpanel #build-tray .card {
        --cw: max(40px, min(var(--rpk-handcw),
            calc((100cqw - (var(--rpk-n, 1) - 1) * 6px) / var(--rpk-n, 1))));
    }
}

/* The results, shown in the page at the end (def.ts finalSummary: 'inline'):
   as wide as the table and centered, the rules Grid Poker's .gpk-result takes
   (platform.css). Each hand column is one five-card row wide; where a
   three-player table is wider than the column the columns shrink together and
   each cell sizes its cards to fit. The stacked phone form keeps its shared
   layout. */
.panel.rpk-result:has(> .result-table:not(.result-stack)) {
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
}
.panel.rpk-result .result-table:not(.result-stack) .result-row {
    grid-template-columns: 70px;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, calc(5 * 46px + 4 * 4px));
}
.panel.rpk-result .result-table:not(.result-stack) .result-cell { container-type: inline-size; }
.panel.rpk-result .result-table:not(.result-stack) .result-cell .card.mini {
    --cw: min(46px, calc((100cqw - 4 * 4px) / 5));
}
/* ---------- In Response board ----------
   Prefix policy: every In Response class and id is inr-. Shared ids (hand-*,
   build-*, log-*, ...) keep their contract names and are scoped through the
   inr- classes the page puts on their panels. Cards are text tiles, in the
   Agents of War palette, with no image dependency. */
.inr-seat { margin: 6px 0; }
.inr-seat-head {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px;
    min-height: 32px;
}
.inr-seat-name {
    font-weight: 700; padding: 2px 8px; border-radius: 8px; border: 1px solid transparent;
    background: none; color: inherit; font: inherit; font-weight: 700; white-space: nowrap;
}
button.inr-seat-name { cursor: pointer; }
.inr-chip {
    background: #123a24; border: 1px solid #2c6e49; border-radius: 999px;
    padding: 2px 10px; font-size: 0.85rem; white-space: nowrap;
}
.inr-influence { background: #1d4d31; font-weight: 700; }
.inr-influence.inr-low { background: #5a1f1f; border-color: #b04a4a; }
.inr-seat-name.inr-active { color: #ffd76d; border-color: #d8a03d; }

/* THE TABLE is one grid (pages/inresponse.mjs). Wide: everything on the
   left, from the opponent's panel down to the hand, and the stack in a column
   on the right that runs the whole height, so a deep stack has room. Narrow:
   one column, the stack between the two Agent rows. #inr-board is
   display: contents, so its two Agent rows are grid items. */
.inr-table {
    display: grid; column-gap: 10px; align-items: start;
    grid-template-columns: minmax(0, 1fr) minmax(210px, 250px);
    grid-template-areas: "opp stack" "orow stack" "own stack" "you stack" "act stack" "hand stack";
}
.inr-board { display: contents; }
#inr-opp { grid-area: opp; }
#inr-opp-agents { grid-area: orow; }
#inr-own-agents { grid-area: own; }
#inr-you { grid-area: you; }
#inr-actions { grid-area: act; }
.inr-table > #hand-panel { grid-area: hand; }
#inr-opp-agents, #inr-own-agents {
    border: 1px solid #2c6e49; padding: 4px 8px 8px; margin: 4px 0;
}
#inr-opp-agents { border-radius: 12px 12px 0 0; border-bottom-style: dashed; margin-bottom: 0; }
#inr-own-agents { border-radius: 0 0 12px 12px; border-top: none; margin-top: 0; }
/* The stack column takes the height of the rows beside it and none of its
   own (contain: size), so its items scroll inside it rather than stretching
   the rows, and the column never moves anything on the left. */
.inr-stack-col {
    grid-area: stack; align-self: stretch; contain: size; min-height: 360px;
    display: flex; flex-direction: column; margin: 6px 0;
}
.inr-stack-col > .inr-stack { flex: 1 1 auto; min-height: 0; }
.inr-row-label { font-size: 0.8rem; opacity: 0.75; margin: 2px 0 4px; text-align: center; white-space: nowrap; }
.inr-agents {
    display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; align-items: stretch;
    min-height: 112px;
}
.inr-empty { opacity: 0.5; font-size: 0.9rem; padding: 18px 6px; align-self: center; }

.inr-stack {
    display: flex; flex-direction: column; min-width: 0;
    border: 1px solid #4a5b8c; border-radius: 10px; background: #12172a; padding: 6px;
}
.inr-stack-head { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; margin-bottom: 4px; }
.inr-stack-title { font-weight: 700; white-space: nowrap; }
.inr-stack-note { font-size: 0.78rem; opacity: 0.7; white-space: nowrap; }
/* The column's height, so a stack taller than it scrolls inside it. */
.inr-stack-items {
    display: flex; flex-direction: column; gap: 6px; overflow-y: auto;
    flex: 1 1 auto; min-height: 0;
}
.inr-recent { font-size: 0.8rem; opacity: 0.65; line-height: 1.3; }

/* Tiles: Agents in play, stack items, and hand cards share one look. */
.inr-agent, .inr-item, .inr-card {
    display: flex; flex-direction: column; gap: 2px; text-align: left;
    background: #0f2f1d; color: inherit; border: 1px solid #2c6e49;
    border-radius: 10px; padding: 6px 8px; font: inherit; position: relative;
}
button.inr-agent, button.inr-item, button.inr-card { cursor: pointer; }
.inr-agent { width: 150px; }
.inr-card { width: 168px; }
.inr-tile-head { display: flex; align-items: baseline; gap: 6px; }
.inr-tile-head .inr-name { flex: 1 1 auto; }
.inr-name { font-weight: 700; font-size: 0.92rem; }
.inr-power {
    background: #1d4d31; border: 1px solid #7fd08f88; border-radius: 999px;
    min-width: 1.5em; text-align: center; padding: 0 5px; font-weight: 700; font-size: 0.9rem;
}
.inr-power.inr-up { background: #2f6b1f; border-color: #b6f08f; }
.inr-power.inr-down { background: #6b1f1f; border-color: #f08f8f; }
.inr-typeline { font-size: 0.78rem; opacity: 0.9; font-style: italic; }
.inr-text { font-size: 0.8rem; line-height: 1.3; }
.inr-flags { display: flex; flex-wrap: wrap; gap: 4px; font-size: 0.74rem; }
.inr-flags:empty { display: none; }
.inr-flag {
    background: #14381f; border: 1px solid #2c6e49; border-radius: 6px;
    padding: 0 5px; line-height: 1.4; white-space: nowrap;
}
.inr-flag-hurt { background: #5a1f1f; border-color: #b04a4a; }
.inr-flag-attack { background: #4a3b10; border-color: #d8a03d; }
.inr-flag-block { background: #1b3550; border-color: #6fa8dc; }
.inr-flag-aim { background: #3a1f4a; border-color: #a77fd0; }
.inr-flag-sym { font-size: 0.95rem; line-height: 1.2; padding: 0 4px; }

/* THE THREE KINDS, the same in the hand and on the stack: an Agent is green,
   an Order blue, and a Warcry amber with a dashed edge. The color is the
   fill, the top edge, and the badge, so it reads at a glance and still lets
   the state rings (.inr-lit, .inr-picked, .inr-aimed) draw on top. */
.inr-kindtile-agent { background: #0f2f1d; border-color: #3f8f5f; border-top: 4px solid #5fbf7f; }
.inr-kindtile-order { background: #151d38; border-color: #4f66a8; border-top: 4px solid #7f9fef; }
.inr-kindtile-warcry {
    background: #2e2410; border-color: #a8802f; border-style: dashed; border-top: 4px solid #e0a83d;
}
.inr-kind {
    display: inline-block; border-radius: 4px; padding: 0 5px; margin-right: 4px;
    font-size: 0.68rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase;
    font-style: normal; color: #0b140e;
}
.inr-kind-agent { background: #5fbf7f; }
.inr-kind-order { background: #7f9fef; }
.inr-kind-warcry { background: #e0a83d; }
.inr-kw { margin-right: 6px; white-space: nowrap; }
.inr-covert { background-image: linear-gradient(rgba(0, 0, 0, 0.16), rgba(0, 0, 0, 0.16)); }
.inr-copy { opacity: 0.88; }
/* Whose item it is: a stripe on the left, green for the bottom seat. */
.inr-item.inr-mine { border-left: 4px solid #7fd08f; }
.inr-item.inr-theirs { border-left: 4px solid #e07a7a; }
.inr-item-top { box-shadow: 0 0 0 2px #ffd76d88; }
.inr-item-who { font-size: 0.76rem; opacity: 0.85; }
.inr-item-aim { font-size: 0.8rem; font-weight: 700; }

/* Board state. The tilt is the independent `rotate`, never `transform`. */
.inr-exhausted { rotate: 3deg; opacity: 0.72; }
.inr-attacking { border-color: #d8a03d; box-shadow: 0 0 0 2px #d8a03d55; }
.inr-blocking { border-color: #6fa8dc; box-shadow: 0 0 0 2px #6fa8dc55; }
.inr-agent.inr-lit, .inr-item.inr-lit, .inr-card.inr-lit, .inr-seat-name.inr-lit {
    border-color: #7fd08f; box-shadow: 0 0 0 2px #7fd08f66;
}
.inr-agent.inr-picked, .inr-card.inr-picked {
    border-color: #ffd76d; box-shadow: 0 0 0 3px #ffd76d88; background-color: #14301f;
}
/* A ring recolors the sides, never the kind's top edge. */
.inr-kindtile-agent.inr-lit, .inr-kindtile-agent.inr-picked { border-top-color: #5fbf7f; }
.inr-kindtile-order.inr-lit, .inr-kindtile-order.inr-picked { border-top-color: #7f9fef; }
.inr-kindtile-warcry.inr-lit { border-top-color: #e0a83d; }
/* A player some stack item targets. */
.inr-seat-name.inr-targeted { border-color: #a77fd0; background: #3a1f4a; }
button.inr-agent:hover, button.inr-item:hover, button.inr-card:hover { filter: brightness(1.12); }
/* Pointing at a tile rings what it is linked to (def.ts linkHover): its
   targets, or the items aimed at it. An outline, so it never replaces the
   box-shadow state rings. */
.inr-aimed { outline: 3px solid #d7b4ff; outline-offset: 2px; }
.inr-aiming { outline: 2px dashed #d7b4ff; outline-offset: 2px; }
.inr-item:focus-visible, .inr-agent:focus-visible, .inr-seat-name:focus-visible {
    outline: 2px dashed #d7b4ff; outline-offset: 2px;
}

/* The action bar sits between your seat and your hand, at a reserved height,
   so the hand does not move as the buttons change. */
.inr-actions {
    display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 8px;
    min-height: 44px; margin: 6px 0;
}
.inr-actnote, .inr-assign-count { font-size: 0.85rem; opacity: 0.8; white-space: nowrap; }

.inr-handpanel #hand-cards { justify-content: center; align-items: stretch; gap: 8px; }
/* Seven cards in hand: four to a row beside the stack column on a wide
   screen, three to a row on a phone, so the opening hand takes two or three
   rows rather than three or four. */
.inr-handpanel .inr-card { width: 148px; }

/* Log lines that put something on the stack, and Influence changes. */
#log-entries .inr-log-stack { color: #c8d4ff; }
#log-entries .inr-log-influence { font-weight: 700; }

@media (max-width: 720px) {
    .inr-table {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "opp" "orow" "stack" "own" "you" "act" "hand";
    }
    /* Between the rows the stack is a strip of its own height again. */
    .inr-stack-col { contain: none; min-height: 0; margin: 0; }
    #inr-opp-agents { border-radius: 12px 12px 0 0; border-bottom: none; }
    #inr-own-agents { border-radius: 0 0 12px 12px; border-top: none; }
    .inr-stack-col > .inr-stack { border-radius: 0; }
    /* A scrolling strip stays left-aligned (ui-conventions.md). */
    .inr-stack-items {
        flex-direction: row; overflow-x: auto; overflow-y: hidden; height: auto; min-height: 96px;
        align-items: stretch;
    }
    .inr-stack-items > .inr-item { flex: 0 0 168px; }
    .inr-agent { width: 128px; }
    .inr-card { width: 140px; }
    .inr-handpanel .inr-card { width: auto; flex: 0 0 calc((100% - 16px) / 3); min-width: 0; padding: 5px 6px; }
    .inr-handpanel .inr-card .inr-text { font-size: 0.74rem; }
    .inr-handpanel .inr-card .inr-name { font-size: 0.84rem; }
    .inr-agents { min-height: 100px; }
    .inr-chip { font-size: 0.78rem; padding: 2px 7px; }
}

/* ---------- the discard pile ---------- */
/* The Discard chip is a button: the pile-chip look, plus a pointer. */
.inr-discard-btn { font-family: inherit; font-size: 13px; line-height: inherit; color: inherit; cursor: pointer; }
.inr-discard-btn:hover { filter: brightness(1.15); }
/* The popover the chip opens: a centered panel over the board. */
.inr-discard-view {
    max-width: min(880px, 94vw); max-height: 80vh; overflow: auto; padding: 12px 14px;
    border: 1px solid #4f66a8; border-radius: 12px; background: #0e1f16; color: inherit;
}
.inr-discard-view::backdrop { background: rgba(0, 0, 0, 0.45); }
.inr-dv-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.inr-dv-title { margin: 0; font-size: 1.05rem; }
.inr-dv-note, .inr-dv-empty { font-size: 0.85rem; opacity: 0.8; margin: 6px 0 10px; }
.inr-dv-cards { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.inr-card-small { width: 150px; font-size: 0.92rem; }

/* ---------- arrows from stack items to their targets ---------- */
.inr-arrows { position: fixed; inset: 0; pointer-events: none; z-index: 4; }
.inr-arrow-svg { display: block; overflow: visible; }
.inr-arrow { opacity: 0.55; }
.inr-arrow .inr-arrow-line { fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-dasharray: 7 5; }
.inr-arrow-top { opacity: 0.9; }
.inr-arrow-top .inr-arrow-line { stroke-dasharray: none; stroke-width: 3; }
.inr-arrow-mine .inr-arrow-line { stroke: #7fd08f; }
.inr-arrow-mine .inr-arrow-head { fill: #7fd08f; }
.inr-arrow-theirs .inr-arrow-line { stroke: #f08a8a; }
.inr-arrow-theirs .inr-arrow-head { fill: #f08a8a; }
.inr-arrow-hot { opacity: 1; filter: drop-shadow(0 0 4px #d7b4ff); }
.inr-arrow-hot .inr-arrow-line { stroke-width: 4; }

/* ---------- Influence changes ---------- */
.inr-influence { position: relative; }
.inr-inf-num { font-variant-numeric: tabular-nums; }
.inr-inf-hit { animation: inr-inf-hit 900ms ease-out both; }
@keyframes inr-inf-hit {
    0% { background: #b02a2a; border-color: #ff8a8a; translate: 0 0; }
    10% { translate: -3px 0; }
    20% { translate: 3px 0; }
    30% { translate: -2px 0; }
    40% { translate: 2px 0; }
    50% { translate: 0 0; }
    100% { translate: 0 0; }
}
.inr-float {
    position: fixed; z-index: 450; pointer-events: none; font-weight: 800; font-size: 1.15rem;
    animation: inr-float 1200ms ease-out forwards; text-shadow: 0 1px 3px #000;
}
.inr-float-loss { color: #ff8a8a; }
.inr-float-gain { color: #9cf0b0; }
@keyframes inr-float {
    0% { opacity: 0; translate: 0 6px; }
    15% { opacity: 1; translate: 0 0; }
    100% { opacity: 0; translate: 0 -34px; }
}
@media (prefers-reduced-motion: reduce) {
    .inr-inf-hit, .inr-float { animation: none; }
}

/* ---------- card names in the log ---------- */
/* A card NAME in a log line: quiet prose that is pressable, since hover does
   not exist on a touch screen (the Agents of War pattern). The preview is
   placed against the sticky #log-panel, its nearest positioned ancestor: the
   scrolling #log-entries must not become one, or it would clip the preview. */
.inr-log-card {
    display: inline; padding: 0; margin: 0; border: 0; border-radius: 0;
    background: none; box-shadow: none; color: inherit; font: inherit;
    text-decoration: underline dotted rgba(255, 255, 255, 0.4); text-underline-offset: 2px; cursor: help;
}
.inr-log-card:hover, .inr-log-card:focus-visible { text-decoration-color: currentColor; }
.inr-peek {
    display: none; position: absolute; left: 0; bottom: calc(100% + 6px);
    z-index: 40; pointer-events: none; filter: drop-shadow(0 4px 12px #000a);
}
.inr-log-card:hover > .inr-peek, .inr-log-card:focus-visible > .inr-peek,
.inr-log-card.inr-peek-open > .inr-peek { display: block; }
.inr-peek .inr-card { width: 190px; font-size: 0.92rem; font-style: normal; font-weight: normal; text-align: left; }
/* On a wide layout the log is a sidebar at the top right, where an upward
   preview runs off the screen, so there it opens to the LEFT of the panel. */
@media (min-width: 951px) {
    .inr-log-card > .inr-peek { left: auto; right: calc(100% + 8px); bottom: auto; top: 0; }
}
/* =========================================================================
   Agony Aunt (agonyaunt)
   A trick avoider played on a 3x3 penalty board. design.md D3 to D9 are the
   source for every figure below.
     the table as a table: West, North and East around the trick cross, this
       player South, and the dump card and the penalty board beside them
       (.agy-wrap, .agy-board, .agy-table, .agy-trick, .agy-side)
     a small panel per seat in its seat color (.agy-seat, .agy-disc)
     the penalty board: nine cream squares with drawn art, every counter on
       its owner's side of its square, and the lines of three at the deal end
       (.agy-penalty, .agy-sq, .agy-ctr, .agy-line)
     the Joker badged with the card it stands for (.agy-jbadge), and a soft
       glow on every penalty card (.agy-hot, .agy-warm)
     one explanation balloon for the whole board (.agy-explain)
     the deal summary and the final summary (.agy-sum, .agy-sumtable,
       .agy-gametable, .agy-pic)
   LAYOUT KEYS ON THE BOARD'S OWN WIDTH. Above 950px the play area sits beside
   the 300px log column, so between 951px and about 1230px it is narrower than
   the single column at a 900px window. A viewport breakpoint cannot describe
   that, so .agy-wrap is a size container and the three layouts are container
   queries on it (Let Me Off's lesson). The hand panel sits outside the
   wrapper, so its card size keys on the viewport.
   ANTI-JITTER. Every box above the hand is a fixed shape: the panels are a
   fixed width with two reserved lines, the cross is the same size empty or
   full with its marker row always present, the side column reserves the
   board's height, the action row reserves one 44px button, and the status
   line reserves its tallest line count at each width.
   Shared ids are scoped through a class this game owns (.agy-status,
   .agy-progress, .agy-handpanel, .agy-sum), never named bare, because those
   ids are on every page. No containment on #board or #hand-panel.
   ========================================================================= */

/* --- colors ------------------------------------------------------------- */
/* The four seat colors, fixed for the whole game (D6), and the inks for the
   cream surfaces (D4). Defined on every root a seat color or a board square
   can appear under: the table, the summary panel, every board picture (the
   log's sits in #log-panel), the log's score tables, and its trick pictures. */
.agy-wrap, .agy-sum, .agy-pic, .agy-logtable, .agy-logx, .agy-explain {
    --agy-c0: #2f6fd6;
    --agy-c1: #d23b3b;
    --agy-c2: #2e9a4a;
    --agy-c3: #f2c230;
    /* The light-surface inks, measured on #f4efe1, #f5f2ec and #efe7d2 (all
       4.8:1 or better). The site's suit inks are tuned for the felt. */
    --agy-ink: #1c1c1c;
    --agy-ink-red: #b3202a;
    --agy-ink-diamond: #b13f12;
    --agy-ink-club: #1d4f91;
    --agy-cream: #f4efe1;
}
/* --agy-seat-ink is the text color ON a seat-color fill (the + counter's
   number, F2): white on blue (4.81:1) and red (4.74:1), near-black on green
   (4.74:1) and yellow (10.17:1). */
.agy-s0 { --agy-seat: var(--agy-c0); --agy-seat-ink: #ffffff; }
.agy-s1 { --agy-seat: var(--agy-c1); --agy-seat-ink: #ffffff; }
.agy-s2 { --agy-seat: var(--agy-c2); --agy-seat-ink: #1c1c1c; }
.agy-s3 { --agy-seat: var(--agy-c3); --agy-seat-ink: #1c1c1c; }

/* Suit glyphs on the felt: colored wherever they appear, because suits decide
   the game. `#log-entries .red` and its companions already cover the log. */
.agy-status .red, .agy-progress .red, .agy-sum-notes .red, .agy-dump-face .red,
.agy-sum-title .red, .agy-sum .winner-banner .red { color: var(--red); }
.agy-status .red.diamond, .agy-progress .red.diamond, .agy-sum-notes .red.diamond,
.agy-dump-face .red.diamond, .agy-sum-title .red.diamond,
.agy-sum .winner-banner .red.diamond { color: var(--red-diamond); }
.agy-status .club, .agy-progress .club, .agy-sum-notes .club, .agy-dump-face .club,
.agy-sum-title .club, .agy-sum .winner-banner .club { color: var(--blue-club); }
/* Suit glyphs on cream: the squares (live and the summary copy) and the
   balloon. A bare glyph (spades) inherits --agy-ink. */
.agy-sq .red, .agy-explain .red { color: var(--agy-ink-red); }
.agy-sq .red.diamond, .agy-explain .red.diamond { color: var(--agy-ink-diamond); }
.agy-sq .club, .agy-explain .club { color: var(--agy-ink-club); }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor :is(.agy-wrap, .agy-sum, .agy-pic, .agy-logtable, .agy-logx, .agy-explain) {
    --agy-ink-diamond: var(--fc-diamond-ink);
    --agy-ink-club: var(--fc-club-ink);
}

/* --- the status line and the progress line ----------------------------- */
/* Two lines reserved: the action wanted, and a hint when one is asked for.
   The phone block reserves the three a pass hint wraps to. */
.status.agy-status { line-height: 1.3; min-height: 2.6em; }
/* LIT whenever this device may act. */
.status.agy-status.agy-turn { color: var(--accent); }
.agy-hintnote { opacity: 0.85; font-weight: 400; }
/* One nowrap line: the deal chip and the dumpth trick. The ellipsis is a net
   so a font wider than measured clips its tail rather than widening the page. */
.progress.agy-progress {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    line-height: 20px;
}
.agy-dealchip {
    display: inline-block;
    margin-right: 10px;
    padding: 0 8px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    line-height: 18px;
}
.agy-dumpth { display: inline; }

/* --- the board: the table as a table ----------------------------------- */
.agy-wrap { container: agy / inline-size; }
/* ONE GRID for the whole table. #agy-table is `display: contents`, so the
   other seats' panels are cells of it and take the areas their seat sits in.
   The MIDDLE layout (a board 640px to 899px wide) is the default: the dump
   card and the penalty board in a row above the table. */
.agy-board {
    --agy-playcw: 56px;
    --agy-sq: clamp(56px, min(12cqi, 11vh), 88px);
    --agy-sqgap: 4px;
    --agy-stripcw: 20px;
    display: grid;
    grid-template-columns: auto auto auto;
    grid-template-areas:
        "side side  side"
        ".    up    ."
        "left trick right"
        ".    down  .";
    justify-content: center;
    align-items: center;
    gap: 8px 12px;
    margin-bottom: 4px;
}
@supports (width: round(down, 1.5px, 1px)) {
    .agy-board { --agy-sq: round(down, clamp(56px, min(12cqi, 11vh), 88px), 1px); }
}
.agy-table { display: contents; }
.agy-seat.pos-left { grid-area: left; justify-self: end; }
.agy-seat.pos-right { grid-area: right; justify-self: start; }
.agy-seat.pos-up { grid-area: up; justify-self: center; }
.agy-seat.pos-down { grid-area: down; justify-self: center; }

/* --- a seat's panel ----------------------------------------------------- */
/* SMALL, the same for every seat, a FIXED width so a long name or a growing
   strip cannot resize the grid, and two reserved lines (D6). The top stripe
   and the disc are the seat's color. */
.agy-seat {
    box-sizing: border-box;
    width: 168px;
    max-width: 100%;
    min-width: 0;
    padding: 6px 9px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    box-shadow: inset 0 3px 0 var(--agy-seat);
    /* Transparent by default, so the to-act ring never shifts the layout. */
    outline: 2px solid transparent;
}
.agy-seat.agy-active { outline-color: var(--accent); }
.agy-seat-head {
    display: flex;
    align-items: center;
    gap: 5px;
    height: 18px;
    white-space: nowrap;
}
.agy-disc {
    display: inline-block;
    flex: none;
    width: 12px;
    height: 12px;
    box-sizing: border-box;
    border-radius: 50%;
    background: var(--agy-seat);
    border: 2px solid #1c1c1c;
    vertical-align: -1px;
}
.agy-disc.agy-disc-sm { width: 9px; height: 9px; border-width: 1.5px; }
.agy-name {
    flex: 1 1 auto;
    min-width: 0;
    font-weight: 700;
    font-size: 13px;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Visually hidden, read by assistive technology ("12 counters left"). */
.agy-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}
.agy-left {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    flex: none;
    font-size: 15px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
/* Every board trigger drops the UA button look (C5): the site has no global
   button reset, so a full-cover face would otherwise paint ButtonFace over the
   cream and break the ink the art inherits. The chips set their own
   backgrounds after this rule. */
.agy-sq-face, .agy-ctr, .agy-line, .agy-none, .agy-passed, .agy-dealer, .agy-dump-face {
    appearance: none;
    -webkit-appearance: none;
    margin: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    cursor: pointer;
}
/* The dealer chip (P15): a round 16px "D", with a wider invisible tap area. */
.agy-dealer {
    position: relative;
    flex: none;
    width: 16px;
    height: 16px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--accent);
    color: var(--accent-ink);
    font-size: 10px;
    font-weight: 800;
    line-height: 16px;
    text-align: center;
}
.agy-dealer::after { content: ''; position: absolute; inset: -8px; }
.agy-seat-line {
    display: flex;
    align-items: center;
    gap: 6px;
    height: calc(var(--agy-stripcw) * 89 / 64 + 4px);
    margin-top: 4px;
}
/* Line 2 (playtest round 1): the "no counter yet" marker's slot, then, in
   the panel of the seat the viewer passed to, the three cards passed. The
   tricks chip and the dump cards taken are gone (the + square counts the
   tricks, F2). The line keeps its fixed height either way, so a panel is one
   shape all game. */
/* The "no counter yet" marker (F4): a hollow ring in the seat's color between
   two light rings, which carry the contrast on the felt in every seat color.
   Its slot is reserved: off, it is an invisible span of the same box. */
.agy-none {
    position: relative;
    flex: none;
    box-sizing: border-box;
    width: 13px;
    height: 13px;
    padding: 0;
    border: 2.5px solid var(--agy-seat);
    border-radius: 50%;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.78), inset 0 0 0 1px rgba(255, 255, 255, 0.78);
}
.agy-none.agy-none-off { visibility: hidden; cursor: default; }
.agy-none::after { content: ''; position: absolute; inset: -8px; }
/* The no-tricks form (D5, from trick 7, a seat on no trick below 17): a dark
   band and a gold outer ring, a second ring and not only a color. */
.agy-none.agy-zero {
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.78), inset 0 0 0 1px rgba(255, 255, 255, 0.78),
        0 0 0 3px #1c1c1c, 0 0 0 5px var(--accent);
}
/* The cards the viewer passed (F3): three 20px simplified cards, the button
   their explanation opens from. A card that has been played stays in place,
   faded with a filter (never opacity), so the strip is one size all deal. */
.agy-passed {
    position: relative;
    display: flex;
    flex: none;
    flex-wrap: nowrap;
    align-items: center;
    gap: 3px;
    height: 100%;
    padding: 0 2px;
    border: 0;
    border-radius: 4px;
}
.agy-passed .card { --cw: var(--agy-stripcw); }
.agy-passed .card.agy-gone { filter: grayscale(0.9) brightness(0.55); }

/* --- the trick: a cross -------------------------------------------------- */
/* Each seat's slot on the side that seat sits, tinted lightly with its color
   (U6), the marker row in a FIXED middle column so its text never resizes
   anything. Same size empty or full. */
.agy-trick {
    grid-area: trick;
    justify-self: center;
    --agy-markw: max(112px, calc(2 * var(--agy-playcw)));
    display: grid;
    grid-template-columns: auto var(--agy-markw) auto;
    grid-template-rows: auto 18px auto;
    grid-template-areas:
        "left up   right"
        "left mark right"
        "left down right";
    align-items: center;
    justify-items: center;
    gap: 2px 8px;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.agy-slot {
    display: flex;
    padding: 3px;
    border-radius: 8px;
    background: color-mix(in srgb, var(--agy-seat) 16%, transparent);
}
.agy-slot.pos-up { grid-area: up; }
.agy-slot.pos-down { grid-area: down; }
.agy-slot.pos-left { grid-area: left; }
.agy-slot.pos-right { grid-area: right; }
.agy-slot .card { --cw: var(--agy-playcw); }
/* A slot still owed a card: outlined, not blank, so it reads as "waiting". */
.agy-slot .card.empty { border-style: dashed; opacity: 0.4; }
.agy-slot.agy-onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
/* The card taking the trick, or that took the held trick: a gold OUTLINE only,
   so it never clashes with the penalty glow (a box-shadow) or the lift. */
.agy-slot .card.agy-winning { outline: 3px solid var(--accent); outline-offset: 2px; }
.agy-mark {
    grid-area: mark;
    max-width: 100%;
    overflow: hidden;
    white-space: nowrap;
    font-size: 11px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
}
.agy-mark.agy-mark-on { color: var(--accent); }

/* --- cards: the penalty glow and the Joker's badge ---------------------- */
/* One treatment in two steps, a soft rose glow COMPOSED with the base drop
   shadow .card carries, so a highlighted card keeps its shadow (D5). */
.card.agy-hot {
    box-shadow: 0 0 7px 2px rgba(255, 92, 128, 0.8), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.card.agy-warm {
    box-shadow: 0 0 5px 1px rgba(255, 92, 128, 0.45), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
/* The badge's containing block, at ZERO specificity: a flight ghost is a clone
   carrying this class, and `.card-fly`'s `position: fixed` (one class) must
   still win over it. */
:where(.agy-joker) { position: relative; }
/* The card the Joker stands for (U5, D7), on a dark pill where the felt inks
   read, at a fixed size so a flight ghost does not swell it. */
.agy-jbadge {
    position: absolute;
    left: 3%;
    top: 40%;
    padding: 0 3px;
    border-radius: 5px;
    background: #1c1c1c;
    color: #f5f2ec;
    font-size: 12px;
    font-weight: 800;
    line-height: 1.3;
    white-space: nowrap;
    pointer-events: none;
}
.agy-jbadge .red { color: var(--red); }
.agy-jbadge .red.diamond { color: var(--red-diamond); }
.agy-jbadge .club { color: var(--blue-club); }
/* The 20px cards, the smallest the game draws (C3): the passed strip and the
   log's trick pictures. The badge is a full-width foot tab small enough that
   "10♦" fits, and the glows shrink so they do not run into the next card. */
.agy-passed .agy-jbadge, .agy-logx .agy-jbadge {
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    /* No side padding, and -0.5px spacing: measured in Chromium (agy-probe
       part 1), "10♦" at -0.3px with 1px padding needed 21px of the 20. */
    padding: 0;
    border-radius: 0 0 3px 3px;
    font-size: 9px;
    line-height: 11px;
    letter-spacing: -0.5px;
    text-align: center;
    overflow: hidden;
}
.agy-passed .card.agy-hot {
    box-shadow: 0 0 2px 1px rgba(255, 92, 128, 0.85), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.agy-passed .card.agy-warm {
    box-shadow: 0 0 2px 1px rgba(255, 92, 128, 0.5), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
/* In a log picture the glow IS the penalty mark (F6), so it is a step
   stronger than the strip's; the cross's gaps leave it room. Both tiers
   keep a 2px spread with a short blur, so the rose band is solid in the 2px
   between a winning card and its ring (below); the two steps differ by
   strength. */
.agy-logx .card.agy-hot {
    box-shadow: 0 0 2px 2px rgba(255, 92, 128, 0.95), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.agy-logx .card.agy-warm {
    box-shadow: 0 0 2px 2px rgba(255, 92, 128, 0.6), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}

/* --- the side: the dump card and the penalty board ---------------------- */
/* The side reserves the board's height in every layout, so a deal's first
   paint and its last are the same size. */
.agy-side {
    grid-area: side;
    justify-self: center;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: calc(3 * var(--agy-sq) + 2 * var(--agy-sqgap));
}
.agy-dump {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 8px;
}
/* The trick slots' width, except on phones, where --agy-dumpcw keeps this
   read-only full-art card above the 46px small-card band (D7). */
.agy-dump .card { --cw: var(--agy-dumpcw, var(--agy-playcw)); }
/* The dump card's caption, its explanation trigger (C9). */
.agy-dump-face {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding: 2px 4px;
    border: 0;
    border-radius: 6px;
    font-size: 12px;
    line-height: 15px;
    white-space: nowrap;
    text-align: left;
}
.agy-dump-title { font-weight: 700; }

/* --- the penalty board --------------------------------------------------- */
/* Nine fixed squares in rulebook order. `isolation` makes the board one
   stacking context, so the face (1), the line overlay (2) and the counters (3)
   stack across squares. */
.agy-penalty, .agy-pic-summary {
    --agy-bsz: calc(3 * var(--agy-sq) + 2 * var(--agy-sqgap));
    position: relative;
    isolation: isolate;
    display: grid;
    grid-template-columns: repeat(3, var(--agy-sq));
    grid-template-rows: repeat(3, var(--agy-sq));
    gap: var(--agy-sqgap);
}
/* One square: everything sized from --agy-sq, so the live board in all three
   layouts and the summary's copy follow one rule (D4). */
.agy-sq {
    position: relative;
    width: var(--agy-sq);
    height: var(--agy-sq);
    border-radius: 6px;
    background: var(--agy-cream);
    color: var(--agy-ink);
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
    /* A counter's hit area, its visible disc (ring included), and how far a
       disc reaches in from its edge. */
    --agy-ctr: max(24px, calc(var(--agy-sq) * 0.3));
    --agy-dot: max(9px, calc(var(--agy-sq) * 0.2));
    --agy-band: calc(var(--agy-sq) * 0.04 + var(--agy-dot));
}
/* The face covers the whole square and takes every tap a counter or a bar
   does not; only its ART sits in the middle, clear of every counter band. */
.agy-sq-face {
    position: absolute;
    inset: 0;
    z-index: 1;
    padding: 0;
    border: 0;
    border-radius: inherit;
}
.agy-sq-art {
    position: absolute;
    inset: max(25%, calc(var(--agy-band) + 1px));
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    pointer-events: none;
    line-height: 1;
    white-space: nowrap;
}
.agy-sq-sym { font-size: calc(var(--agy-sq) * 0.27); font-weight: 700; }
.agy-sq-sym svg {
    display: block;
    width: calc(var(--agy-sq) * 0.27);
    height: calc(var(--agy-sq) * 0.27);
}
.agy-sq-fact { font-size: calc(var(--agy-sq) * 0.17); font-weight: 800; }
/* The frames never share a property, so two cues on one square never replace
   each other. The Aunt's squares read as a set: an accent surface and frame. */
.agy-sq.agy-aunt-set { background: #efe7d2; box-shadow: inset 0 0 0 2px #8a5a2e; }
/* The dumpth or last trick is in the cross (only ever on # and Ω). */
.agy-sq.agy-now { box-shadow: inset 0 0 0 3px #c9a227; }
/* A seat still holds every counter placed (D5): empty squares dashed in its
   color. The dashes lie on a solid #1c1c1c band of the same size, so the gaps
   are dark and the dashes read in every seat color (yellow on cream alone is
   about 1.4:1). Pseudo-elements, because agy-aunt-set and agy-now own
   box-shadow and an outline's painting order against the face is not
   guaranteed. */
.agy-sq.agy-allnine-slot::before,
.agy-sq.agy-allnine-slot::after {
    content: '';
    position: absolute;
    inset: 2px;
    z-index: 2;
    border-radius: 4px;
    pointer-events: none;
}
.agy-sq.agy-allnine-slot::before { border: 2px solid #1c1c1c; }
.agy-sq.agy-allnine-slot::after { border: 2px dashed var(--agy-live); }
/* A counter: a transparent hit area FLUSH against its owner's side, offsets
   only and no transform at rest, so the arrival animation owns `translate`.
   Its visible disc is the ::before, on the button's outer side. */
.agy-ctr {
    position: absolute;
    z-index: 3;
    width: var(--agy-ctr);
    height: var(--agy-ctr);
    padding: 0;
    border: 0;
}
.agy-ctr::before {
    content: '';
    position: absolute;
    box-sizing: border-box;
    width: var(--agy-dot);
    height: var(--agy-dot);
    border-radius: 50%;
    background: var(--agy-seat);
    border: 2px solid #1c1c1c;
}
.agy-ctr.pos-down { left: calc(50% - var(--agy-ctr) / 2); bottom: 0; }
.agy-ctr.pos-up { left: calc(50% - var(--agy-ctr) / 2); top: 0; }
.agy-ctr.pos-left { top: calc(50% - var(--agy-ctr) / 2); left: 0; }
.agy-ctr.pos-right { top: calc(50% - var(--agy-ctr) / 2); right: 0; }
.agy-ctr.pos-down::before { left: calc(50% - var(--agy-dot) / 2); bottom: calc(var(--agy-sq) * 0.04); }
.agy-ctr.pos-up::before { left: calc(50% - var(--agy-dot) / 2); top: calc(var(--agy-sq) * 0.04); }
.agy-ctr.pos-left::before { top: calc(50% - var(--agy-dot) / 2); left: calc(var(--agy-sq) * 0.04); }
.agy-ctr.pos-right::before { top: calc(50% - var(--agy-dot) / 2); right: calc(var(--agy-sq) * 0.04); }
/* The most tricks square's trick counts (F2): each seat's number on the side
   its counter would go, the same compass rule and the same 4% inset as a
   disc. The art box shrinks to the middle third here, so the numbers' band
   (4% + a 25% chip) never reaches the "+". A plain number is ink on the cream
   with a seat-color underline; at the deal end the + counter CARRIES its
   owner's number, as a chip in the seat's color in place of the disc, so all
   four counts stay readable on the final board and the summary's copy. */
.agy-sq-6 {
    --agy-tch: calc(var(--agy-sq) * 0.25);
    --agy-tin: calc(var(--agy-sq) * 0.04);
}
/* Inset 33% clears the numbers' band (29%) by 4% of the square, and the "+"
   drops to 0.22 of the square: its line box, measured in Chromium at about
   1.38 times its font size, then fits the 34% art box at every size (probe 1). */
.agy-sq-6 .agy-sq-art { inset: 33%; }
.agy-sq-6 .agy-sq-sym { font-size: calc(var(--agy-sq) * 0.22); }
/* Above the line overlay (2), level with the counters (3) and before them in
   the DOM, so a line of three through + never hides a number; taps pass
   through to the face or the bar underneath. */
.agy-tcounts { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
.agy-tcount {
    position: absolute;
    box-sizing: border-box;
    min-width: var(--agy-tch);
    height: var(--agy-tch);
    padding: 0 1px;
    border-radius: 999px;
    font-size: max(8px, calc(var(--agy-sq) * 0.18));
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: var(--agy-tch);
    text-align: center;
    white-space: nowrap;
    color: var(--agy-ink);
    pointer-events: none;
}
/* A cream ground, so a number reads where a line of three crosses it. */
.agy-tcounts .agy-tcount {
    background: var(--agy-cream);
    box-shadow: inset 0 -2px 0 var(--agy-seat);
    border-radius: 2px;
}
.agy-tcount.pos-down, .agy-ctr.pos-down > .agy-tcount { left: 50%; bottom: var(--agy-tin); translate: -50% 0; }
.agy-tcount.pos-up, .agy-ctr.pos-up > .agy-tcount { left: 50%; top: var(--agy-tin); translate: -50% 0; }
.agy-tcount.pos-left, .agy-ctr.pos-left > .agy-tcount { top: 50%; left: var(--agy-tin); translate: 0 -50%; }
.agy-tcount.pos-right, .agy-ctr.pos-right > .agy-tcount { top: 50%; right: var(--agy-tin); translate: 0 -50%; }
/* The + counter: its chip is the counter, so the disc is not drawn. */
.agy-sq-6 .agy-ctr::before { display: none; }
.agy-tcount.agy-tcount-own {
    background: var(--agy-seat);
    color: var(--agy-seat-ink);
    border: 1.5px solid #1c1c1c;
    line-height: calc(var(--agy-tch) - 3px);
}
/* Lines of three, at the deal end only (P6): a 14px hit area with a 5px stripe
   in the owner's color edged in near-black, over the faces and under the
   counters. */
.agy-lines {
    position: absolute;
    inset: 0;
    z-index: 2;
    pointer-events: none;
}
.agy-line {
    position: absolute;
    pointer-events: auto;
    padding: 0;
    border: 0;
}
.agy-line::before {
    content: '';
    position: absolute;
    border-radius: 3px;
    background: var(--agy-seat);
    box-shadow: 0 0 0 1px #1c1c1c;
}
/* Rows: across the vertical middle of their squares. */
.agy-penalty .agy-line-0, .agy-penalty .agy-line-1, .agy-penalty .agy-line-2,
.agy-pic-summary .agy-line-0, .agy-pic-summary .agy-line-1, .agy-pic-summary .agy-line-2 {
    left: calc(var(--agy-sq) * 0.12);
    right: calc(var(--agy-sq) * 0.12);
    height: 14px;
}
.agy-penalty .agy-line-0, .agy-pic-summary .agy-line-0 { top: calc(var(--agy-sq) / 2 - 7px); }
.agy-penalty .agy-line-1, .agy-pic-summary .agy-line-1 { top: calc(var(--agy-sq) * 1.5 + var(--agy-sqgap) - 7px); }
.agy-penalty .agy-line-2, .agy-pic-summary .agy-line-2 { top: calc(var(--agy-sq) * 2.5 + 2 * var(--agy-sqgap) - 7px); }
/* Columns: down the horizontal middle of their squares. */
.agy-penalty .agy-line-3, .agy-penalty .agy-line-4, .agy-penalty .agy-line-5,
.agy-pic-summary .agy-line-3, .agy-pic-summary .agy-line-4, .agy-pic-summary .agy-line-5 {
    top: calc(var(--agy-sq) * 0.12);
    bottom: calc(var(--agy-sq) * 0.12);
    width: 14px;
}
.agy-penalty .agy-line-3, .agy-pic-summary .agy-line-3 { left: calc(var(--agy-sq) / 2 - 7px); }
.agy-penalty .agy-line-4, .agy-pic-summary .agy-line-4 { left: calc(var(--agy-sq) * 1.5 + var(--agy-sqgap) - 7px); }
.agy-penalty .agy-line-5, .agy-pic-summary .agy-line-5 { left: calc(var(--agy-sq) * 2.5 + 2 * var(--agy-sqgap) - 7px); }
/* Diagonals: a row bar through the center, turned with `rotate` (never
   `transform`, which the animation owns), its ends inside the corner squares. */
.agy-penalty .agy-line-6, .agy-penalty .agy-line-7,
.agy-pic-summary .agy-line-6, .agy-pic-summary .agy-line-7 {
    left: 50%;
    top: 50%;
    width: calc(var(--agy-bsz) * 1.22);
    height: 14px;
    margin-left: calc(var(--agy-bsz) * -0.61);
    margin-top: -7px;
}
.agy-penalty .agy-line-6, .agy-pic-summary .agy-line-6 { rotate: 45deg; }
.agy-penalty .agy-line-7, .agy-pic-summary .agy-line-7 { rotate: -45deg; }
.agy-line-0::before, .agy-line-1::before, .agy-line-2::before,
.agy-line-6::before, .agy-line-7::before { left: 0; right: 0; top: calc(50% - 2.5px); height: 5px; }
.agy-line-3::before, .agy-line-4::before, .agy-line-5::before { top: 0; bottom: 0; left: calc(50% - 2.5px); width: 5px; }

/* --- the explanation balloon (U8) --------------------------------------- */
/* The optHelp balloon's look under the game's own class. A manual popover in
   the top layer, placed with fixed coordinates by board.ts: the UA's inset and
   margin are reset, or `right: 0; bottom: 0` would stay in force. */
.agy-explain {
    inset: auto;
    margin: 0;
    width: max-content;
    max-width: min(260px, calc(100vw - 24px));
    padding: 10px 12px;
    background: var(--balloon);
    border: 1px solid var(--balloon-edge);
    border-radius: 10px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--balloon-ink);
    text-align: left;
}
.agy-explain .agy-x-head { margin: 0 0 4px; font-weight: 700; }
.agy-explain .agy-x-line { margin: 0 0 3px; }
.agy-explain .agy-x-line:last-child { margin-bottom: 0; }

/* --- actions ------------------------------------------------------------- */
/* One 44px button high whether or not it has a button in it, so the hand
   below never moves between phases. */
.controls.agy-actions { min-height: 44px; margin: 6px 0; flex-wrap: nowrap; }
.agy-actions .btn, .agy-sum .result-actions .btn { min-height: 44px; }

/* --- the hand ------------------------------------------------------------ */
/* Game-scoped hand size (the site's --cw would wrap 13 cards to two rows at
   1280). The panel sits outside the board's container, so it keys on the
   viewport: one row from about a 1,220px window up. */
.agy-handpanel { --agy-handcw: 56px; }
@media (min-width: 951px) {
    .agy-handpanel { --agy-handcw: clamp(56px, min(7.5vw - 38px, 9vh), 92px); }
    @supports (width: round(down, 1.5px, 1px)) {
        .agy-handpanel { --agy-handcw: round(down, clamp(56px, min(7.5vw - 38px, 9vh), 92px), 1px); }
    }
}
@media (max-width: 560px) {
    .agy-handpanel { --agy-handcw: 40px; }
    .agy-handpanel #hand-cards { gap: 6px 5px; }
}
/* A passing game's hand title on a narrow screen (playtest round 1, found by
   probe 1's Passing On runs at 390 and 360): "Your hand (outlined cards were
   passed to you)" wraps below about a 610px window and runs under the Sort
   button, and the wrap came and went with the phase, so the hand panel grew
   21px when the pass resolved. Here the note takes a second line of its own,
   smaller, under a first line taller than the 25px button, and the joint (", " or
   " ") is hidden. The two lines are reserved for the whole of a passing
   game, so the panel is one height all game; a game with Passing Off never
   has a note and keeps one line. Wider windows keep the one-line title. */
@media (max-width: 640px) {
    .agy-handpanel.agy-passing #hand-title { line-height: 26px; min-height: 44px; }
    .agy-handpanel.agy-passing .agy-titlesep { display: none; }
    .agy-handpanel.agy-passing .agy-titlenote {
        display: block;
        font-size: 13px;
        font-weight: 400;
        line-height: 18px;
    }
}
.agy-handpanel #hand-cards .card { --cw: var(--agy-handcw); }
/* A card that cannot be played now: a filter, not opacity, never below about
   0.85 legibility. Only on this seat's turn. */
.card.agy-mute { filter: brightness(0.82) saturate(0.7); }
/* Cards received in the pass, outlined (a hint's pulse wins while it runs). */
.card.agy-passed-in:not(.hinted):not(.sel) { outline: 2px dashed var(--accent); outline-offset: 1px; }

/* --- the summaries (D8) -------------------------------------------------- */
.agy-sum .agy-sum-title { margin: 4px 0 8px; text-align: center; }
/* The final board, at a fixed size at every width: 138px square fits any
   phone and is the smallest at which the art fits beside a counter. */
.agy-pic-summary {
    --agy-sq: 44px;
    --agy-sqgap: 3px;
    width: max-content;
    margin: 0 auto 10px;
}
/* Score tables: real tables, so each column is sized once for every row, and
   an explicit text-align on every cell (the lifted dialog centers text). */
.agy-sumtable, .agy-gametable {
    border-collapse: collapse;
    margin: 0 auto;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
}
.agy-sumtable th, .agy-sumtable td, .agy-gametable th, .agy-gametable td {
    padding: 3px 8px;
    border-bottom: 1px solid var(--panel-border);
    vertical-align: top;
}
.agy-sumtable th, .agy-gametable th { font-weight: 700; }
.agy-sumtable thead th, .agy-gametable thead th { font-size: 12px; opacity: 0.85; }
.agy-sumtable .agy-seatcol, .agy-gametable .agy-seatcol { text-align: left; white-space: nowrap; }
.agy-sumtable .agy-num, .agy-gametable .agy-num { text-align: right; white-space: nowrap; }
.agy-sumtable .agy-seatcol .agy-disc { margin-right: 5px; }
.agy-gametable thead .agy-disc { margin-right: 4px; }
.agy-gametable { margin-top: 4px; }
/* A special score's short tag under the counters it left. */
.agy-tag { display: block; font-size: 10px; font-weight: 600; opacity: 0.8; text-align: right; }
.agy-sum-notes { max-width: 440px; margin: 8px auto 0; font-size: 13px; text-align: left; }
.agy-sum-note { margin: 2px 0; }

/* --- the log --------------------------------------------------------------- */
/* A closed deal's heading opens on its final board in miniature, with the
   heading's text in a column to its RIGHT at every width (F5): one row beside
   the summary's caret, so a second or third line of text wraps under the
   first line, never under the picture. The picture's top sits on the first
   line's glyph box, measured in Chromium and WebKit by agy-probe part 8. */
.agy-logrow {
    display: inline-flex;
    align-items: flex-start;
    gap: 6px;
    width: calc(100% - 1em);
    vertical-align: top;
}
.agy-pic-log {
    display: grid;
    flex: none;
    grid-template-columns: repeat(3, 9px);
    gap: 1px;
    /* 1px under the first line's glyph box: Chromium measures the box at the
       row's top, so the picture sits level with the text (F5). */
    margin-top: 1px;
}
.agy-loghead { flex: 1 1 auto; min-width: 0; }
.agy-pcell { width: 9px; height: 9px; border-radius: 1px; background: var(--agy-seat); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.45); }
.agy-pcell.agy-pempty { background: rgba(255, 255, 255, 0.15); box-shadow: none; }
.agy-logtable { margin: 4px 0 6px; font-size: 12px; }
.agy-logtable th, .agy-logtable td { padding: 2px 6px; }
/* A finished trick in the log (F6): a small cross of its four cards beside
   the trick's sentence, Hearts' log trick in shape. Every track is a fixed
   card size, so the cross is one footprint whatever it holds and a line never
   changes height as a sprite loads. Carries a game class, so it cannot reach
   another game's #log-entries. */
#log-entries .agy-logtrick {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 4px 0;
}
.agy-logx {
    --agy-lxcw: 20px;
    display: grid;
    flex: none;
    grid-template-columns: repeat(3, calc(var(--agy-lxcw) + 2px));
    grid-template-rows: repeat(3, calc(var(--agy-lxcw) * 89 / 64 + 2px));
    grid-template-areas: ". up ." "left . right" ". down .";
    gap: 1px 4px;
    padding-left: 8px;
}
.agy-logx .card { --cw: var(--agy-lxcw); }
.agy-logx-cell {
    position: relative;
    display: flex;
    padding: 1px;
    border-radius: 3px;
    line-height: 0;
    background: color-mix(in srgb, var(--agy-seat) 22%, transparent);
}
.agy-logx-cell.pos-up { grid-area: up; }
.agy-logx-cell.pos-left { grid-area: left; }
.agy-logx-cell.pos-right { grid-area: right; }
.agy-logx-cell.pos-down { grid-area: down; }
/* The card that won: a gold ring, an outline, so it composes with the glow.
   It sits OUTSIDE the glow's solid spread (2px, the rules above), so a
   penalty card that wins its own trick shows the rose band and then the
   ring; at 1px the ring covered the band and a self-won Queen read as a
   plain winner (UI review finding 1). The ring ends 4px out from the card,
   3px out from its cell, so it clears the line's 4px padding and the
   cross's 4px column gap by 1px. */
.agy-logx .card.agy-logwin { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The card that led: a caret outside its left edge near the top, a shape
   and not only a color, out of flow so marking a card never resizes the
   cross. Placed clear of every ring: left of its own card's ring, and 2px
   down so the ring of a winner in the row above passes over it. */
.agy-logx-led {
    position: absolute;
    top: 2px;
    left: -9px;
    font-size: 10px;
    line-height: 1;
    font-weight: 800;
    color: var(--ink);
    pointer-events: none;
}
.agy-logtext { flex: 1 1 auto; min-width: 0; }

/* --- the scoring table in the cheat sheet --------------------------------- */
.agy-scoretable { width: 100%; border-collapse: collapse; margin: 6px 0 10px; }
.agy-scoretable th, .agy-scoretable td {
    text-align: left;
    padding: 3px 8px 3px 0;
    border-bottom: 1px solid var(--panel-border);
    font-size: 13px;
}

/* --- reduced motion -------------------------------------------------------- */
/* The counter flights are skipped in script under reduced motion; nothing
   here animates on its own, so this only drops the shared hover lift on a
   board trigger should one ever be added. */
@media (prefers-reduced-motion: reduce) {
    .agy-sq-face, .agy-ctr, .agy-line, .agy-none, .agy-passed, .agy-dealer, .agy-dump-face { transition: none; }
}

/* --- the wide layout: a board 900px wide and up --------------------------- */
/* Four columns: West, the cross, East, and the side column (the dump card
   ABOVE the penalty board), so the whole table and the hand fit 1280x720. */
@container agy (min-width: 900px) {
    .agy-board {
        --agy-playcw: clamp(56px, min(6cqi, 8vh), 80px);
        --agy-sq: clamp(64px, min(8.4cqi, 11vh), 104px);
        grid-template-columns: auto auto auto auto;
        grid-template-areas:
            ".    up    .     side"
            "left trick right side"
            ".    down  .     side";
        gap: 8px 16px;
    }
    @supports (width: round(down, 1.5px, 1px)) {
        .agy-board {
            --agy-playcw: round(down, clamp(56px, min(6cqi, 8vh), 80px), 1px);
            --agy-sq: round(down, clamp(64px, min(8.4cqi, 11vh), 104px), 1px);
        }
    }
    .agy-side {
        flex-direction: column;
        align-self: center;
        min-height: calc(var(--agy-playcw) * 89 / 64 + 8px + 3 * var(--agy-sq) + 2 * var(--agy-sqgap));
    }
}

/* --- the phone layout: a board under 640px wide ---------------------------- */
/* Fives' arrangement: the three other panels in a row ACROSS THE TOP, still
   West, North and East, the cross under them, this player under that; the
   board above them all, so what is touched sits by the thumb. Every
   reservation is re-derived here. */
@container agy (max-width: 639.98px) {
    .agy-board {
        --agy-playcw: 44px;
        /* Playtest round 1 (final review NIT 1): the dump card is a read-only
           card in full art, so it sits above the 46px band rather than in it.
           52px is 72px tall, and with its caption the stacked dump block is
           still shorter than the 159px board beside it at 360px. */
        --agy-dumpcw: 52px;
        --agy-sq: clamp(46px, 15cqi, 64px);
        --agy-sqgap: 3px;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        grid-template-areas:
            "side  side  side"
            "left  up    right"
            "trick trick trick"
            "down  down  down";
        gap: 8px 6px;
        align-items: start;
    }
    @supports (width: round(down, 1.5px, 1px)) {
        .agy-board { --agy-sq: round(down, clamp(46px, 15cqi, 64px), 1px); }
    }
    .agy-dump { flex-direction: column; align-items: center; gap: 4px; }
    .agy-dump-face { align-items: center; text-align: center; }
    .agy-seat.pos-left, .agy-seat.pos-up, .agy-seat.pos-right {
        width: auto;
        justify-self: stretch;
        padding: 5px 6px 6px;
    }
    /* A third of a 360px screen is about 109px, so the head wraps to two
       reserved lines: the disc and the name, then the D chip and the counters. */
    .agy-seat:not(.pos-down) .agy-seat-head {
        flex-wrap: wrap;
        height: 38px;
        align-content: flex-start;
        row-gap: 2px;
    }
    .agy-seat:not(.pos-down) .agy-name { flex: 1 1 calc(100% - 20px); }
    .agy-seat:not(.pos-down) .agy-left { margin-left: auto; }
    .agy-trick { gap: 2px 6px; padding: 6px 8px; }
}

/* --- phones, outside the board's container ---------------------------------- */
/* The status line wraps to three lines with a pass hint at 360px. */
@media (max-width: 560px) {
    .status.agy-status { min-height: 3.9em; }
}
/* ==========================================================================
   Juggling Act
   ==========================================================================
   A shedding game where every player holds a hidden hand and a face-up hand in
   a fixed order. Shared classes (.card, .panel, .panel-head, .card.simple.back,
   .card.sel, .card.clickable, .logline, .btn, .result-actions, .winner-banner)
   are reused as they are; only this game's own rules live here, all `jga-`.

   SHARED IDS ARE SCOPED. #hand-panel, #hand-cards, #hand-title, and
   #status-line are on every page, so each rule naming one goes through
   `.jga-handpanel` (the page's class on the panel) or `.jga-status` (the class
   render() puts on the status line).

   NOTHING MOVES WHILE YOU PLAY. The opponents' rows never wrap (they overlap
   past their width), the stage, the controls, and the status line reserve
   their heights, the prepare gaps between the viewer's cards are always laid out,
   and each of the viewer's hands is one row, exactly one card tall, at every
   width (ONE ROW below). Heights are in terms of --cw and the 89/64 card
   aspect, so they follow the card size.

   PAIR SPACING. In every row whose cards are painted face up, two neighbors
   that could sit together in a throw (the same rank, or ranks one apart)
   are a little closer than the default and any other pair a little farther
   (jga-near and jga-far, from def.ts pairClass). A row of backs keeps the
   default. In the viewer's rows def.ts fitGaps eases the two sizes toward the
   default wherever they would push a row past its width. */

/* ---------- the opponents ---------- */
.jga-opps {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 340px);
    justify-content: center;
    gap: 8px;
    margin-bottom: 10px;
    --jga-mcw: 30px;
    /* The gap between two small cards: default (and between backs), near, far. */
    --jga-mgap: 3px;
    --jga-mgap-near: 1px;
    --jga-mgap-far: 5px;
}
.jga-opp {
    min-width: 0;
    padding: 6px 9px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--panel);
}
.jga-opp-active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.jga-opp-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px;
    white-space: nowrap;
    margin-bottom: 4px;
}
.jga-opp-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: 700;
    font-size: 14px;
}
.jga-stats {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
    white-space: nowrap;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
}
.jga-standing { font-weight: 800; font-size: 15px; }
.jga-total { opacity: 0.8; }
.jga-total.jga-hold { visibility: hidden; }
.jga-pile {
    padding: 0 7px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    font-size: 12px;
}
/* One row that never wraps: past its width the cards overlap, stepping by
   (row width - one card) / (n - 1), so the panel never grows. --jga-n is the
   row's card count, set by miniRow. An empty row keeps one card's height. */
.jga-mini {
    display: flex;
    justify-content: center;
    flex-wrap: nowrap;
    min-height: calc(var(--jga-mcw) * 89 / 64);
    margin: 3px 0;
}
.jga-mini .card { --cw: var(--jga-mcw); }
/* Each margin is its pair's gap or the even step that fills the row, whichever
   is smaller, so the row never outgrows its width: past it every step is the
   same overlap, and the pair spacing gives way first. */
.jga-mini .card + .card {
    margin-left: min(var(--jga-mgap), calc((100% - var(--jga-n, 1) * var(--jga-mcw)) / (var(--jga-n, 1) - 1)));
}
.jga-mini .card.jga-near {
    margin-left: min(var(--jga-mgap-near), calc((100% - var(--jga-n, 1) * var(--jga-mcw)) / (var(--jga-n, 1) - 1)));
}
.jga-mini .card.jga-far {
    margin-left: min(var(--jga-mgap-far), calc((100% - var(--jga-n, 1) * var(--jga-mcw)) / (var(--jga-n, 1) - 1)));
}
/* A known card in an opponent's hidden hand is the one face among backs; the
   ring only helps it stand out. Nothing in these rows is ever playable, so the
   ring cannot be read as the playable highlight. */
.jga-mini .card.jga-known { box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.75), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
/* The strongest throw an opponent's face-up hand could make now
   (def.ts, strongestSlice): a thin pale-blue ring and a faint glow, in place.
   It is meant to be plainly different from your own playable cards (a gold
   ring, a faint gold glow, and a raise, beside dimmed cards) and the gold
   ring of a known card: it is pale blue, on the small cards of an opponent's
   row, and it sits outside the card, so every face stays readable. */
.jga-opp-up .card.jga-best {
    box-shadow:
        0 0 0 2px rgba(176, 214, 255, 0.85),
        0 0 8px 2px rgba(176, 214, 255, 0.4),
        0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}

/* ---------- the stage ---------- */
.panel.jga-stage { margin: 0; text-align: center; --jga-scw: 58px; }
.jga-stage .panel-head { justify-content: center; }
.jga-throwrow {
    display: flex;
    justify-content: center;
    flex-wrap: nowrap;
    padding: 4px 0;
    min-height: calc(var(--jga-scw) * 89 / 64 + 8px);
}
.jga-throwrow .card { --cw: var(--jga-scw); }
.jga-throwrow .card + .card {
    margin-left: min(6px, calc((100% - var(--jga-n, 1) * var(--jga-scw)) / (var(--jga-n, 1) - 1)));
}
.jga-throwrow .card.jga-aside { opacity: 0.6; }
/* Live solo: the slot of the full throw that your throw's leaving card stands
   in while its ghost is there (def.ts leavingSlot, landLeaving). It takes a
   card's size and spacing, so the full throw stands centered, and paints
   nothing: the ghost is what the player sees. */
.jga-throwrow .card.jga-leaving { visibility: hidden; }
/* The ghost: placed in the page rather than the viewport, so a scroll carries
   it with the stage while it stands, and over the board but under every
   overlay (the result dialog lifts over a game that ends on this throw). */
.card-fly.jga-leaving-ghost { position: absolute; z-index: 30; }
/* The viewed seat's own throw: a border and a tint in a color of its
   own, distinct from the gold that marks the turn and the playable cards. The
   border keeps its 1px width and the ring is an inset shadow, so nothing moves. */
.panel.jga-stage { --jga-mine: #7ee0c3; }
.panel.jga-stage.jga-mine {
    border-color: var(--jga-mine);
    box-shadow: inset 0 0 0 1px var(--jga-mine);
    background: rgba(126, 224, 195, 0.09);
}
.jga-stage.jga-mine .jga-throwcap { color: var(--jga-mine); font-weight: 700; }
.jga-throwcap {
    font-size: 13px;
    min-height: 1.45em;
    line-height: 1.45;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ---------- the controls ----------
   #jga-controls reserves the row's height and is the container the row's
   rules measure (100cqw below is its width); .jga-ctlrow (def.ts
   controlsRow) is the row.

   ONE LINE, AT EVERY WIDTH (the user, 2026-10-17: "the action buttons are on
   different rows. Fix that"). The table's five buttons (Throw, Prepare,
   Catch, Cancel, and Pass; jga-five) and solo's six (Discard before Cancel)
   always share one centered line, each at its label's own width: nothing has
   a fixed or minimum width, and nothing grows to fill the line. On a narrow
   row the labels, their padding, and the gaps shrink together, linearly in
   the row's width, from full size (16px labels, 14px padding, 8px gaps) on a
   row as wide as the full-size buttons plus 8px down to the smallest size on
   a 300px row (a 320px screen). Each end leaves at least 8px of the row
   free, and so does every width between, since the buttons' total width is
   linear in the four sizes. Full-size widths measured in Chromium and
   WebKit alike, 2026-10-17: Throw 76.3 (at the primary's weight, below),
   Prepare 86.5, Catch 72, Discard 85.4, Cancel 80.3, and Pass 61.1, so the
   table's row needs 424px and solo's 518px; on a 300px row they need 282px
   and 290px. Buttons stay at least 36px tall, the site's touch-target
   minimum (the coarse-pointer block in duelpoker/style.css). flex-wrap is
   only a fallback for a font much wider than these.

   PASS IS LAST AND APART (the user, 2026-10-17: pressing it "is generally a
   mistake unless there's nothing better to do"). It is the row's last button,
   so last in Tab order too, with a margin on its left that makes its gap 24px
   at full size and never under 16px.
   WHAT SHOWS IS CENTERED (def.ts controlsRow). A button the position rules
   out leaves the row (jga-out), and one that comes and goes within the turn
   holds its place hidden (jga-idle), between Throw and Pass, so the buttons
   that show sit centered on the board axis. Leading at the table (jga-lead),
   Throw is centered alone, with Cancel's place to its right. */
.jga-controls {
    container-type: inline-size;
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin: 10px 0;
    min-height: 40px;
}
.jga-ctlrow {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--jga-bgap);
}
.jga-ctlrow .btn {
    flex: none;
    white-space: nowrap;
    min-height: 36px;
    font-size: var(--jga-bfont);
    padding-left: var(--jga-bpad);
    padding-right: var(--jga-bpad);
}
.jga-ctlrow .jga-pass { margin-left: var(--jga-pgap); }
/* Leading at the table: Throw alone in the middle column, centered on the
   axis however wide Cancel is, and Cancel's place (held hidden until a card
   is tapped) at the start of the right-hand one. The two outer columns are
   equal (minmax(0, 1fr) never grows to fit Cancel), so a selection moves
   nothing. */
.jga-ctlrow.jga-lead {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    column-gap: var(--jga-bgap);
}
.jga-ctlrow.jga-lead .jga-throw { grid-column: 2; }
.jga-ctlrow.jga-lead .jga-cancel { grid-column: 3; justify-self: start; }
/* The table: 13px labels, 6px padding, 4px gaps, and Pass's gap 4 + 12px on
   a 300px row; full size from a 435px row. */
.jga-controls.jga-five .jga-ctlrow {
    --jga-bfont: clamp(13px, calc(13px + (100cqw - 300px) * 0.02222), 16px);
    --jga-bpad: clamp(6px, calc(6px + (100cqw - 300px) * 0.05926), 14px);
    --jga-bgap: clamp(4px, calc(4px + (100cqw - 300px) * 0.02963), 8px);
    --jga-pgap: clamp(12px, calc(12px + (100cqw - 300px) * 0.02963), 16px);
}
/* Solo, in two straight pieces. 11.5px labels, 4px padding, 3px gaps, and
   Pass's gap 3 + 13px on a 300px row (a 320px screen); 13px labels and 5px
   padding, the gaps unchanged, on a 340px row (a 360px screen); full size
   from a 528px row. Within each piece every size is linear in the row's
   width, so fitting at its two ends is fitting all through it. The labels
   take the width the padding and the gaps give up, which holds them at
   13px and over on a phone. */
.jga-controls.jga-solo .jga-ctlrow {
    --jga-bfont: clamp(11.5px, min(calc(11.5px + (100cqw - 300px) * 0.0375), calc(13px + (100cqw - 340px) * 0.015957)), 16px);
    --jga-bpad: clamp(4px, max(calc(4px + (100cqw - 300px) * 0.025), calc(5px + (100cqw - 340px) * 0.047872)), 14px);
    --jga-bgap: clamp(3px, calc(3px + (100cqw - 340px) * 0.026596), 8px);
    --jga-pgap: clamp(13px, calc(13px + (100cqw - 340px) * 0.015957), 16px);
}
/* The primary's weight (.btn.primary, a legal selection) is wider than the
   plain label, so a hidden copy of the label at that weight holds the
   button's width, and the row does not move when the selection becomes a
   throw. The copy is read from data-label, which def.ts's throwButton sets
   from the label itself, so the two cannot drift apart. The empty
   alternative text keeps the copy out of the button's name. */
.jga-controls .jga-throw::after {
    content: attr(data-label);
    content: attr(data-label) / '';
    display: block;
    height: 0;
    overflow: hidden;
    visibility: hidden;
    font-weight: 600;
}
.jga-controls .btn.selected { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Held in place, hidden, so the row does not move when they come and go
   within a turn; out of the row when the position rules them out
   (def.ts controlsRow). */
.jga-controls .btn.jga-idle { visibility: hidden; }
.jga-controls .btn.jga-out { display: none; }

/* ---------- the viewer's hands ---------- */
/* --jga-gap is the prepare gap at either end of a row and beside a back; between
   two painted cards it is --jga-gap-near or --jga-gap-far (PAIR SPACING above).
   def.ts reads the three here, on the panel, and may ease the last two on a row.
   ONE ROW (the user, 2026-10-22: "Ideally, all the cards in a hand are in one
   row"). Neither row wraps. Each row is the query container for its cards, and
   every card in both rows is the row's width less --jga-cap + 1 default gaps,
   over --jga-cap, rounded down to a whole pixel, between --jga-hmin and
   --jga-hmax. --jga-cap is the larger dealt hand plus one, which def.ts
   sizeHands sets on the panel for the whole deal, so the two rows share one
   size and line up, and a card that leaves or arrives resizes nothing. A row
   is one card tall: the cards and the gaps are, and an empty row still has
   its end gap. A row that grows past --jga-cap (--jga-n is its own count,
   from def.ts renderRow) keeps the size: its gaps narrow to share what room
   is left (--jga-room), down to nothing, and past that the gaps' negative
   margins overlap its cards evenly, so it still fits its one line. def.ts
   fitArt draws the cards from the simplified atlas at 34px and under. */
.jga-handpanel {
    --jga-hmax: 58px;
    --jga-hmin: 16px;
    --jga-cap: 10;
    --jga-raise: 2px;
    --jga-raise-room: 6px;
    --jga-gap: 8px;
    --jga-gap-near: 5px;
    --jga-gap-far: 10px;
}
/* Fixed line boxes, so a minus sign or a new total never changes the head's height. */
.jga-handpanel #hand-title {
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 10px;
    white-space: nowrap;
    height: 24px;
    line-height: 24px;
}
.jga-handpanel #hand-title .jga-stats { font-weight: 600; line-height: 24px; }
.jga-handpanel #hand-title .jga-stats > span { line-height: 20px; }
/* A slot is a card and the gap before it; it adds no box of its own. */
.jga-handpanel .jga-slot { display: contents; }
/* The face-up hand comes first, under the panel's head; the hidden hand's head
   sits between the two rows. */
.jga-handpanel .jga-hiddenhead { margin-top: 8px; }
.jga-handpanel #jga-hidden-title { white-space: nowrap; }
/* The gaps supply the spacing between cards, so the rows set none. The top
   padding is the room the playable raise rises into, always laid out, so a
   raise never pushes anything. --cw holds 100cqw unresolved (a custom
   property is substituted where it is used), so each card and gap reads it
   against its own row. */
.jga-handpanel #hand-cards,
.jga-handpanel #jga-up-cards {
    container-type: inline-size;
    flex-wrap: nowrap;
    gap: 0;
    padding-top: var(--jga-raise-room);
    justify-content: center;
    --cw: clamp(var(--jga-hmin), calc((100cqw - (var(--jga-cap) + 1) * var(--jga-gap)) / var(--jga-cap)), var(--jga-hmax));
}
/* Whole pixels, for the reason and with the guard platform.css gives its own --cw. */
@supports (width: round(down, 1.5px, 1px)) {
    .jga-handpanel #hand-cards,
    .jga-handpanel #jga-up-cards {
        --cw: clamp(var(--jga-hmin), round(down, calc((100cqw - (var(--jga-cap) + 1) * var(--jga-gap)) / var(--jga-cap)), 1px), var(--jga-hmax));
    }
}
/* PLAYABLE AGAINST NOT, ON YOUR TURN. A card that is part of a throw you
   may make wears a 2px gold ring, a faint glow, and a 2px raise; a card that
   is not is lightly desaturated and darkened. The user found the first,
   stronger treatment (a 3px ring, a 14px glow, a 4px raise, grayscale 0.85
   and brightness 0.62) too strong (2026-10-13), and these values were picked
   by eye in screenshots at 1280 and 390 (highlight-before/after-*.png in
   .agents/tasks/jugglingact-catch-2026-10-13/). The raise room below
   (--jga-raise-room) is unchanged, so nothing moves. The raise is the
   independent `translate` property, never `transform`, which belongs to the
   animations (the pop-in, the hover lift, .card.sel, the flight ghosts) and
   composes with it; the dim is a `filter`, never `opacity`. When nothing can
   throw, nothing is dimmed: every card is a prepare target then. */
.jga-handpanel #hand-cards .card.jga-playable,
.jga-handpanel #jga-up-cards .card.jga-playable {
    translate: 0 calc(-1 * var(--jga-raise));
    box-shadow:
        0 0 0 2px var(--accent),
        0 0 6px 1px rgba(255, 209, 102, 0.28),
        0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
/* A selected card is ringed in the ink color over the gold, so the selection
   still reads on a ringed card. */
.jga-handpanel #hand-cards .card.jga-playable.sel,
.jga-handpanel #jga-up-cards .card.jga-playable.sel { outline-color: var(--ink); }
.jga-handpanel #hand-cards .card.jga-dim,
.jga-handpanel #jga-up-cards .card.jga-dim { filter: grayscale(0.45) brightness(0.85); }
/* A card of yours that everyone can see wears the site's shared eye badge
   (.eye-badge in platform.css, eyeBadge in shared/ui/panels.ts), not a ring, so
   it never reads as the playable highlight and keeps its mark when playable.
   `position: relative` is a TWO-class rule so it out-specifies `.card-fly`'s
   `position: fixed` on a ghost, and def.ts names jga-badged in
   ghostStripClasses. */
.card.jga-badged { position: relative; }

/* A gap is always there, 8px wide by default (5px or 10px between a pair; 4px,
   2px, and 6px on a phone), and exactly one card tall, beside the cards.
   Armed, it shows an insertion bar down its middle and its hit area spreads over
   a quarter of each neighboring card, so the middle half of every card still
   answers a tap on the card (picking it to move instead). Placing a catch,
   when no hand card answers a tap, it spreads to the middle of each neighbor
   (jga-gap-wide). */
.jga-gap {
    flex: none;
    /* Each gap's share of the row's width the cards leave: never under a
       default gap until the row holds more than --jga-cap cards. */
    --jga-room: calc((100cqw - var(--jga-n, 1) * var(--cw)) / (var(--jga-n, 1) + 1));
    width: clamp(0px, var(--jga-room), var(--jga-gap, 8px));
    height: calc(var(--cw) * 89 / 64);
    position: relative;
    margin-block: 0;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    appearance: none;
    -webkit-appearance: none;
}
.jga-gap.jga-near { width: clamp(0px, var(--jga-room), var(--jga-gap-near, 8px)); }
.jga-gap.jga-far { width: clamp(0px, var(--jga-room), var(--jga-gap-far, 8px)); }
/* Cards wider in all than the row: the gaps between two cards (not the two
   ends) take the excess back as negative margins, shared evenly, so the
   cards overlap and the row still starts and ends at its edges. */
.jga-handpanel .jga-slot ~ .jga-slot > .jga-gap {
    margin-inline: min(0px, calc((100cqw - var(--jga-n, 1) * var(--cw)) / (2 * max(var(--jga-n, 1) - 1, 1))));
}
.jga-gap:disabled { cursor: default; }
.jga-gap.jga-gap-on { cursor: pointer; z-index: 2; }
.jga-gap.jga-gap-on::before {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: calc(var(--cw) / -4);
    right: calc(var(--cw) / -4);
}
.jga-gap.jga-gap-on.jga-gap-wide::before {
    left: calc(var(--cw) / -2);
    right: calc(var(--cw) / -2);
}
.jga-gap.jga-gap-on::after {
    content: '';
    position: absolute;
    top: 2px;
    bottom: 2px;
    left: calc(50% - 2px);
    width: 4px;
    border-radius: 2px;
    background: var(--accent);
    opacity: 0.55;
}
.jga-gap.jga-gap-on:hover::after,
.jga-gap.jga-gap-on:focus-visible::after { opacity: 1; }

/* ---------- progress ---------- */
.progress.jga-progress {
    opacity: 1;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 10px;
    min-height: 30px;
    white-space: nowrap;
}
/* Fewest cards left (localhost only): the variant's name beside the round. */
.jga-progress .jga-variant { font-weight: 600; }

/* ---------- summaries ---------- */
table.jga-summary,
table.jga-final {
    border-collapse: collapse;
    margin: 8px auto;
    font-variant-numeric: tabular-nums;
}
table.jga-summary th,
table.jga-summary td,
table.jga-final th,
table.jga-final td { padding: 3px 8px; vertical-align: middle; }
table.jga-summary th,
table.jga-final th { font-size: 12px; opacity: 0.7; font-weight: 600; }
.jga-summary .jga-l,
.jga-final .jga-l { text-align: left; }
.jga-summary .jga-r,
.jga-final .jga-r { text-align: right; }
/* The minus between Score pile and In hands, and the "=" after In hands,
   header included. */
table.jga-summary th.jga-op,
table.jga-summary td.jga-op { text-align: center; padding-left: 2px; padding-right: 2px; }
/* In hands is left-aligned, header included, so its number sits right after
   the minus (the user asked for this, against the right-aligned default). A
   real table sizes the column once, so every row still lines up. */
table.jga-summary th.jga-hands,
table.jga-summary td.jga-hands { text-align: left; padding-left: 2px; }
.jga-summary .jga-totalcell,
.jga-final .jga-totalcell { font-weight: 800; }
.jga-summary tr.jga-out td { color: var(--accent); }
.jga-final tr.jga-winner td,
.jga-summary tr.jga-winner td { background: rgba(255, 209, 102, 0.14); }
.jga-summary-title { text-align: center; margin: 10px 0 0; font-size: 14px; }

/* ---------- the log ---------- */
.logline.jga-logout { color: var(--accent); font-weight: 700; }

/* ---------- rules lists ---------- */
ol.jga-list,
ul.jga-list { margin: 6px 0 0 18px; padding: 0; }
ol.jga-list li,
ul.jga-list li { margin-bottom: 5px; }

/* ---------- the status line ----------
   The longest prompts (your own throw, named in hotseat as "Player 1's")
   run to about 100 characters, about 860px at 18px. Above 950px the board
   column is the viewport minus the 300px log column and 48px of padding and
   gap, so such a prompt takes two lines up to a viewport of about 1210px; at
   950px and below the single column is at most 918px, too close to trust.
   Two lines are reserved through 1240px, three on a phone, and four under
   360px (both below), so no prompt changes the status line's height at any
   width. */
#status-line.jga-status { line-height: 1.35; }
@media (max-width: 1240px) {
    #status-line.jga-status { min-height: calc(2 * 1.35em); }
}

/* ---------- the solo game (Ruling 9) ----------
   Players 1. The setup holds the two options that do not apply in their
   place, hidden (stable containers): Length and Bot level. def.ts puts
   jga-solo on #screen-setup (the shell writes only `hidden` there, and on the
   level group, which this leaves alone). */
#screen-setup.jga-solo .opt-group:has(#opt-length),
#screen-setup.jga-solo .opt-group:has(#opt-scoring),
#screen-setup.jga-solo .opt-group:has(#opt-level) { visibility: hidden; }

/* The Jokers sit where the opponents would, centered, at one fixed height:
   48px full-art cards, just above the 46px band where a read-only card must
   use the simplified atlas (ui-conventions.md); a turned Joker is the shared
   .card.simple.back. The flip turns the WRAPPER, never .card, whose
   transform belongs to the animator (ui-conventions.md, sideways cards). */
.jga-opps.jga-jokers {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 10px;
    height: calc(48px * 89 / 64 + 8px);
    --jga-jcw: 48px;
}
.jga-jokers-label { font-weight: 700; font-size: 14px; white-space: nowrap; }
.jga-jokerrow { display: flex; gap: 6px; }
.jga-joker { display: block; }
.jga-joker .card { --cw: var(--jga-jcw); }
.jga-joker.jga-joker-flip { animation: jga-joker-flip 450ms ease-out; }
@keyframes jga-joker-flip {
    0% { transform: perspective(300px) rotateY(90deg); }
    100% { transform: perspective(300px) rotateY(0deg); }
}
@media (prefers-reduced-motion: reduce) {
    .jga-joker.jga-joker-flip { animation: none; }
}

/* The name of your solo result (def.ts soloResultName) is the shared centered
   .winner-banner. "Go back to clown school." fits one line at 390px and wraps
   at 360px and below (so does "THE CIRCUS IS YOURS!" at 320px, measured in
   Chromium, 2026-10-17); balanced, it wraps as "Go back to / clown school."
   rather than leaving "school." on a line of its own. */
.winner-banner.jga-solo-name { text-wrap: balance; }

/* The longest solo prompts (a prepare's, with its Joker warning, about 114
   characters) take two lines up to 1288px, past the 1240px the table game's
   reservation stops at, so solo keeps two lines through 1340px (measured as
   above; the phone rules below hold three and four lines, which the solo
   prompts need from 490px and 354px down). */
@media (min-width: 1241px) and (max-width: 1340px) {
    #status-line.jga-status.jga-solo { min-height: calc(2 * 1.35em); }
}

/* ---------- touch screens ----------
   A finger needs a wider target than a pointer. The armed gap's hit area
   already spans a quarter of each neighbor, and any wider would cover the
   middle of the cards, so only the bar grows. */
@media (pointer: coarse) {
    .jga-gap.jga-gap-on::after { width: 5px; left: calc(50% - 2.5px); }
}

/* ---------- narrow screens ----------
   The opponents stack, one compact panel each, in play order. With three
   opponents (4 players) each panel lays its two rows side by side under its
   head, so the three panels stay short enough for your hand to start on
   screen; 7 and 6 cards fit a half row with little or no overlap.
   The stage's cards are read-only, so they sit at 48px, just above the band
   where ui-conventions.md wants the simplified atlas (35px to 46px). */
@media (max-width: 560px) {
    .jga-opps { grid-auto-flow: row; grid-auto-columns: auto; grid-template-columns: minmax(0, 1fr); --jga-mcw: 26px; }
    .jga-opp { padding: 4px 8px; }
    .jga-opps.jga-n4 .jga-opp {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        column-gap: 10px;
    }
    .jga-opps.jga-n4 .jga-opp-head { grid-column: 1 / -1; }
    .panel.jga-stage { --jga-scw: 48px; }
    /* Narrower side padding and gaps leave the cards the room: at 320px a row
       is 286px wide, against 266px with the shared panel padding. */
    .jga-handpanel {
        padding-left: 6px;
        padding-right: 6px;
        --jga-hmax: 46px;
        --jga-gap: 4px;
        --jga-gap-near: 2px;
        --jga-gap-far: 6px;
    }
    /* Up to seven columns in a phone's dialog: tighter cells, and a name never wraps
       (a header may, which leaves every column where it was). */
    table.jga-summary th,
    table.jga-summary td,
    table.jga-final th,
    table.jga-final td { padding: 3px 5px; }
    table.jga-summary td.jga-l { white-space: nowrap; }
    #status-line.jga-status { min-height: calc(3 * 1.35em); }
}
/* Under 360px the longest prompts take a fourth line. */
@media (max-width: 359px) {
    #status-line.jga-status { min-height: calc(4 * 1.35em); }
}
/*
 * Dungeon Bounty. Every class is prefixed `dgb-` [Requirement 33.3], and every
 * selector that names a SHARED id (#hand-cards, #hand-panel, #status-line,
 * #progress-line, #board) is scoped through one of those classes, because a
 * bare rule on a shared id reaches every other game's board
 * [ui-conventions.md, "Shared ids need a game class"].
 *
 * Two rules this file deliberately does NOT write:
 *   - no `min-height` and no `justify-content` on `#hand-cards`: the tray's
 *     height is reserved at runtime by src/shared/ui/handspace.ts and the
 *     centering is one global rule;
 *   - no `container-type` on `#hand-panel` or `#board`.
 *
 * site/webplay.css is GENERATED from this file by tools/gen-css.mjs. Edit here
 * and run `npm run gen`.
 */

/* ------------------------------------------------------- the progress line */

/*
 * THE PROGRESS LINE IS A ROW OF CHIPS, so it needs a gap of its own.
 *
 * Shared `.progress` in platform.css is plain centered text with no flex and
 * no gap, and the games that put chips on that line opt into the
 * `.progress.*-scoreboard` list, which is where `display: flex` and the gap
 * live. This game is in neither place, so four bare spans CONCATENATED and the
 * line read `Level 1Cavern 18Strength 5Base J 5 · Q 10 · K 15` at every width
 * and in both modes. The worst join was `Strength 5` against the legend, where
 * two rules numbers abut and the legend's first number can be read as part of
 * the strength [build review 8, finding 1].
 *
 * The column gap is 14px rather than a word space, because the legend is
 * itself a `·`-separated list: a chip separator any smaller would read as one
 * more item in it. `align-items: baseline` keeps the four chips on one text
 * baseline whatever their font metrics.
 *
 * The reservation is in the phone block below, where the row can actually
 * wrap. `tools/dungeonbounty/dgb-probe.mjs` measures both halves: a gap between every
 * adjacent pair, and the line count against the reservation.
 */
#progress-line.dgb-progress {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: baseline;
    gap: 2px 14px;
}

/* ---------------------------------------------------------------- the board */

.dgb-zonetitle {
    margin: 0 0 6px;
    text-align: center;
    font-size: 0.95rem;
    letter-spacing: 0.02em;
}

/*
 * The three board panels. The padding is 10px rather than `.panel`'s own 14px,
 * and that is the ONE-SCREEN FIT rather than a taste: measured in Chromium at
 * 1280x800 (`tools/dungeonbounty/dgb-probe.mjs`), the VERSUS board ran 15px past the fold,
 * because versus carries a fourth panel the solo board does not (the
 * Overlord's hand) and every panel's padding is paid twice. Three panels at
 * 4px a side is 24px, which with the Overlord hand's own trim below puts the
 * controls and the top of the tray back on screen at both of the widths the
 * brief names.
 */
#dgb-dungeon,
#dgb-overlord,
#dgb-hero {
    margin: 0 auto 10px;
    max-width: 760px;
    padding-top: 10px;
    padding-bottom: 10px;
}

/* A resolved cavern holds at most two cards, so it never scrolls. */
.dgb-cavern {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: flex-start;
    gap: 14px;
    min-height: calc(86px * 1.39 + 26px);
}

/*
 * SCOUTING PUTS FIVE CARDS IN A ROW SIZED FOR TWO, so that one row scrolls.
 * Measured at 390px (`tools/dungeonbounty/dgb-probe.mjs`): the deck chip, five cards, and the
 * discard chip come to 406px in a 336px box, which pushed the whole PAGE 43px
 * sideways, and 58px at 360px. Wrapping instead would be worse: a second row
 * grows the zone by a card's height for the duration of the scout, and every
 * panel below it, the tray included, moves with it [ui-conventions.md, "A card
 * strip that wraps is the biggest single offender"].
 *
 * The left-alignment is in the phone block below, where the row can actually
 * scroll: `justify-content: center` with `overflow-x: auto` clips the left
 * overflow behind an unreachable edge, so a strip that scrolls must not be
 * centered. At 1280 and 768 the five cards fit, so the row stays centered
 * there and `overflow-x` never engages.
 */
.dgb-scoutrow {
    overflow-x: auto;
}

.dgb-cavern .card.dgb-cavcard {
    --cw: 86px;
}

.dgb-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    position: relative;
}

/* The empty slot is exactly as wide as a filled one, so a weapon arriving
 * cannot change the row's width. Both numbers are re-derived at the phone
 * breakpoint below, where `.dgb-weapon` shrinks to 48px. */
.dgb-slot.dgb-empty {
    justify-content: center;
    width: 62px;
    min-height: calc(62px * 1.39);
    border: 1px dashed rgba(255, 255, 255, 0.28);
    border-radius: 6px;
    font-size: 0.72rem;
    text-align: center;
    opacity: 0.85;
}

/*
 * The Overlord's hand. A scrolling strip rather than a wrapping one, for the
 * reason the bounties are: the hand reaches five and the hand limit lets it
 * grow past that mid-turn, and a second row would push every zone below it
 * down the screen [ui-conventions.md]. A scrolling strip must not be centered,
 * so this one is left-aligned once it can scroll.
 */
/*
 * The Overlord's hand: a SCROLLING strip, so it is left-aligned rather than
 * centered. `justify-content: center` with `overflow-x: auto` clips the left
 * overflow behind an unreachable edge [ui-conventions.md], and this row grows
 * to seven cards (five in hand plus two drawn).
 */
.dgb-ohand {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: flex-start;
    gap: 6px;
    overflow-x: auto;
    min-height: calc(36px * 1.39);
}

/*
 * 36px, and READ-ONLY at any width, which is what lets it be this small: the
 * Overlord's runs are bought by pressing a button per run rather than by
 * tapping the cards, so no card in this row is a tap target. It sits in
 * `ui-conventions.md`'s 35-to-46px band, where a read-only zone takes the
 * simplified atlas, and `renderOverlord` asks for `simple` on every card here
 * (a back has no other atlas anyway). It was 46px until the probe measured the
 * versus board 15px past one screen at 1280x800: read-only cards shrink first
 * [brief].
 */
.dgb-ohand .card.dgb-ocard {
    --cw: 36px;
}

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

.dgb-pilechip .card {
    --cw: 46px;
}

/*
 * The two rules are a pair on purpose: the first is the typography a pile
 * count and an enemy's health share, and the second is the chip only the
 * health wears. Merging them would either duplicate the typography or put a
 * chip behind the deck and discard counts [build review 9, finding 5].
 */
.dgb-pilecount,
.dgb-health {
    white-space: nowrap;
    font-size: 0.74rem;
    font-variant-numeric: tabular-nums;
}

.dgb-health {
    padding: 1px 6px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.35);
}

/* ---------------------------------------------------------------- the Hunter */

.dgb-slots,
.dgb-bounties {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: 10px;
}

/* Two slots, so this one cannot grow past its reserved row. */
.dgb-slots {
    flex-wrap: wrap;
    min-height: calc(62px * 1.39 + 8px);
}

/*
 * The club paints OVER its enchanting gem, and both z-indexes are positive on
 * purpose. `.dgb-slot` is `position: relative` with no z-index, so it opens no
 * stacking context: a child at `z-index: -1` would paint behind the
 * BACKGROUNDS of every ancestor up to the nearest stacking context, which in
 * practice is the panel, and the gem would be invisible rather than tucked
 * behind its club. Ordering the two siblings inside the same stacking context
 * is what actually tucks it.
 *
 * `.dgb-weapon` is a flex item of `.dgb-slot`, and z-index applies to a flex
 * item without `position`, which is why it needs no `position: relative`.
 */
.dgb-slots .card.dgb-weapon {
    --cw: 62px;
    z-index: 1;
}

/* The enchanting gem sits behind its club, faded with a filter rather than
 * opacity, and never so far it cannot be read [brief]. */
.dgb-slots .card.dgb-gemon {
    --cw: 46px;
    position: absolute;
    left: 24px;
    top: 14px;
    z-index: 0;
    filter: brightness(0.9);
}

/*
 * The bounties are a SCROLLING STRIP, not a wrapping one. The pack holds twelve
 * enemies, so a wrapping row reaches two lines part way through level one and
 * three by the end, and every one of those lines pushes the hand tray down the
 * screen: ui-conventions.md names a wrapping card strip the biggest single
 * source of panel movement. `nowrap` plus `overflow-x: auto` keeps it one row
 * forever, and `renderHero` sets `scrollLeft = scrollWidth` so the newest
 * bounty stays in view.
 *
 * A scrolling strip must NOT be centered: `justify-content: center` plus
 * `overflow-x` clips the left overflow behind an unreachable edge, so this one
 * overrides the shared centering above once it can scroll.
 */
.dgb-bounties {
    flex: 1 1 auto;
    min-width: 0;
    flex-wrap: nowrap;
    justify-content: flex-start;
    /*
     * CENTERED down the row rather than top-aligned, which is a no-op for the
     * cards (they are exactly the reserved height) and is the whole point for
     * the empty state: the note sat at the top of a 64px strip while its
     * caption sat halfway down it, so the two words of one sentence were on
     * different baselines.
     */
    align-items: center;
    overflow-x: auto;
    min-height: calc(46px * 1.39);
}

/*
 * The strip's caption and the strip, on one line. The caption is a flex item
 * OUTSIDE the scrolling strip, so it holds its place while the cards scroll
 * under it, and `flex: 1 1 auto; min-width: 0` on the strip is what lets the
 * strip take the rest of the row and scroll inside it rather than push the
 * panel sideways [build review 8, finding 4]. The row's height is the strip's
 * reserved height, which the caption is well inside, so this costs no movement.
 */
.dgb-bountyrow {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 8px;
    margin-top: 8px;
}

.dgb-striptitle {
    flex: 0 0 auto;
    white-space: nowrap;
    font-size: 0.8rem;
    letter-spacing: 0.02em;
}

.dgb-bounties .card {
    --cw: 46px;
}

.dgb-note {
    margin: 0;
    text-align: center;
    font-size: 0.8rem;
}

/* ---------------------------------------------------------------- the controls */

/*
 * ONE CONTAINER HOLDS BOTH THE PROSE AND THE CONTROLS HERE, and that is a
 * deliberate departure from ui-conventions.md's "Controls and prose in
 * separate containers, controls first". `renderActions` itself and five of the
 * renderers it delegates to (the fight rows, the scout, and the Overlord's
 * build, runs and discard) append a `.dgb-note` into `#dgb-actions` ahead of
 * the buttons [def.ts], `.dgb-note` carries no `flex-basis`, and `.controls`
 * is a wrapping flex row, so a note is a flex item that CAN share a line with
 * a button. Measured at 390px during the Overlord's build: the note "Which
 * card goes into the cavern first?" packs onto the same line as a "Set out
 * the" button.
 *
 * Splitting them would not buy what that rule buys elsewhere, because the
 * thing it protects against is not present: see "A ROW COUNT CANNOT BE
 * RESERVED HERE" below, where the measurement shows this zone's line count
 * follows LABEL WIDTHS rather than how many items are in it, so prose in a box
 * of its own leaves the button rows just as unpredictable. WHAT ACTUALLY HOLDS
 * THE TRAY STILL IS THE RUNTIME HIGH-WATER MARK in `reserveActions` [def.ts],
 * which the probe measures at 0px of tray movement within one decision at
 * 1280, 768, 390 and 360px in both roles. The open layout preference is
 * recorded as unverified item 5 of the task's verification record.
 *
 * Two rows are reserved here; the other two numbers this zone needs are
 * MEASURED AT RUNTIME by `reserveActions` in def.ts, because neither can be
 * written down in CSS:
 *
 *   - a `min-height` high-water mark for as long as one decision lasts, so
 *     tapping a tray card to pick a weapon cannot move the tray under the
 *     finger. Measured at 390px, the zone runs from 96px at rest to 383px for
 *     an ace with three targets in each hand, and a flat reservation of the
 *     tallest would leave ~290px of nothing under every ordinary turn
 *     [ui-conventions.md, "The hand is reserved at runtime, not in CSS"];
 *   - a `max-height` that keeps the controls and the top of the tray on screen
 *     [brief]. The budget is the viewport minus this zone's own top, and that
 *     top is 568px in solo and 681px in versus at 1280x800 (versus carries a
 *     fourth panel), so one CSS number cannot serve both.
 *
 * `overflow-y` is what makes the cap safe rather than clipping: the two rare
 * tall shapes (the six ace clears, and the Overlord's run list, which review 5
 * left uncapped for this measurement) scroll inside the zone.
 */
.dgb-actions {
    min-height: 96px;
    align-content: flex-start;
    overflow-y: auto;
}

.dgb-actions .btn.dgb-act {
    min-height: 44px;
}

/*
 * A ROW COUNT CANNOT BE RESERVED HERE, which is worth stating where somebody
 * will next try: the zone is a wrapping flex ROW of content-width controls, so
 * how many lines a control set takes is a function of its LABEL WIDTHS, not of
 * how many controls there are. Measured at 390px during the Overlord's build:
 * four 44px items packed into two lines on the first paint ("Which card goes
 * into the cavern first?" plus a "Set out the" button fit one line) and three
 * on the second. An invisible spacer row was tried and reserved nothing,
 * because it is 30px wide and packs onto a line that already exists. What
 * handles the reshape is `reserveActions` keying its mark on the build's own
 * step [def.ts].
 */

/*
 * One enemy's row: its health, the live attack, and the two commits, in one
 * line so every option for that enemy is visible at once [design D3, D3b].
 * `nowrap` on the numbers, because a wrapped health breakdown would make the
 * row taller than the reservation above.
 */
.dgb-fight {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 8px;
    width: 100%;
}

.dgb-attack {
    white-space: nowrap;
    font-size: 0.78rem;
    font-variant-numeric: tabular-nums;
}

.dgb-flee {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 6px;
}

/*
 * A PICKED card is ringed rather than faded, and a card that is not currently
 * legal is dimmed with a FILTER and never past 0.85 [brief: do not fade cards
 * so far they cannot be read]. A `.dgb-off` wrapper carries no `.clickable`
 * class, because the renderer attaches no handler to it: the class is both the
 * site's affordance mark and the set tools/smoke-all.mjs presses, so a member
 * of it that does nothing lies to the player and spends a smoke click on
 * nothing (`tap` and `inert` in def.ts put the two on together).
 */
.dgb-picked {
    outline: 2px solid var(--accent, #e8c35a);
    outline-offset: 1px;
}

.dgb-off {
    filter: brightness(0.86) saturate(0.9);
    cursor: default;
}

/*
 * A CARD NAME inside a control label, a board note, or a chip (`cardSpan` in
 * def.ts). Suits decide the halving, the curse pair, the enchant match, and the
 * relic triple here, so the glyph carries its color on every surface that names
 * a card, not only in the log.
 *
 * The modifier names are suitTextClass' own and the three hues are the site's,
 * which is why this is three rules rather than three colors: `.diamond` sits
 * one class deeper so it wins over `.red`, the same shape as the `.cardlabel`
 * and `#log-entries` blocks in platform.css. `nowrap` only, never `.cardlabel`,
 * whose bold would re-weight one word of a button's label.
 */
.dgb-suit {
    white-space: nowrap;
}

.dgb-suit.red { color: var(--red); }
.dgb-suit.red.diamond { color: var(--red-diamond); }
.dgb-suit.club { color: var(--blue-club); }

/*
 * An affordance beside a card: Curse, Enchant, Relic, Keep. Every one is a real
 * `<button class="btn small clickable ...">`, so ITS SIZE COMES FROM
 * `.btn.small` IN platform.css, not from here: that selector is two classes and
 * these are one, so an authored `font-size` or `padding` would be dead CSS.
 * Measured in Chromium on the generated stylesheet at 390px and 1280px, with
 * the game's own classes: 24.8px tall and 50.5px ("Keep") to 66.3px
 * ("Enchant") wide, computed 12px / 3px 10px, and 36px tall under
 * `@media (pointer: coarse)`, which is where a phone actually reads them. Both
 * are byte for byte the site's own `.btn.small` (the hand-sort button on every
 * page), and the touch figure is the platform's own target-size rule. The
 * brief's "at least 44px tall" is honored where the player commits, by
 * `.dgb-actions .btn.dgb-act` above.
 *
 * The three declarations this rule used to carry (`font-size: 0.68rem`,
 * `padding: 1px 6px`, `min-height: 0`) were all dead for that reason, the last
 * one on a coarse pointer too, where `.btn.small`'s 36px wins. Deleting them
 * was measured to change nothing: same heights, same widths, and the same
 * 82.4px hand row, with and without them, on both pointer kinds.
 *
 * So a badge IS routinely wider than the card it hangs off (66.3px against a
 * 40px tray card on a phone). That widens its own tray column and nothing
 * else: see `.dgb-traywrap` below for why that is safe.
 *
 * `nowrap` is therefore the whole rule. It is the one thing worth setting: a
 * two-line "Enchant" would make the column taller than its neighbors and
 * ripple the tray's reserved height.
 */
.dgb-afford,
.dgb-keep {
    white-space: nowrap;
}

.dgb-where {
    font-variant-numeric: tabular-nums;
}

.dgb-kept .card {
    outline: 2px solid var(--accent, #e8c35a);
    outline-offset: 1px;
}

/* ---------------------------------------------------------------- the merchant */

/*
 * The pile viewer is the LAST element of the board, so opening it moves
 * nothing. It wraps rather than scrolls, because the discard pile reaches forty
 * cards and a one-row strip of forty would be unreadable; nothing sits below it
 * to be pushed.
 */
.dgb-pileview {
    margin: 0 auto 10px;
    max-width: 760px;
}

.dgb-pilecards {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    gap: 6px;
}

.dgb-pilecards .card {
    --cw: 46px;
}

.dgb-pilewrap {
    display: flex;
    flex-direction: column;
    align-items: center;
}

/* ---------------------------------------------------------------- the tray */

#hand-panel.dgb-handpanel .card.dgb-tray {
    --cw: 52px;
}

/*
 * A tray card and its affordance are one column, so a badge wider than its
 * card widens only that COLUMN, never the card and never the row's height.
 * Measured at 390px: a 40px card under a 66.3px "Enchant" makes a 66.3px
 * column (`.dgb-afford` above has the table).
 *
 * That extra width is safe, and it is worth saying which mechanism makes it
 * safe, because it is not this rule. `#hand-cards` is the shared wrapping,
 * centered row, and its height is held at its high-water mark per viewport
 * width by `src/shared/ui/handspace.ts` at runtime, so a column going wide
 * (and the hand therefore wrapping one card earlier) cannot walk the tray up
 * or down under the player's finger [ui-conventions.md, "The hand is reserved
 * at runtime, not in CSS"]. Nothing here sets a `min-height` on `#hand-cards`,
 * which that doc forbids. The probe measures the consequence rather than the
 * cause: 0px of tray movement within one decision at 1280, 768, 390 and 360px
 * in all four driven games, and no panel or document overflow at any of them.
 */
.dgb-traywrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
}

/* ---------------------------------------------------------------- the result */

.dgb-result {
    border-collapse: collapse;
    margin: 0 auto;
    font-variant-numeric: tabular-nums;
}

.dgb-result th {
    text-align: left;
    padding: 2px 12px 2px 0;
    font-weight: 600;
}

.dgb-result td {
    text-align: right;
    padding: 2px 0;
}

.dgb-title {
    max-width: 40em;
    margin: 10px auto 0;
    text-align: center;
}

/* ---------------------------------------------------------------- the phone */

@media (max-width: 720px) {
    /*
     * THE STATUS LINE TAKES TWO LINES HERE, so two are reserved. The shared
     * `.status` reserves one (26px at 18px/1.4), and this game's line runs to
     * two at 390px ("You are in the cavern. Pick weapons and spells, then slay
     * the K of Hearts.") and back to one, which moved the progress line, both
     * board zones, the controls, and the tray 24 to 25px every time it changed
     * [ui-conventions.md, "The status and progress lines change line count per
     * phase: reserve their tallest line count, at each breakpoint"]. Measured
     * at 390 and 360px: 26px and 50px, so 50px is the reservation. At 768 and
     * 1280 the line is one line in every position measured, so no reservation
     * is written there.
     */
    #status-line.dgb-status {
        min-height: 50px;
    }

    /*
     * AND TWO LINES FOR THE PROGRESS ROW, for the same reason and measured the
     * same way. `tools/dungeonbounty/dgb-probe.mjs` reports what one line wants against the
     * room it has: 344 to 360px of a 340px row at 360px wide, so the four
     * chips wrap there in every position measured, and 390px fits them on one
     * line with 10px to spare in the widest. The line count also CHANGES
     * during play at both widths, because scouting paints no Cavern chip, and
     * this row sits above both board zones, the controls, and the tray, so
     * every change moved all of them by a line
     * [ui-conventions.md, "Reserve their tallest line count, at each
     * breakpoint"].
     *
     * 41px is the MEASURED two-line height (14px at 1.4, twice, plus the 2px
     * row gap), not the desktop figure scaled. At 768 and 1280 the row is one
     * line in every position measured, with 300px and more to spare, so
     * nothing is reserved there beyond shared `.progress`'s own 20px.
     */
    #progress-line.dgb-progress {
        min-height: 41px;
    }

    .dgb-cavern {
        gap: 8px;
        min-height: calc(62px * 1.39 + 26px);
    }

    /* The scouting row scrolls at this width (see `.dgb-scoutrow` above), and a
     * strip that scrolls is left-aligned rather than centered. */
    .dgb-scoutrow {
        justify-content: flex-start;
    }

    .dgb-cavern .card.dgb-cavcard {
        --cw: 62px;
    }

    .dgb-slots {
        min-height: calc(48px * 1.39 + 8px);
    }

    .dgb-slots .card.dgb-weapon {
        --cw: 48px;
    }

    .dgb-slot.dgb-empty {
        width: 48px;
        min-height: calc(48px * 1.39);
    }

    .dgb-slots .card.dgb-gemon {
        --cw: 36px;
        left: 18px;
        top: 10px;
    }

    .dgb-bounties {
        min-height: calc(34px * 1.39);
    }

    .dgb-ohand {
        min-height: calc(34px * 1.39);
    }

    .dgb-ohand .card.dgb-ocard {
        --cw: 34px;
    }

    .dgb-bounties .card {
        --cw: 34px;
    }

    #hand-panel.dgb-handpanel .card.dgb-tray {
        --cw: 40px;
    }

    /* Re-derived rather than scaled: leaving the desktop figure here is the
     * same bug with a different number [ui-conventions.md]. */
    .dgb-pilecards .card {
        --cw: 34px;
    }
}
/* =========================================================================
   Mus (mus)
   A cross-shaped table (Fives, Biscuits): at four players the opponents West
   and East either side of the round tracker, the partner North, and the
   viewer's own line South; at two players the one opponent North. The hand
   panel below holds one row of 4 (or two rows, Hand 1 and Hand 2), each with
   its values strip: High, Low, Pairs, and Total, with the percentile under
   each.
   NOTHING MOVES (ui-conventions "Containers must not move"). Every line that
   can change is reserved from the deal: seat badges and declaration rows,
   the reveal slots (filled in place at the showdown), the tracker cells, the
   status and progress lines, the one-line action row, and the values cells
   (fixed width and height, `nowrap`). #hand-cards is reserved by the shell
   (shared/ui/handspace.ts), so there is no min-height on it here.
   TWO SIZE CONTAINERS, both on game classes: the board and the hand panel
   each choose their arrangement from their own measured width, because the
   log column comes and goes at 950px and a media query would need a
   different breakpoint on each side of it.
   SMALL CARDS ARE SIMPLIFIED. Revealed seat cards are 30px (22px on a phone)
   and drawn `.card.simple`; the hand keeps the full art.
   Every selector is scoped through a `mus-` class: no rule names a bare
   shared id.
   ========================================================================= */

/* --- the board ----------------------------------------------------------- */
.mus-board {
    container-type: inline-size;
    container-name: mus-board;
    --mus-mcw: 30px;
    --mus-gap: 3px;
    margin-bottom: 4px;
}
.mus-board .mus-grid {
    display: grid;
    gap: 8px;
    justify-items: center;
    align-items: start;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-areas: "w n e" "c c c" "s s s";
}
.mus-board .mus-table { display: contents; }
.mus-board.mus-two .mus-grid {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "n" "c" "s";
}
@container mus-board (min-width: 720px) {
    .mus-board.mus-four .mus-grid {
        grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
        grid-template-areas: ". n ." "w c e" ". s .";
        gap: 10px;
    }
    .mus-board.mus-four .mus-seat.pos-w { justify-self: end; }
    .mus-board.mus-four .mus-seat.pos-e { justify-self: start; }
}
.mus-board .mus-seat.pos-w { grid-area: w; }
.mus-board .mus-seat.pos-n { grid-area: n; }
.mus-board .mus-seat.pos-e { grid-area: e; }
.mus-board .mus-center { grid-area: c; display: flex; justify-content: center; width: 100%; }
/* Your own line holds only the order, your name, and your last action, so it
   sits on one row: the hand panel below holds everything else of yours. */
.mus-board .mus-mine { grid-area: s; }
.mus-board .mus-seat.mus-mine { flex-direction: row; gap: 8px; }

/* --- a seat panel: every line fixed-height and nowrap -------------------- */
.mus-board .mus-seat {
    box-sizing: border-box;
    max-width: 100%;
    min-width: 0;
    padding: 6px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    background: var(--panel);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    text-align: center;
}
.mus-board .mus-seat.mus-active { border-color: var(--accent); }
.mus-board .mus-seathead {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    max-width: 100%;
    height: 20px;
    white-space: nowrap;
}
.mus-board .mus-order {
    flex: none;
    font-size: 11px;
    line-height: 16px;
    padding: 0 5px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.12);
    white-space: nowrap;
}
.mus-board .mus-seatname {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 700;
}
.mus-board .mus-badge {
    height: 18px;
    line-height: 18px;
    font-size: 12px;
    white-space: nowrap;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    padding: 0 6px;
    border-radius: 9px;
    color: rgba(242, 239, 233, 0.6);
}
.mus-board .mus-badge.mus-lit {
    color: var(--accent-ink);
    background: var(--accent);
    font-weight: 700;
}
.mus-board .mus-badgeround { margin-right: 4px; opacity: 0.75; font-weight: 400; }
.mus-board .mus-decl-row {
    display: flex;
    justify-content: center;
    gap: 4px;
    height: 16px;
    white-space: nowrap;
}
.mus-board .mus-decl {
    font-size: 11px;
    line-height: 16px;
    padding: 0 5px;
    border-radius: 8px;
    border: 1px solid var(--panel-border);
}
.mus-board .mus-reveal {
    display: flex;
    justify-content: center;
    gap: var(--mus-gap);
    min-height: calc(var(--mus-mcw) * 89 / 64);
    min-width: calc(4 * var(--mus-mcw) + 3 * var(--mus-gap));
}
.mus-board .mus-reveal .card { --cw: var(--mus-mcw); flex: none; }
.mus-board .mus-slots { display: flex; justify-content: center; gap: 10px; }
.mus-board .mus-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}

/* --- the round tracker ---------------------------------------------------- */
.mus-board .mus-rounds {
    display: flex;
    justify-content: center;
    gap: 6px;
}
.mus-board .mus-round {
    box-sizing: border-box;
    width: 96px;
    padding: 3px 2px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    background: var(--panel);
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
}
.mus-board .mus-round.mus-muted { opacity: 0.55; }
.mus-board .mus-round.mus-now { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.mus-board .mus-roundname { font-weight: 700; font-size: 13px; line-height: 18px; height: 18px; }
.mus-board .mus-roundstate { font-size: 12px; line-height: 16px; height: 16px; }

/* --- status and progress: reserved line counts --------------------------- */
/* In line-heights: 2 lines at desktop, 3 at the phone's 16px (the worst-case
   answer line with online names), 1 and 2 for the progress line. */
.status.mus-status { line-height: 1.4; min-height: 2.8em; text-align: center; }
.progress.mus-progress { line-height: 1.4; min-height: 1.4em; text-align: center; }

/* --- the action row: one line at every width ----------------------------- */
.controls.mus-actions {
    flex-wrap: nowrap;
    justify-content: center;
    gap: 6px;
    margin: 8px 0;
    min-height: 52px;
    align-items: center;
}
.mus-actions .btn { min-height: 44px; padding: 7px 10px; font-size: 15px; white-space: nowrap; }
.mus-actions .mus-more { min-width: 44px; }
.mus-actions .mus-amounts {
    margin: auto;
    padding: 10px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: var(--felt-dark);
    color: var(--ink);
    max-width: 92vw;
}
.mus-actions .mus-amounts:popover-open {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px;
}

/* --- the hand panel -------------------------------------------------------- */
.mus-handpanel {
    container-type: inline-size;
    container-name: mus-hand;
}
.mus-handpanel #hand-sort { display: none; }
/* The title is empty behind a pass-and-play gate (no hand is shown), and an
   empty h3 let the cards rise 7px at every gate. One line is reserved. */
.mus-handpanel #hand-title { line-height: 1.4; min-height: 1.4em; }
.mus-handpanel #hand-cards {
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: center;
    gap: 8px;
}
.mus-handpanel .mus-handrow {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
}
@container mus-hand (min-width: 680px) {
    .mus-handpanel.mus-four .mus-handrow { flex-direction: row; gap: 16px; }
}
@container mus-hand (min-width: 780px) {
    .mus-handpanel.mus-two .mus-handrow { flex-direction: row; gap: 10px; }
}
.mus-handpanel .mus-rowchip {
    box-sizing: border-box;
    width: 84px;
    height: 18px;
    font-size: 12px;
    line-height: 18px;
    text-align: center;
    border-radius: 9px;
    background: rgba(255, 255, 255, 0.12);
    white-space: nowrap;
}
.mus-handpanel .mus-cards {
    display: flex;
    justify-content: center;
    gap: 6px;
    min-height: calc(var(--cw) * 89 / 64);
    min-width: calc(4 * var(--cw) + 18px);
}
.mus-handpanel .mus-cards .card { flex: none; }
.mus-handpanel .card.picked {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    translate: 0 -5px;
}
.mus-handpanel .card.mus-swapsel {
    /* The value --blue-club holds in the two-color deck, written out: this ring
       marks a card to swap, not a suit, so the four-color deck (which turns
       --blue-club green) must not recolor it. */
    outline: 3px solid #d6e0eb;
    outline-offset: 1px;
    translate: 0 -5px;
}
.mus-handpanel { --mus-fresh: #7fd6ff; }
.mus-handpanel .card.mus-fresh { box-shadow: inset 0 0 0 2px var(--mus-fresh), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }

/* --- the values strip: four fixed cells ---------------------------------- */
.mus-handpanel .mus-values {
    display: flex;
    justify-content: center;
    gap: 6px;
    font-size: 0.75rem;
    white-space: nowrap;
}
.mus-handpanel .mus-val {
    box-sizing: border-box;
    padding: 2px 4px;
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    text-align: center;
}
.mus-handpanel .mus-val-high,
.mus-handpanel .mus-val-low { width: 6.5em; }
.mus-handpanel .mus-val-pairs { width: 8.5em; }
.mus-handpanel .mus-val-total { width: 3.5em; }
.mus-handpanel .mus-valname { font-weight: 700; line-height: 1.2em; height: 1.2em; }
.mus-handpanel .mus-valtext { min-height: 2.4em; height: 2.4em; line-height: 1.2em; }
.mus-handpanel .mus-valline { white-space: nowrap; }
.mus-handpanel .mus-pct { line-height: 1.2em; height: 1.2em; color: var(--accent); }

/* --- the summary: a real table aligned by column -------------------------- */
#result-panel .mus-summaryhead { text-align: center; }
#result-panel table.mus-summary {
    border-collapse: collapse;
    margin: 0 auto;
    font-size: 14px;
}
.mus-summary th,
.mus-summary td {
    padding: 4px 8px;
    border-bottom: 1px solid var(--panel-border);
    vertical-align: top;
}
.mus-summary .mus-sumround { text-align: left; white-space: nowrap; }
.mus-summary .mus-sumresult { text-align: left; min-width: 8em; }
.mus-summary .mus-sumpts {
    text-align: right;
    width: 1%;
    font-variant-numeric: tabular-nums;
}
.mus-summary td.mus-sumpts { white-space: nowrap; }
/* F6: a team header wraps at its `&` at every width (each half is a
   nowrap span), so the Result column keeps the room its arithmetic needs. */
.mus-summary th.mus-sumpts { white-space: normal; }
.mus-summary .mus-teamname { white-space: nowrap; }
.mus-summary .mus-row-deal th,
.mus-summary .mus-row-deal td,
.mus-summary .mus-row-score th,
.mus-summary .mus-row-score td { font-weight: 700; }
/* F6: every hand at the deal's end, a real table (ui-conventions "Score
   tables line up by column"), its small cards the simplified sprite. */
#result-panel table.mus-hands {
    border-collapse: collapse;
    margin: 10px auto 0;
    font-size: 13px;
    --cw: 22px;
}
.mus-hands th,
.mus-hands td {
    padding: 3px 6px;
    border-bottom: 1px solid var(--panel-border);
    vertical-align: middle;
}
.mus-hands thead th { font-size: 12px; opacity: 0.7; }
.mus-hands .mus-hname { text-align: left; white-space: nowrap; font-weight: 400; }
.mus-hands thead .mus-hname { font-weight: 700; }
.mus-hands .mus-horder {
    display: inline-block;
    min-width: 2.2em;
    margin-right: 4px;
    padding: 0 4px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.12);
    font-size: 11px;
    line-height: 16px;
    text-align: center;
}
.mus-hands .mus-hcards { white-space: nowrap; text-align: left; }
.mus-hands td.mus-hcards .card { display: inline-block; vertical-align: middle; margin-right: 2px; }
.mus-hands .mus-hpairs { text-align: left; }
.mus-hands .mus-hline { white-space: nowrap; }
.mus-hands .mus-htotal { text-align: right; font-variant-numeric: tabular-nums; }
/* Lifted into the modal, the table may scroll, but Next deal stays in view.
   The row must be opaque to hide what scrolls under it, and it paints above
   the overlay's own dimming ::before (z-index -1 inside the panel), so its
   background is the panel's color with that same 50% dim laid on top. */
.panel.result-overlay .mus-summaryactions {
    position: sticky;
    bottom: -14px;
    margin-bottom: -14px;
    padding: 10px 0 14px;
    background:
        linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)),
        linear-gradient(var(--panel), var(--panel)),
        var(--felt-dark);
}

/* --- the Rules summary's scoring table (F6): three left-aligned columns --- */
table.mus-scoring { border-collapse: collapse; margin: 6px 0 8px; font-size: 13.5px; }
.mus-scoring th,
.mus-scoring td {
    padding: 3px 12px 3px 0;
    text-align: left;
    vertical-align: top;
    border-bottom: 1px solid var(--panel-border);
}
.mus-scoring thead th { font-size: 12px; opacity: 0.6; font-weight: 700; }
.mus-scoring td:first-child,
.mus-scoring td:nth-child(2) { white-space: nowrap; }

/* --- the Rules summary's glossary: two left-aligned columns --------------- */
table.mus-glossary { border-collapse: collapse; margin: 6px 0 4px; font-size: 13.5px; }
.mus-glossary th,
.mus-glossary td { padding: 3px 14px 3px 0; text-align: left; vertical-align: top; }
.mus-glossary thead th { font-size: 12px; opacity: 0.6; font-weight: 700; }
.mus-glossary td:first-child { font-style: italic; white-space: nowrap; }

/* --- the phone breakpoint (the platform's own, 560px) --------------------- */
@media (max-width: 560px) {
    .mus-board { --mus-mcw: 22px; --mus-gap: 2px; }
    .mus-board .mus-seat { padding: 4px; }
    .mus-board .mus-badgeround { display: none; }
    .mus-board .mus-round { width: calc((100% - 18px) / 4); flex: none; }
    .mus-board .mus-rounds { width: 100%; }
    .status.mus-status { font-size: 16px; min-height: 4.2em; }
    .progress.mus-progress { min-height: 2.8em; }
    .mus-handpanel .mus-values { font-size: 0.7rem; }
    .mus-summary th,
    .mus-summary td { padding: 3px 5px; }
    #result-panel table.mus-hands { --cw: 18px; font-size: 12px; }
    .mus-hands th,
    .mus-hands td { padding: 2px 4px; }
    /* Two-player names ("Player 2's Hand 1") were 10px wider than the modal:
       the order chip sits over the name, and a long online name may wrap. */
    .mus-hands .mus-hname { white-space: normal; }
    .mus-hands .mus-horder { display: block; width: fit-content; margin: 0 0 2px; }
    .mus-scoring td:first-child,
    .mus-scoring td:nth-child(2) { white-space: normal; }
}

/* The Examples section's four hands (pages/mus.mjs). A value such as
   "K K J 7" is read as one thing and must not break across lines, nor may a
   seat's name, so those cells stay on one line; on a phone the cards drop to
   18px and only the Pairs value may wrap. */
.cheatsheet .mus-extable td { white-space: nowrap; }
@media (max-width: 560px) {
    .cheatsheet .mus-extable .cardex { --cw: 18px; }
    .cheatsheet .mus-extable td:nth-child(5) { white-space: normal; }
    .cheatsheet .mus-extable th,
    .cheatsheet .mus-extable td { padding: 3px 3px; }
}
/* ------------------------------------------------------------------------- */
/* Army Ants                                                                 */
/* ------------------------------------------------------------------------- */
/*
 * Prefix policy: every Army Ants class is `ant-` and every id `#ant-`. Shared
 * ids are reached only through the game's own classes (`.ant-status`,
 * `.ant-progress`); nothing here names `#hand-cards` or `#hand-panel`, and
 * the hand panel stays hidden (def.ts empties and hides it every render).
 * Shared classes are reused as they are: `.panel`, `.btn`, `.card`,
 * `.card.empty`, `.card.simple`, `.clickable`, and `.cardlabel`.
 *
 * THE BOARD HOLDS ITS SIZE ALL GAME (design D4): 5 fixed columns and 9 fixed
 * row tracks (4 rows, the center track, 4 rows) off one knob, `--ant-cw`,
 * so nothing a command does can resize it. The rail beside it is exactly the
 * board's height, and the status line reserves its tallest shape, so the
 * board and the rail never move between repaints.
 *
 * Army tints follow Techu's seat colors: red #e8665a, black a cool blue
 * #7fa7d6, distinct from red under the common color-vision deficiencies.
 * The last command's marks are Garbo's last-move blue, #6aa9e0.
 */

/* ---- the stage, and the knob every card reads (D4) ---- */

/* The board plus the rail is ONE centered unit (D4, R13.9). The stage spans
   main.playarea, so its midpoint is the axis .status and .progress center on. */
.ant-stage {
    container-type: inline-size;
    display: flex;
    justify-content: center;
    margin-bottom: 14px;
}
/*
 * The card width is the smaller of a width term and a height term, clamped
 * to 28px..64px. The width term is what the column leaves after the rail, the
 * rail gap, the board's padding and border, and the 4 column gaps. The height
 * term is what the viewport leaves after the page chrome above the board, the
 * 8 row gaps, the center track, the padding, and the border, over 11.125 (8
 * rows of 89/64 card widths). 100cqi resolves against .ant-stage, the only
 * size container around the board; the flying ghost is mounted on body with
 * a fixed pixel size, so it reads no cqi.
 */
.ant-main {
    --ant-gap: 4px;
    --ant-mid: 10px;
    --ant-pad: 8px;
    --ant-rail: 168px;
    --ant-railgap: 12px;
    /* Measured by tools/armyants/armyants-probe.mjs at 1280x720 (task 19.2,
       2026-10-06): the board's top at 151px plus #screen-game's 16px bottom
       padding. The same top holds at 1280x800 and 768x1024. Cards: 44px at
       1280x720, 51px at 1280x800, 64px (the cap) at 768x1024. */
    --ant-chrome: 167px;
    --ant-vh: 100vh;
    --ant-cwraw: clamp(28px, min(
        (100cqi - var(--ant-rail) - var(--ant-railgap) - 2 * var(--ant-pad) - 2px
            - 4 * var(--ant-gap)) / 5,
        (var(--ant-vh) - var(--ant-chrome) - 8 * var(--ant-gap) - var(--ant-mid)
            - 2 * var(--ant-pad) - 2px) / 11.125), 64px);
    --ant-cw: var(--ant-cwraw);
    --ant-ch: calc(var(--ant-cw) * 89 / 64);
    display: grid;
    grid-template-columns: auto var(--ant-rail);
    gap: var(--ant-railgap);
}
/* A second declaration of a custom property would not fall back (it accepts
   any tokens, so the later one always wins), hence the @supports. */
@supports (height: 100dvh) {
    .ant-main { --ant-vh: 100dvh; }
}
/* Whole-pixel cards, so the atlas shows no seam (Duel Poker's guard). Guarded,
   because an unsupported round() inside a custom property blanks every card
   that reads it. */
@supports (width: round(down, 1.5px, 1px)) {
    .ant-main { --ant-cw: round(down, var(--ant-cwraw), 1px); }
}
/* The grid never stretches; the rail is exactly the board's height, so its
   rows put each roster level with its own army (design, Rail). */
.ant-main > .panel.ant-board {
    align-self: start;
    margin: 0;
    padding: var(--ant-pad);
}
.ant-main > .ant-rail { align-self: stretch; }

/* ---- the grid, the territories, and the center line ---- */

/*
 * Fixed tracks off the knob. Its own stacking context, so the badges and the
 * marks stack among the cards and nowhere else (Minigolf: without it its ball
 * painted over the tutorial's coach panel docked over the board on a phone).
 */
.ant-grid {
    display: grid;
    grid-template-columns: repeat(5, var(--ant-cw));
    grid-template-rows: repeat(4, var(--ant-ch)) var(--ant-mid) repeat(4, var(--ant-ch));
    gap: var(--ant-gap);
    position: relative;
    isolation: isolate;
}
/* The two territories: absolutely placed in their four rows, so they take no
   grid cell and the 40 cells auto-place around the center line in DOM order.
   They sit first in the DOM, so every cell paints over them. */
.ant-grid > .ant-band {
    position: absolute;
    grid-column: 1 / -1;
    inset: -2px;
    border-radius: 8px;
    pointer-events: none;
}
.ant-grid > .ant-band-top { grid-row: 1 / 5; }
.ant-grid > .ant-band-bottom { grid-row: 6 / 10; }
.ant-grid > .ant-band-red { background: rgba(232, 102, 90, 0.16); }
.ant-grid > .ant-band-black { background: rgba(127, 167, 214, 0.16); }
/* The center line, across the five columns of the 10px middle track. */
.ant-grid > .ant-centerline {
    grid-column: 1 / -1;
    grid-row: 5;
    align-self: center;
    height: 0;
    border-top: 2px dashed rgba(255, 255, 255, 0.55);
}

/* ---- a cell: one card or one empty slot ---- */

/*
 * Every positioned rule below is scoped `.ant-cell > …`, so the body-mounted
 * animation ghost (`.card-fly` is `position: fixed`) is never re-positioned
 * or re-styled by a rule meant for the grid (Jackhammer hit exactly that).
 * `overflow` stays visible so the swap badge's tap area can reach into the
 * gap on the partner's left.
 */
.ant-cell { position: relative; min-width: 0; }
.ant-cell > .card {
    --cw: var(--ant-cw);
    width: 100%;
    height: 100%;
}
/*
 * No hover lift on the board. The platform's 5px lift moves a card out from
 * under a pointer resting near its bottom edge, which ends the hover, drops
 * the card, and starts the hover again: on this board every hover draws a
 * preview, so that loop would flicker the whole preview. The preview itself is
 * the hover feedback.
 */
.ant-cell > .card.clickable:hover { transform: none; }

/* Ready (nothing selected): an own card with a command. */
.ant-cell > .card.ant-ready { outline: 1px solid var(--accent); outline-offset: 1px; }
/* Selected: lifted 2px by the tap, never by a hover. */
.ant-cell > .card.ant-sel,
.ant-cell > .card.ant-sel:hover {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    transform: translateY(-2px);
}
/* Joins the line: tapping it extends the selection (D2). */
.ant-cell > .card.ant-join { outline: 2px dashed var(--accent); outline-offset: 1px; }
/* An attack target: a red ring as a shadow, so it never fights an outline. */
.ant-cell > .card.ant-ring {
    box-shadow: 0 0 0 3px #ff3b3b, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
/*
 * The platform's two outline states win back over the markers above (build
 * review 16). The keyboard focus ring (.card.clickable:focus-visible) and the
 * tutorial's highlight (.card.hinted, .coach-mark, pulsed by hintpulse on
 * outline-color) are outlines too, and the ready and joins-the-line marks,
 * later in the sheet at (0,3,0), would otherwise shrink both to a 1px or
 * dashed line on exactly the cards a player tabs to and a beat points at.
 */
.ant-cell > .card.hinted,
.ant-cell > .card.coach-mark { outline: 3px solid var(--accent); outline-offset: 2px; }
.ant-cell > .card.clickable:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
/* Faded with a selection: a filter, never opacity (R13.7). */
.ant-cell > .card.ant-dim { filter: brightness(0.9) saturate(0.7); }

/* A lit empty destination: accent tint and a dashed accent border. */
.ant-cell > .card.empty.ant-lit {
    background: rgba(255, 209, 102, 0.22);
    border: 2px dashed var(--accent);
}
.ant-cell > .card.empty.ant-lit:hover { background: rgba(255, 209, 102, 0.36); }

/* The hover and focus previews (IDLE), one step quieter than a selection's
   marks, so a look never reads as a choice. */
.ant-cell > .card.empty.ant-pv-lit {
    background: rgba(255, 209, 102, 0.14);
    border: 1px dashed var(--accent);
}
.ant-cell > .card.ant-pv-ring {
    box-shadow: 0 0 0 2px #ff3b3b, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.ant-cell > .card.ant-pv-swap {
    box-shadow: 0 0 0 2px var(--accent), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
/*
 * The March footprint (hover or focus on a target): a dashed card-sized
 * outline on every landing cell, a slot or one of the line's own cards. Drawn
 * by the wrapper, inside the last command's mark, so the selected cards keep
 * their gold outline under it. Dark dashes with a white halo, so it shows on a
 * white face and on a dark slot alike (Minigolf's ghost ball).
 */
.ant-cell:has(> .ant-foot)::after {
    content: '';
    position: absolute;
    inset: 4px;
    border: 2px dashed #16181d;
    border-radius: 6%;
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.85);
    pointer-events: none;
    z-index: 2;
}

/* ---- the last command (D3) ---- */

/*
 * On the wrapper, drawn just inside the cell's edge and above the card, so a
 * selection outline or an attack ring outside the card never hides it, and
 * it never hides them. One mark per cell (runMarks keeps the latest).
 */
.ant-cell.ant-last-from::before,
.ant-cell.ant-last-to::before,
.ant-cell.ant-last-took::before {
    content: '';
    position: absolute;
    inset: 0;
    border: 2px solid #6aa9e0;
    border-radius: 6%;
    pointer-events: none;
    z-index: 1;
}
.ant-cell.ant-last-from::before { border-style: dotted; }
/* Where a card fell: a centered × on the empty slot, which has no ink to clear. */
.ant-cell.ant-last-fell::before {
    content: '\00d7';
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #6aa9e0;
    font-size: calc(0.6 * var(--ant-cw));
    font-weight: 700;
    line-height: 1;
    pointer-events: none;
    z-index: 1;
}

/* ---- badges: one per cell, in the left edge band ---- */

/*
 * Minigolf's measured ball, unchanged: 0.32 cw across, 0.03 cw out past the
 * left edge, its top 0.56 cw down, so it is centered 0.72 cw down the face.
 * Measured on this atlas to clear the rank and suit ink of all 52 faces, and
 * away from the corners where four cards meet (R13.6). Near-black with a
 * white rim, because the face under it is white. 12px on a 360px phone.
 */
.ant-cell > .ant-badge {
    position: absolute;
    left: calc(-0.03 * var(--ant-cw));
    top: calc(0.56 * var(--ant-cw));
    width: calc(0.32 * var(--ant-cw));
    height: calc(0.32 * var(--ant-cw));
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0;
    padding: 0;
    border-radius: 50%;
    border: 1.5px solid #ffffff;
    background: #14161b;
    color: #ffffff;
    font-family: inherit;
    font-size: calc(0.24 * var(--ant-cw));
    font-weight: 700;
    line-height: 1;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
    z-index: 3;
}
/* Every badge but the ⇄ is display only, so a tap on it reaches the card
   beneath: attacking a ringed card, or selecting an endangered Queen (build
   review 12, NIT 1). */
.ant-cell > .ant-badge:not(.ant-swap) { pointer-events: none; }
/* The Queen in danger (Battle only): a red dot, no glyph. */
.ant-cell > .ant-badge.ant-danger { background: #ff3b3b; }
/* An Attack that took (the last command): the last command's blue, so it
   never reads as an outcome badge, which is near-black inside a red ring.
   Its × is the near-black badge ink, about 7.5:1 on the blue, because white
   on it is about 2.4:1 (build review 16). */
.ant-cell > .ant-badge.ant-took { background: #6aa9e0; color: #14161b; }
/*
 * The Swap badge is a button: accent fill with the white rim. Its glyph is
 * the accent ink rather than white, because white on the accent is about
 * 1.4:1 (the same pairing every `.btn.primary` uses).
 */
.ant-cell > .ant-badge.ant-swap {
    appearance: none;
    -webkit-appearance: none;
    background: var(--accent);
    color: var(--accent-ink);
    cursor: pointer;
}
/*
 * The swap badge's tap area: the gap left of the partner plus the partner's
 * ink-free left band, x in [-gap, 0.25cw] and y in [0.50cw, 0.94cw] of the
 * partner card. Never over another card element, so a tap on the selected
 * card or on the middle of the partner cannot commit a Swap (D2). The offsets
 * are relative to the badge box (left -0.03 cw, top 0.56 cw).
 */
.ant-cell > .ant-badge.ant-swap::before {
    content: '';
    position: absolute;
    left: calc(0.03 * var(--ant-cw) - var(--ant-gap));
    width: calc(var(--ant-gap) + 0.25 * var(--ant-cw));
    top: calc(-0.06 * var(--ant-cw));
    height: calc(0.44 * var(--ant-cw));
}

/* ---- the rail: two rosters around the action buttons ---- */

/* As tall as the board (the stretch above); the middle row absorbs a
   three-line button, so the rail's own height never changes (R14.6). */
.ant-rail {
    display: grid;
    grid-template-rows: auto 1fr auto;
    gap: 8px;
    min-width: 0;
}
.ant-actions {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 6px;
    min-height: 0;
}
/* 44px is the minimum, not the height: a label may wrap to three lines. */
.ant-actions > .btn.ant-action {
    width: 100%;
    min-height: 44px;
    padding: 6px 8px;
    font-size: 14px;
    line-height: 1.2;
    white-space: normal;
    overflow-wrap: break-word;
}

/* A roster: the seat and army, then 9 slots, Ace first (D3). */
.ant-roster { min-width: 0; text-align: center; }
.ant-rtitle {
    font-size: 12px;
    font-weight: 600;
    opacity: 0.9;
    margin-bottom: 3px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* At the 168px rail: one row of 9 columns, each the value over the two
   glyphs stacked. Fixed tracks, so the slots never shift as cards fall. */
.ant-rgrid {
    display: grid;
    grid-template-columns: repeat(9, minmax(0, 1fr));
    gap: 1px;
}
.ant-rslot {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 2px 0;
    border-radius: 4px;
    background: rgba(0, 0, 0, 0.18);
    font-size: 12px;
    line-height: 1.15;
    white-space: nowrap;
}
.ant-rval { font-weight: 700; }
/* The suit glyphs in their own inks; the platform scopes its bare suit
   modifiers to the log, so the roster opts in here. Spades keep the ink. */
.ant-roster .ant-rsuit.red { color: var(--red); }
.ant-roster .ant-rsuit.red.diamond { color: var(--red-diamond); }
.ant-roster .ant-rsuit.club { color: var(--blue-club); }
/* A fallen card: dimmed and struck through. The slot's aria-label says it in
   words (build review 12, NIT 3). */
.ant-roster .ant-rsuit.ant-gone {
    opacity: 0.4;
    text-decoration: line-through;
    text-decoration-thickness: 1.5px;
}

/* ---- status and progress ---- */

/* Reserved at the longest status (a selected card with its legend, the
   Queen's Favor hand-over, or an auto-pass with the Queen under attack, each
   with a 24-character online name): about 88 characters. Two lines above
   560px, three at 560px and below with Techu's smaller phone font. Measured
   by tools/armyants/armyants-probe.mjs. */
.status.ant-status { line-height: 1.35; min-height: calc(2 * 1.35em); }
/* One line reserved: "Draw in 10 turns unless a card falls" is the longest. */
.progress.ant-progress { white-space: nowrap; }

/* ---- narrower stages and screens ---- */

/* The stage under 560px: the 112px rail, and the roster as a 3 by 3 grid
   (A to 3, 4 to 6, 7 to 9), each slot one line such as "A ♥♦". Text stays
   at 11px or more. */
@container (max-width: 559px) {
    .ant-main {
        --ant-gap: 3px;
        --ant-pad: 4px;
        --ant-rail: 112px;
        --ant-railgap: 8px;
    }
    .ant-rail { gap: 6px; }
    .ant-actions > .btn.ant-action { padding: 5px 6px; font-size: 13px; }
    .ant-rtitle { font-size: 11px; margin-bottom: 2px; }
    .ant-rgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 2px; }
    .ant-rslot {
        flex-direction: row;
        justify-content: center;
        gap: 2px;
        padding: 1px 0;
        font-size: 11px;
    }
}
/* The status line sits outside the stage container, so its reserve follows
   the viewport. */
@media (max-width: 560px) {
    /* Measured by the probe at 360x780 (task 19.2, 2026-10-06): the board's
       top at 150.5px plus the 10px bottom padding, rounded up. The phones
       stay width-bound: 39px cards at 360px, 45px at 390px. */
    .ant-main { --ant-chrome: 161px; }
    .status.ant-status { font-size: 15px; line-height: 1.3; min-height: calc(3 * 1.3em); }
}
/* ------------------------------------------------------------------------- */
/* Grit                                                                      */
/* ------------------------------------------------------------------------- */
/*
 * Prefix policy: every Grit-specific class is `grt-`. Shared classes are
 * reused as they are: hand cards select with `.card.sel`, arm with
 * `.clickable`, flash with `.hinted`; backs are `.card.simple.back`; the
 * outcome uses `.winner-banner`, `.result-note` and `.result-actions`.
 * Shared ids (#hand-cards, #status-line) are reached only through a game
 * class (.grt-handpanel, .grt-status).
 *
 * NOTHING MOVES (ui-conventions.md, "Containers must not move"):
 *  - every slot is a fixed box, as tall as a card plus two bands
 *    (--grt-h + 2 * --grt-b), so an upright card, a sideways Wager, an empty
 *    recess and the target bands all fit inside it;
 *  - the TOP card of a slot (face up or face down) always sits in the slot's
 *    own rectangle, the recess an empty slot shows (--grt-b from the top), and
 *    the card it covers peeks out above it in the band over the recess; a
 *    Wager lies across the middle of that rectangle;
 *  - the target bands are inside the slot box;
 *  - the totals are nowrap with the widest form's min-width;
 *  - the Unseen row is nowrap, one tile tall and six tiles wide at least;
 *  - the two status lines each reserve their tallest form (measured).
 *
 * Nothing on the board is dashed: an empty slot is a recess in the felt.
 */
.grt-table {
    --grt-cw: 72px;
    --grt-h: calc(var(--grt-cw) * 89 / 64);
    --grt-b: calc(var(--grt-h) * 0.12);
    --grt-gap: 14px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
}
.grt-board, .grt-unseen { padding: 8px 12px 10px; margin-bottom: 8px; }
/* One fixed-height line: the title and the total centered as a pair. The
   total's ? tiles are taller than its text, so the head's height is set here
   rather than left to its content (measured: 2px of walk otherwise). */
.grt-boardhead { min-height: 0; height: 28px; margin-bottom: 4px; align-items: center; flex-wrap: nowrap; }
.grt-boardhead .grt-boardtitle { margin: 0; text-align: center; white-space: nowrap; }
.grt-unseen .panel-head { min-height: 22px; }
.grt-unseen .grt-unseentitle { margin: 0 0 6px; text-align: center; }
.grt-slots {
    display: flex;
    justify-content: center;
    gap: var(--grt-gap);
}

/* --- a slot -------------------------------------------------------------- */
.grt-slot {
    position: relative;
    flex: none;
    width: var(--grt-h);
    height: calc(var(--grt-h) + 2 * var(--grt-b));
}
.grt-slot > .card {
    --cw: var(--grt-cw);
    position: absolute;
    left: calc(50% - var(--cw) / 2);
}
.grt-slot > .card.grt-peek { top: 0; filter: brightness(0.75); }
/* The slot's own rectangle: the same box as the empty recess below. */
.grt-slot > .card.grt-up,
.grt-slot > .card.grt-down { top: var(--grt-b); }
/* The Wager lies sideways across the box, centered on the slot's rectangle
   (the box's own center). `translate` and `rotate`, never
   `transform`, scoped under the slot so a flight ghost (a clone on <body>)
   takes its turn from the animator alone (ui-conventions.md). */
.grt-slot > .card.grt-wager {
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    rotate: 90deg;
}
/* Your own face-down card's value, read upright on a sideways Wager too. */
.grt-slot > .card > .grt-known {
    position: absolute;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    padding: 2px 6px;
    border-radius: 6px;
    border: 1px solid rgba(0, 0, 0, 0.35);
    background: #f2efe9;
    color: #1a1a1a;
    font-size: 16px;
    font-weight: 800;
    line-height: 1.1;
    white-space: nowrap;
    pointer-events: none;
}
.grt-slot > .card.grt-wager > .grt-known { rotate: -90deg; }
.grt-known .red { color: #d94a2f; }
/* An empty slot: a card-shaped recess where a card would lie. */
.grt-slot.grt-empty::before {
    content: '';
    position: absolute;
    top: var(--grt-b);
    left: calc(50% - var(--grt-cw) / 2);
    width: var(--grt-cw);
    height: var(--grt-h);
    border-radius: 7px;
    background: rgba(0, 0, 0, 0.2);
    box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.1);
}

/* --- the targets (a card is picked) ------------------------------------- */
.grt-slot > .grt-target {
    position: absolute;
    z-index: 1;
    left: 0;
    width: 100%;
    box-sizing: border-box;
    margin: 0;
    padding: 0 0 6px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    gap: 3px;
    border: 2px solid rgba(255, 209, 102, 0.75);
    border-radius: 9px;
    background: rgba(12, 32, 20, 0.55);
    color: #fff;
    font: inherit;
    font-size: 13px;
    font-weight: 700;
    cursor: pointer;
}
.grt-slot > .grt-target:hover,
.grt-slot > .grt-target:focus-visible {
    background: rgba(255, 209, 102, 0.28);
    border-color: var(--accent);
    outline: none;
}
.grt-slot > .grt-target.grt-tfull { top: 0; height: 100%; }
.grt-slot > .grt-target.grt-ttop,
.grt-slot > .grt-target.grt-tbottom { height: 50%; min-height: 44px; }
.grt-slot > .grt-target.grt-ttop { top: 0; border-radius: 9px 9px 2px 2px; }
.grt-slot > .grt-target.grt-tbottom {
    bottom: 0;
    border-radius: 2px 2px 9px 9px;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    padding: 0;
    gap: 5px;
}
.grt-slot > .grt-target.grt-ttop { justify-content: center; padding: 0; }
.grt-slot > .grt-target.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
    outline-offset: 1px;
}
.grt-tlabel { white-space: nowrap; }
/* A small card lying sideways: the Wager's shape. */
.grt-tglyph {
    display: inline-block;
    width: 16px;
    height: 11px;
    border-radius: 2px;
    background: #f2efe9;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);
}
body.motion-off .grt-slot > .grt-target.hinted { animation: none; }

/* --- the board totals ---------------------------------------------------- */
.grt-sum {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 3px;
    min-width: 6.5em;
    height: 26px;
    white-space: nowrap;
    font-size: 15px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.grt-sum .grt-qback {
    --cw: 18px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 12px;
    font-weight: 800;
    text-shadow: 0 0 2px #000;
}
.grt-sum.grt-over { color: var(--red); }
.grt-sum.grt-over .grt-sumnum { text-decoration: line-through; }

/* --- the Unseen row ------------------------------------------------------ */
.grt-unseenrow {
    --cw: 34px;
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: 4px;
    min-height: calc(34px * 89 / 64);
    min-width: calc(6 * 34px + 5 * 4px);
    margin: 0 auto;
}
.grt-unseenrow .grt-qheart {
    width: var(--cw);
    height: calc(var(--cw) * 89 / 64);
    flex: none;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 6%;
    background: #fff;
    box-shadow: 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
    font-size: 16px;
    font-weight: 800;
    line-height: 1;
}
.grt-unseenrow .grt-qheart, .grt-unseenrow .grt-qheart .red { color: #d94a2f; }

/* --- the hand ------------------------------------------------------------ */
.grt-handpanel #hand-cards .card { --cw: 88px; }

/* --- status and progress ------------------------------------------------- */
/* Each line reserves its tallest form, measured in Chromium with the hotseat
   names and the real bold card labels (probes/status-wrap.mjs, 2026-10-06):
   line 1 is one line at 1280 and two at 390, where "Round 3: Player 1 plays
   first (10♥ beats J♠)." wraps; line 2 is one line at 1280 and two at 390,
   where the hint "Hint: play 10♠ face down on Player 2's empty slot." wraps
   (48.6px). The first measurement set plain text and missed the wider bold
   labels, which let the boards walk 24px in a hotseat set at 390. */
.status.grt-status { line-height: 1.35; }
.grt-status .grt-status1 { min-height: 1.35em; }
.grt-status .grt-status2 { min-height: 1.35em; }
.grt-status .cardlabel { white-space: nowrap; }
/* The shell's fallback strip, kept out of the page during the final flip
   (renderResult in def.ts says why). */
.endgame-line.grt-endhold { display: none; }
.grt-error { margin-top: 4px; font-size: 13px; color: #ff9c8a; }
.progress.grt-progress {
    opacity: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    flex-wrap: wrap;
    min-height: 34px;
}
.grt-progress .grt-settarget { font-size: 13px; opacity: 0.7; }

/* --- the result ---------------------------------------------------------- */
.grt-result {
    border-collapse: collapse;
    margin: 6px auto 4px;
    font-size: 14px;
}
.grt-result th, .grt-result td { padding: 3px 10px; text-align: left; vertical-align: middle; }
/* Every cell in a seat column ends on the same right edge: the numbers, the
   Wager, and the Board row's cards (ui-conventions, "Score tables line up by
   column"). */
.grt-result th.num, .grt-result td.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
    min-width: 3.5em;
}
.grt-result td.grt-rboard { --cw: 34px; text-align: right; white-space: nowrap; }
.grt-result td.grt-rboard > .card,
.grt-result td.grt-rboard > .grt-rnone,
.grt-result td.grt-rboard > .grt-rwagerbox { display: inline-block; vertical-align: middle; margin-left: 3px; }
.grt-result td.grt-rboard > .grt-rnone { width: var(--cw); text-align: center; opacity: 0.6; }
/* The Wager sideways: its box is as wide as the card is tall, so it overlaps
   nothing, and the turn is `rotate`, scoped under the box. */
.grt-result td.grt-rboard > .grt-rwagerbox {
    position: relative;
    width: calc(var(--cw) * 89 / 64);
    height: calc(var(--cw) * 89 / 64);
}
.grt-result .grt-rwagerbox > .card {
    position: absolute;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    rotate: 90deg;
}
.grt-result td.grt-over { color: var(--red); text-decoration: line-through; }

/* --- a phone ------------------------------------------------------------- */
@media (max-width: 720px) {
    .grt-table { --grt-cw: 64px; --grt-gap: 8px; }
    .grt-handpanel #hand-cards .card { --cw: 72px; }
    .grt-board, .grt-unseen { padding: 6px 8px 8px; }
    .grt-slot > .grt-target { font-size: 12px; }
    .grt-status .grt-status1 { min-height: calc(2 * 1.35em); }
    .grt-status .grt-status2 { min-height: calc(2 * 1.35em); }
    /* The result table fits its dialog at 360px with a board of card,
       sideways Wager, card in both seat columns (probes/r1-contain.mjs). At
       34px it ran 26px past the content box at 390. */
    .grt-result th, .grt-result td { padding: 3px 6px; }
    .grt-result td.grt-rboard { --cw: 26px; }
}
/* ------------------------------------------------------------------------- */
/* Rolldown                                                                  */
/* ------------------------------------------------------------------------- */
/*
 * Prefix policy: every Rolldown class is `rld-` and every id `#rld-`. Shared
 * ids are reached only through the game's own classes (`.rld-handpanel`,
 * `.rld-status`, `.rld-progress`); nothing here sets a `min-height` or
 * `justify-content` on `#hand-cards` (src/shared/ui/handspace.ts reserves the
 * hand at runtime).
 *
 * EVERY ROW RESERVES ITS HEIGHT (design, "Board layout", D5): the seat rows,
 * both dice rows (six fixed slots, each holding its die or an empty outline
 * and reserving the band of the die's reroll button whether or not one is
 * painted), the strip of six fixed pair slots, and the status and progress
 * lines. The cards overlay is an absolute layer, so opening it moves nothing.
 *
 * DICE NEVER TAKE A `transform`: the roll cue turns the SVG with the
 * independent `rotate` property and the set cue grows it with `scale`, so the
 * shared flight ghost (a clone of the die) cannot inherit a transform, and
 * `body.motion-off` stops both (P20).
 */
/* ---- sizes: one set of custom properties on the board ---- */
.rld-board {
    --rld-die: clamp(44px, min(8vw, 7vh), 64px);
    /* Each die's reroll button (follow-up 2026-10-10): its visible chip, the
       gap above it, and the band every slot reserves for both. */
    --rld-rrchip: 20px;
    --rld-rrgap: 2px;
    --rld-rrband: 22px;
    --rld-mini: 30px;
    --rld-seatcw: 30px;
    --rld-rowgap: 6px;
    --rld-diegap: 10px;
    --rld-slot-w: calc(var(--rld-mini) + 12px);
    --rld-ring: var(--accent, #ffd166);
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    row-gap: var(--rld-rowgap);
    margin-bottom: 14px;
}
@media (max-width: 719px) {
    .rld-board {
        --rld-mini: 26px;
        --rld-seatcw: 26px;
        --rld-rowgap: 4px;
    }
}
@media (max-width: 560px) {
    .rld-board { --rld-diegap: 6px; }
}
/* On a touch screen the band is the 44px tap target (plus a pixel either
   side, so it touches neither its die nor the row below), with the chip
   centered in it. */
@media (pointer: coarse) {
    .rld-board {
        --rld-rrchip: 22px;
        --rld-rrgap: 11px;
        --rld-rrband: 46px;
    }
}
/* ---- seat rows ---- */
.rld-seat {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    row-gap: 2px;
    width: 100%;
}
.rld-seatline {
    display: flex;
    align-items: center;
    justify-content: center;
    column-gap: 8px;
    min-height: calc(var(--rld-seatcw) * 1.39 + 4px);
    white-space: nowrap;
}
@media (min-width: 720px) {
    /* One line at 720px and up: the far row's cards join its first line. */
    .rld-seat.rld-far { flex-direction: row; justify-content: center; column-gap: 8px; }
}
.rld-name {
    display: inline-block;
    max-width: 9em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
}
@media (max-width: 719px) {
    .rld-name { max-width: 6em; }
}
.rld-seatname { font-weight: 700; }
.rld-first {
    display: inline-block;
    padding: 1px 6px;
    border-radius: 8px;
    background: var(--rld-ring);
    color: var(--accent-ink, #3a2c00);
    font-size: 12px;
    font-weight: 700;
    white-space: nowrap;
}
.rld-first.rld-hidden { visibility: hidden; }
.rld-deck {
    display: inline-block;
    min-width: 4.5em;
    text-align: center;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    opacity: 0.9;
}
.rld-farhand {
    display: flex;
    align-items: center;
    justify-content: center;
    column-gap: 3px;
    min-width: calc(3 * var(--rld-seatcw) + 6px);
    min-height: calc(var(--rld-seatcw) * 1.39);
}
.rld-board .card.rld-seatcard { --cw: var(--rld-seatcw); }
.rld-played {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: calc(var(--rld-seatcw) + 8px);
    height: calc(var(--rld-seatcw) * 1.39 + 4px);
    padding: 0;
    border: 1px dashed rgba(255, 255, 255, 0.45);
    border-radius: 5px;
    background: transparent;
    cursor: pointer;
}
.rld-played:not(.rld-empty) { border-style: solid; }
.rld-played:disabled { cursor: default; }
.rld-played.rld-last { outline: 2px dotted var(--rld-ring); outline-offset: 1px; }
/*
 * ---- the ability label on a card's face (follow-up 2026-10-22) ----
 * Min, Max, Min/Max, 2 Rerolls, and Reroll All (def.ts `labelFace`), a dark
 * band with a light rim so it reads on red and black art alike, centered in
 * the lower middle of the face: its bottom edge 67% of the way down, just
 * above the bottom-right corner index (which starts 68% down), and, when a
 * long label wraps, its top still under the top-left index (32% down) at the
 * hand's 40px floor. It sits inside the card's own box, so no card and no
 * container changes size.
 *
 * SIZED BY THE CARD'S OWN WIDTH. The face is the label's size container, so
 * `cqw` is 1% of the card as laid out: `var(--cw)` wherever a container sets
 * `--cw`, and the ghost's own width in flight. A flight ghost is a clone in
 * <body>, where the container rule that set `--cw` no longer matches (there a
 * 30px seat card would read `.card.mini`'s 46px), so a size written in
 * `var(--cw)` would draw a ghost's label for the wrong card. 14cqw is the
 * largest size at which every label fits on one line clear of the Joker's
 * two side "JOKER" words (16% and 84% across). Below a 57px card the 8px
 * floor takes over, a long label on one line would reach those words, so it
 * wraps at its space, or after the slash (a <wbr>), and the band hugs its
 * widest word (`min-content`) rather than spanning 78% of the card.
 *
 * SHOWN ON A CARD AT LEAST 38px WIDE (brief L5, the user's ruling).
 * Narrower than that, "Rerolls" cannot fit at 8px inside the card, and even
 * a one-line label covers most of what a seat card shows, so the label is
 * left off and the card's name (its tooltip and accessible name, "Queen of
 * Spades: 2 Rerolls") carries it. def.ts builds no band at all on the board's
 * 30px and 26px seat cards (both played slots, the overlay's rows, the far
 * row's revealed leftovers); the container rule below leaves it off the
 * summary's 34px leftovers on a phone.
 *
 * ON THE SIMPLIFIED SPRITE IT SITS AT THE FOOT, ON ONE LINE. The only
 * simplified faces that draw a label are the round summary's leftovers, 46px
 * on a desktop. That sprite has no corner indices: its rank fills the upper
 * half and its suit the lower half, with almost no gap between them. In the
 * lower middle, a two-line label covers the foot of the rank (the Q's tail,
 * the A's crossbar) and the top of the suit. So on `.simple` the band sits 4%
 * off the bottom edge on one line, 79% to 96% down. The rank stays whole,
 * and the suit's top, the part that tells a heart from a spade or a club,
 * stays clear.
 *
 * A FLIGHT TAKES ITS DESTINATION'S LABEL, AS IT TAKES ITS ART. A ghost is a
 * deep clone of where the card was, so a hand card's ghost carries the band.
 * `matchDestinationArt` (shared/ui/animate.ts) gives a ghost `simple` from
 * lift-off exactly when it lands on a simplified card. In Rolldown every
 * simplified card a flight can land on is a played slot, which draws no
 * label (L5). So a simplified ghost hides the band from its first frame, and
 * the label leaves with the art at lift-off rather than vanishing on landing
 * (ui-conventions.md, "A flight paints its destination's art from
 * lift-off"). Summary leftovers are copies with no card id, so they are never
 * a flight's destination.
 *
 * Never positioned with `transform` (a hover lift, the pop-in, and a flight
 * ghost all write it), never faded on its own (a dimmed card's filter fades
 * it with the card), and never a target (`pointer-events: none`). The card's
 * `position: relative` is at zero specificity so a ghost's
 * `.card-fly { position: fixed }` still wins. `white-space: normal` because
 * the seat rows it can sit in are `nowrap`.
 */
:where(.card.rld-ability) { position: relative; container: rld-face / inline-size; }
.card > .rld-cardlbl {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 33%;
    box-sizing: border-box;
    width: max-content;
    max-width: 78%;
    margin: 0 auto;
    padding: 2cqw 4cqw 1cqw;
    border: 1px solid rgba(255, 255, 255, 0.85);
    border-radius: 5cqw;
    background: rgba(16, 16, 16, 0.88);
    color: #ffffff;
    font-size: max(8px, 14cqw);
    font-weight: 600;
    line-height: 0.95;
    text-align: center;
    white-space: normal;
    pointer-events: none;
}
@container rld-face (max-width: 57.13px) {
    .card > .rld-cardlbl { width: min-content; }
}
@container rld-face (max-width: 37.98px) {
    .card > .rld-cardlbl { display: none; }
}
.card.simple > .rld-cardlbl { bottom: 4%; width: max-content; max-width: 96%; }
.card-fly.simple > .rld-cardlbl { display: none; }
/* ---- dice rows ---- */
.rld-dicerow {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    column-gap: var(--rld-diegap);
    /* 6px each side: the "was N" badge (left: -6px) and the result tag
       (right: -6px) on an end die stay inside the row (probe, overflow). */
    padding: 2px 6px;
}
/* A slot: the die (or its empty socket), then the band of its reroll button,
   one fixed box whatever it holds. */
.rld-dieslot {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: var(--rld-die);
    height: calc(var(--rld-die) + 14px + var(--rld-rrband));
    flex: none;
}
.rld-die {
    position: relative;
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    width: var(--rld-die);
    height: calc(var(--rld-die) + 14px);
    padding: 0;
    margin: 0;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    flex: none;
}
button.rld-die { cursor: pointer; }
button.rld-die:focus-visible { outline: 3px solid var(--rld-ring); outline-offset: 2px; }
.rld-svg {
    /* Positioned so it paints over the last-action halo (`::before`, below). */
    position: relative;
    display: block;
    width: var(--rld-die);
    height: var(--rld-die);
    overflow: visible;
}
.rld-face {
    font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
    font-variant-numeric: tabular-nums;
    pointer-events: none;
}
.rld-ring { stroke: transparent; stroke-width: 0; }
.rld-cap {
    display: block;
    height: 14px;
    line-height: 14px;
    font-size: 11px;
    white-space: nowrap;
    opacity: 0.85;
}
.rld-badge, .rld-tag {
    position: absolute;
    top: -4px;
    padding: 0 3px;
    border-radius: 6px;
    font-size: 10px;
    line-height: 14px;
    font-weight: 700;
    white-space: nowrap;
    pointer-events: none;
}
.rld-badge { left: -6px; background: var(--rld-ring); color: var(--accent-ink, #3a2c00); }
.rld-tag { right: -6px; background: #ffffff; color: #1a1a1a; }
.rld-badge:empty, .rld-tag:empty { display: none; }
/* State rings, stroked on the silhouette (design, "State classes"). */
.rld-die.rld-cancap .rld-ring { stroke: var(--rld-ring); stroke-width: 4; stroke-dasharray: 8 6; }
.rld-die.rld-lit .rld-ring { stroke: var(--rld-ring); stroke-width: 6; }
/* `rld-rrprev`: the die whose reroll button is hovered or focused takes the selection ring. */
.rld-die.rld-sel .rld-ring, .rld-die.rld-rrprev .rld-ring { stroke: var(--rld-ring); stroke-width: 8; }
.rld-die.rld-sel .rld-svg, .rld-die.rld-rrprev .rld-svg { filter: drop-shadow(0 0 4px var(--rld-ring)); }
.rld-die.rld-preview .rld-ring { stroke: var(--rld-ring); stroke-width: 3; stroke-opacity: 0.6; }
/*
 * The opponent's last action on a die has its OWN channel, a pale halo
 * behind the die plus the "was N" badge, so it never replaces a ring state:
 * a die the opponent just changed that can capture keeps its dashed
 * `rld-cancap` ring (build review 4, finding 4; the probe checks a die with
 * both). The halo sits inside the row's 6px side padding.
 */
.rld-die.rld-last::before {
    content: '';
    position: absolute;
    left: -5px;
    top: -5px;
    width: calc(var(--rld-die) + 10px);
    height: calc(var(--rld-die) + 10px);
    border-radius: 50%;
    background: radial-gradient(closest-side, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.3) 72%, rgba(255, 255, 255, 0));
    pointer-events: none;
}
.rld-die.rld-dim .rld-svg { filter: saturate(0.4) brightness(0.88); }
/* A number card with no legal target: the same fade, never opacity. */
.rld-handpanel .card.rld-dim { filter: saturate(0.4) brightness(0.88); }
.rld-socket .rld-outline { stroke: rgba(255, 255, 255, 0.35); }
.rld-socket .rld-cap { opacity: 0.5; }
/*
 * ---- each die's reroll button (follow-up 2026-10-10) ----
 * A small chip under the caption, quiet at rest so twelve of them do not
 * crowd the board, full strength on hover, on focus, and while its die is
 * selected (`rld-on`). Unarmed it is `disabled` and faded with a filter, never
 * opacity, and keeps its box. On a coarse pointer the chip stays small and an
 * invisible `::after` makes the tap target the slot's width by 44px, starting
 * a pixel under the die and ending inside the slot (platform.css's
 * coarse-pointer `.panel-head` button is the pattern).
 */
.rld-rerollbtn {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    flex: none;
    width: min(32px, var(--rld-die));
    height: var(--rld-rrchip);
    margin: var(--rld-rrgap) 0 0;
    padding: 0;
    border: 1px solid rgba(255, 255, 255, 0.32);
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.2);
    color: rgba(255, 255, 255, 0.82);
    font: inherit;
    cursor: pointer;
}
.rld-rrglyph { display: block; width: 14px; height: 14px; overflow: visible; pointer-events: none; }
.rld-rerollbtn.rld-on,
.rld-rerollbtn:enabled:hover,
.rld-rerollbtn:enabled:focus-visible {
    border-color: var(--rld-ring);
    background: var(--rld-ring);
    color: var(--accent-ink, #3a2c00);
}
.rld-rerollbtn:focus-visible { outline: 2px solid #ffffff; outline-offset: 2px; }
.rld-rerollbtn:disabled { cursor: default; filter: saturate(0.4) brightness(0.85); }
@media (pointer: coarse) {
    .rld-rerollbtn::after {
        content: '';
        position: absolute;
        top: calc(0px - var(--rld-rrgap));
        left: 50%;
        width: var(--rld-die);
        height: 44px;
        margin-left: calc(var(--rld-die) / -2);
    }
}
/* ---- cues: rotate and scale, never transform ---- */
@keyframes rld-shake {
    0% { rotate: 0deg; }
    20% { rotate: -10deg; }
    40% { rotate: 8deg; }
    60% { rotate: -6deg; }
    80% { rotate: 3deg; }
    100% { rotate: 0deg; }
}
@keyframes rld-pulse {
    0% { scale: 1; }
    50% { scale: 1.15; }
    100% { scale: 1; }
}
.rld-svg.rld-rolling { animation: rld-shake 480ms ease-out; }
.rld-svg.rld-setting { animation: rld-pulse 320ms ease-out; }
.rld-svg.rld-setting .rld-ring { stroke: var(--rld-ring); stroke-width: 6; }
body.motion-off .rld-svg.rld-rolling,
body.motion-off .rld-svg.rld-setting { animation: none; }
/* ---- the strip of captured pairs ---- */
.rld-strip {
    display: flex;
    justify-content: center;
    column-gap: 6px;
    padding: 2px 0;
}
.rld-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: var(--rld-slot-w);
    height: calc(2 * var(--rld-mini) + 2px);
    overflow: hidden;
    white-space: nowrap;
}
.rld-slot.rld-emptyslot {
    border: 1px dashed rgba(255, 255, 255, 0.3);
    border-radius: 6px;
    box-sizing: border-box;
}
.rld-lastlbl { font-size: 10px; line-height: 12px; }
.rld-lastpts { font-size: 14px; line-height: 16px; font-weight: 700; font-variant-numeric: tabular-nums; }
.rld-pair {
    display: flex;
    flex-direction: column;
    row-gap: 2px;
    border-radius: 6px;
    outline: 1px solid rgba(255, 255, 255, 0.2);
}
.rld-pair.rld-newest { outline: 2px solid rgba(255, 255, 255, 0.55); }
.rld-pair.rld-last { outline: 2px dotted var(--rld-ring); }
.rld-pairrow { display: flex; align-items: center; column-gap: 2px; }
.rld-mini { display: inline-block; width: var(--rld-mini); height: var(--rld-mini); }
.rld-mini .rld-svg { width: var(--rld-mini); height: var(--rld-mini); }
.rld-chev { display: inline-block; width: 8px; font-size: 14px; font-weight: 700; line-height: 1; }
/* ---- the near bar: the near seat row, full width and centered ----
   It used to share a line with a fixed 230px controls row holding the one
   Reroll button; with a reroll button on every die that row is gone
   (follow-up 2026-10-10), so the near row is one full-width line at every
   width, like the far row's first line, and its box never moves sideways. */
.rld-nearbar {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
}
/* ---- the cards overlay (absolute: it moves nothing) ---- */
.rld-cards { display: none; }
.rld-cards.rld-open {
    position: absolute;
    inset: 0;
    z-index: 20;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    padding-top: 6px;
    background: rgba(0, 0, 0, 0.45);
    border-radius: inherit;
}
.rld-cardspanel {
    width: min(440px, calc(100% - 12px));
    max-height: calc(100% - 12px);
    overflow-y: auto;
    box-sizing: border-box;
    padding: 8px 10px 10px;
    border-radius: 8px;
    background: var(--felt-dark, #17492d);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5);
}
.rld-cardshead { position: relative; display: flex; justify-content: center; align-items: center; min-height: 31px; }
.rld-cardshead h3 { margin: 0; font-size: 15px; }
.rld-cardshead > .btn.small { position: absolute; right: 0; top: 0; }
.rld-cardsh { margin: 8px 0 4px; font-size: 13px; text-align: center; }
.rld-cardsnote { margin: 0 0 4px; font-size: 12px; text-align: center; opacity: 0.85; }
.rld-cardsrow { display: flex; flex-wrap: wrap; justify-content: center; gap: 3px; }
.rld-none { font-size: 12px; opacity: 0.7; }
/* ---- status and progress, through the game's own classes ---- */
.status.rld-status { min-height: 52px; }
.rld-status .rld-error { font-size: 13px; }
@media (max-width: 719px) {
    .status.rld-status { font-size: 15px; line-height: 20px; min-height: 40px; }
}
@media (max-width: 479px) {
    .status.rld-status { min-height: 60px; }
}
.progress.rld-progress {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    column-gap: 10px;
    row-gap: 2px;
}
.rld-progress .rld-pspan { white-space: nowrap; }
@media (max-width: 719px) {
    .progress.rld-progress { min-height: calc(2 * 1.4em + 2px); }
}
/* ---- the summaries: real tables with fixed columns ---- */
.rld-pairline { margin: 4px 0 8px; text-align: center; }
table.rld-score, table.rld-rounds {
    border-collapse: collapse;
    margin: 0 auto 8px;
    table-layout: fixed;
}
/* 7em + 6em + 6em: the bold "This round" header fits its column (build review 4, finding 3). */
table.rld-score { width: 19em; }
/* 4.5em + 6.5em + 7.5em + 4.5em: "Round", "d20 and d12", and "Points" fit
   their columns (the probe FAILs a clipped cell); 13px under 480px keeps the
   table inside the lifted panel at 360px. */
table.rld-rounds { width: 23em; }
@media (max-width: 479px) {
    table.rld-rounds { font-size: 13px; }
}
table.rld-score th, table.rld-score td,
table.rld-rounds th, table.rld-rounds td {
    padding: 2px 6px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
table.rld-score .rld-c1, table.rld-rounds .rld-c1 { text-align: left; }
table.rld-score .num, table.rld-rounds .num { text-align: right; font-variant-numeric: tabular-nums; }
table.rld-score th:first-child { width: 7em; }
table.rld-score th:nth-child(2), table.rld-score th:nth-child(3) { width: 6em; }
table.rld-rounds th:nth-child(1) { width: 4.5em; }
table.rld-rounds th:nth-child(2) { width: 6.5em; }
table.rld-rounds th:nth-child(3) { width: 7.5em; }
table.rld-rounds th:nth-child(4) { width: 4.5em; }
table.rld-rounds tr.total td { border-top: 1px solid rgba(255, 255, 255, 0.35); font-weight: 700; }
.rld-leftrow {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    margin: 4px 0;
    white-space: nowrap;
}
.rld-leftlbl { font-size: 13px; margin-right: 4px; }
/* ------------------------------------------------------------------------- */
/* Collapsi                                                                  */
/* ------------------------------------------------------------------------- */
/*
 * Prefix policy: every Collapsi class is `clp-` and every id `#clp-` or
 * `#opt-clp-`. Shared ids are reached only through the game's own classes
 * (`.clp-status`, `.clp-scoreboard`); nothing here names `#hand-cards` or
 * `#hand-panel`, which def.ts hides and empties every render.
 *
 * THE BOARD'S BOX NEVER CHANGES SIZE IN A ROUND (design D4): one knob,
 * `--clp-cw0`, the smaller of a width term and a height term computed from
 * the DEAL's side (3, 4, or 6), sets a fixed box; a grown Shifting frame's
 * cards shrink to fit inside it (`--clp-cw`), and the panel centers the grid
 * both ways. So the page never moves.
 *
 * SCOPING (design, "Stylesheet"): every board-only rule is under
 * `.clp-cell > ...` or `.clp-grid > ...`, so the body-mounted FLIP ghost
 * (`.card-fly`, position fixed) is never re-placed by a grid rule. What rides
 * INSIDE a card (the pawn token, the door and × badges, the mark rings) is
 * written in percent of the card's box under `.clp-card > ...`, so it rides
 * the ghost at the right size and spot.
 *
 * THE PAINT ORDER (D2), all inside the one stacking context `.clp-stage`
 * creates: shift arrows 4, tokens and badges 3, the `.clp-paths` overlay 2,
 * mark rings and preview marks 1. Nothing between the stage and a token sets
 * z-index, opacity below 1, filter, transform, isolation, will-change, or
 * contain; the one filter on the board is `.clp-dim`'s, and def.ts never puts
 * it on a card that holds a token or a badge.
 *
 * `--clp-chrome` and the status and progress reservations are MEASURED by
 * tools/collapsi/collapsi-probe.mjs (Task 19.1, 2026-10-10): the fit needs 245px of
 * chrome at the desktop views and 266.5px at 390 and 360 (the provisional 236
 * and 250 were short by 9 and 16.5px), and over every repaint of every probe
 * pass the status line, the chips row, and the action row held one height at
 * each view (the reservations below are those heights).
 */

/* ---- the stage and the knob (D4) ---- */

.clp-stage {
    container-type: inline-size;
    isolation: isolate;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    row-gap: 12px;
    margin-bottom: 14px;
}
/* Two classes and a child combinator, so it beats platform.css's `.panel`
   whatever the sheet order (the Army Ants form). */
.clp-stage > .panel.clp-board {
    /* set inline per render: --clp-n0 (deal side), --clp-cols, --clp-rows (frame) */
    --clp-n0: 4;
    --clp-cols: 4;
    --clp-rows: 4;
    --clp-gap: 3px;
    --clp-pad: 8px;
    --clp-gut: 12px;
    /* The page top to the panel's top border edge plus everything below its
       bottom edge that must stay on screen (the row gap, the action row,
       #screen-game's padding): measured 245px at 1280x800, 1280x720, and
       768x1024 by tools/collapsi/collapsi-probe.mjs, 2026-10-10. */
    --clp-chrome: 245px;
    --clp-vh: 100vh;
    padding: var(--clp-pad);
    margin: 12px 0 0;
    display: grid;
    place-content: center;
    /* The grid's tracks: two gutters, the cards, and a gap between every pair
       of tracks, so (n0 + 1) gaps across. The panel is border-box with a 1px
       border, hence the 2px. */
    --clp-cw0: clamp(28px, min(
        (100cqi - 2 * var(--clp-pad) - 2px - 2 * var(--clp-gut)
            - (var(--clp-n0) + 1) * var(--clp-gap)) / var(--clp-n0),
        (var(--clp-vh) - var(--clp-chrome) - 2 * var(--clp-pad) - 2px - 2 * var(--clp-gut)
            - (var(--clp-n0) + 1) * var(--clp-gap)) / (var(--clp-n0) * 1.390625)), 96px);
    /* The box: the deal's card area at --clp-cw0, fixed for the round. */
    --clp-boxw: calc(var(--clp-n0) * var(--clp-cw0) + (var(--clp-n0) - 1) * var(--clp-gap));
    --clp-boxh: calc(var(--clp-n0) * var(--clp-cw0) * 1.390625 + (var(--clp-n0) - 1) * var(--clp-gap));
    /* The cards: the frame fitted inside the box. Equal to --clp-cw0 on a fixed board. */
    --clp-cwraw: min(var(--clp-cw0),
        (var(--clp-boxw) - (var(--clp-cols) - 1) * var(--clp-gap)) / var(--clp-cols),
        (var(--clp-boxh) - (var(--clp-rows) - 1) * var(--clp-gap)) / (var(--clp-rows) * 1.390625));
    --clp-cw: var(--clp-cwraw);
    min-height: calc(var(--clp-boxh) + 2 * var(--clp-gut) + 2 * var(--clp-gap)
        + 2 * var(--clp-pad) + 2px);
}
.clp-stage > .panel.clp-board.clp-shiftboard { --clp-gut: 26px; }
/* A second declaration of a custom property would not fall back, hence the
   @supports (Army Ants). */
@supports (height: 100dvh) {
    .clp-stage > .panel.clp-board { --clp-vh: 100dvh; }
}
/* Whole-pixel cards, so the atlas shows no seam (card-width-blur). */
@supports (width: round(down, 1.5px, 1px)) {
    .clp-stage > .panel.clp-board { --clp-cw: round(down, var(--clp-cwraw), 1px); }
}

/* ---- the grid ---- */

/* The containing block of .clp-paths. position: relative with no z-index
   makes no stacking context (D2). */
.clp-board > .clp-grid {
    position: relative;
    display: grid;
    grid-template-columns: var(--clp-gut) repeat(var(--clp-cols), var(--clp-cw)) var(--clp-gut);
    grid-template-rows: var(--clp-gut) repeat(var(--clp-rows), calc(var(--clp-cw) * 1.390625)) var(--clp-gut);
    gap: var(--clp-gap);
}
.clp-grid > .clp-cell { position: relative; min-width: 0; }
.clp-cell > .card {
    position: relative;
    --cw: var(--clp-cw);
    width: 100%;
    height: 100%;
}
/* An empty place inside a line's span: a gap, which blocks a step (P9c). */
.clp-grid > .clp-cell.clp-gap {
    border: 1.5px dashed rgba(255, 255, 255, 0.35);
    border-radius: 6%;
    box-sizing: border-box;
}
/* An empty place outside every span: nothing drawn (.clp-void). */

/*
 * No hover lift on the board: the platform's 5px lift moves a card out from
 * under a pointer resting near its bottom edge, which ends the hover and
 * starts it again, and every hover here draws a preview (Army Ants).
 */
.clp-cell > .card.clickable:hover { transform: none; }

/* Nothing inside a card takes the pointer, and the arrows sit on top (D2). */
.clp-cell > .card > *,
.clp-cell > .card::before,
.clp-cell > .card::after { pointer-events: none; }
.clp-grid:has(> .clp-arrow:hover) > .clp-cell > .card.clp-pv-line { pointer-events: none; }

/* A lit destination or pick: a 3px gold ring. */
.clp-cell > .card.clp-lit {
    outline: 3px solid var(--accent);
    outline-offset: 1px;
    cursor: pointer;
}
/* The platform's two outline states win back over the lit ring. */
.clp-cell > .card.hinted,
.clp-cell > .card.coach-mark { outline: 3px solid var(--accent); outline-offset: 2px; }
.clp-cell > .card.clickable:focus-visible { outline: 3px solid #ffffff; outline-offset: 2px; }
/* Faded while choosing: a filter, never opacity, rank and suit kept legible. */
.clp-cell > .card.clp-dim { filter: saturate(0.6) brightness(0.97); }
/* SHIFT(d): the chosen card's row and column, tinted. */
.clp-cell > .card.clp-tint {
    box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.55), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
/*
 * D2's occupancy ring (switched on in Task 19.2, 2026-10-10, after the probe's
 * 14-wide 360px screenshot showed the 9px triangle and cross not reading as
 * shapes): a 2px inset ring in the seat's fill (`--clp-occ`, set inline) on
 * the card under every pawn, at every size. Written on `.clp-card` so it
 * rides the slide's ghost; it keeps the card's shadow and, in SHIFT(d), the tint.
 */
.clp-card.clp-occ {
    box-shadow: inset 0 0 0 2px var(--clp-occ), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.clp-cell > .card.clp-occ.clp-tint {
    box-shadow: inset 0 0 0 2px var(--clp-occ), 0 0 0 2px rgba(255, 209, 102, 0.55),
        0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}

/* ---- what rides inside a card: percent of the card's box (D2) ---- */

/*
 * The pawn token (feedback round 2, F2, the user's call over D2's left-edge
 * band): 64% of the card's width, in the middle of the card. It covers the
 * middle of the rank and the suit, so it carries the card's rank itself, and
 * with the Diagonal step on its suit as well (def.ts `boardTokenSvg`).
 */
.clp-card > .clp-pawn {
    position: absolute;
    z-index: 3;
    width: 64%;
    aspect-ratio: 1;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    box-sizing: border-box;
    pointer-events: none;
}
.clp-card > .clp-pawn .clp-toklabel { font-family: inherit; font-weight: 800; }
/*
 * The seat's own suit badge (3 and 4 players under the Diagonal step; def.ts
 * `tokenBadge`). The token is its own size container, so a token under 20px
 * wide (its glyph would be under 6.3px, a grown Shifting frame on a phone)
 * drops the badge and keeps the label; the fill hue and the chip still say
 * the suit there. The token already makes a stacking context (z-index 3), so
 * the containment changes nothing in the paint order.
 */
.clp-card > .clp-pawn { container: clp-tok / inline-size; }
.clp-card > .clp-pawn .clp-tokbadge-suit { font-family: inherit; font-weight: 800; }
@container clp-tok (max-width: 19.5px) {
    .clp-tokbadge { display: none; }
}
/* The label is the destination card's, so it waits for the token to arrive. */
.clp-card > .clp-pawn.clp-pawn-moving .clp-toklabel { visibility: hidden; }
/*
 * The door and × badges stay in the left-edge band, 0.72 card widths down the
 * face, clear of the rank and suit ink of every face the three decks use
 * (design D2, measured on the simplified atlas 2026-10-09): the card under a
 * × is face up and in play, so its rank must stay readable. The overhang is
 * capped under the 3px gap.
 */
.clp-card > .clp-door,
.clp-card > .clp-gone {
    position: absolute;
    z-index: 3;
    --clp-tok: clamp(9px, 25%, 24px);
    --clp-over: clamp(0px, calc(var(--clp-tok) - 21%), 2.75px);
    width: var(--clp-tok);
    aspect-ratio: 1;
    left: calc(-1 * var(--clp-over));
    top: 51.78%;
    translate: 0 -50%;
    box-sizing: border-box;
    pointer-events: none;
}
.clp-card > .clp-pawn > svg,
.clp-card > .clp-door > svg {
    display: block;
    width: 100%;
    height: 100%;
    overflow: visible;
}
/* A pawn that went out: a dark badge with a white rim and a × in its seat color. */
.clp-card > .clp-gone {
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    border: 1.5px solid #ffffff;
    background: #14161b;
    color: var(--clp-seat, #ffffff);
    font-size: clamp(8px, 18%, 18px);
    font-weight: 700;
    line-height: 1;
}
/* The last-move rings and SHIFT(d)'s chosen ring, inset over the card. */
.clp-card > .clp-ring {
    position: absolute;
    inset: -3px;
    z-index: 1;
    border-radius: 8%;
    border: 2px solid var(--clp-seat, #6aa9e0);
    pointer-events: none;
}
.clp-card > .clp-ring.clp-ring-from { border-style: dotted; }
.clp-card > .clp-ring.clp-ring-chosen { border: 3px solid var(--accent); inset: -4px; }
/* F3: the card a pawn was just knocked out from, until the next move. */
.clp-card > .clp-ring.clp-ring-out {
    inset: -5px;
    border: 3px solid var(--clp-seat, #ffffff);
    box-shadow: 0 0 0 2px #14161b, 0 0 10px 2px rgba(255, 255, 255, 0.55);
}

/* ---- F5: a collapsed card is a button that shows its face for 2 seconds ---- */

.clp-cell > .card.back.clp-peek { cursor: pointer; }
.clp-cell > .card.back.clp-peek:focus-visible { outline: 3px solid #ffffff; outline-offset: 2px; }
/*
 * The face, faded with a filter (grayscale, low contrast; not see-through,
 * the user's standing rule) and edged in a dashed line, so it still reads as
 * a collapsed card. The fade in and out is the face's own 2-second animation;
 * def.ts joins a repainted face to it at the right point with a negative
 * delay. Its first 240 ms and last 300 ms ramp `opacity()` inside the filter,
 * which renders exactly as `opacity`: a short crossfade over the back, and
 * see-through while it runs. Only the 1.46 s between is the faded state.
 */
.clp-card > .card.clp-peekface {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    --cw: var(--clp-cw);
    z-index: 1;
    pointer-events: none;
    box-shadow: inset 0 0 0 2px rgba(20, 22, 27, 0.55);
    filter: grayscale(0.6) contrast(0.72) brightness(1.04);
    animation: clp-peek 2000ms ease-in-out both;
}
.clp-card > .card.clp-peekface::after {
    content: '';
    position: absolute;
    inset: 3px;
    border: 1.5px dashed rgba(20, 22, 27, 0.6);
    border-radius: 6%;
    pointer-events: none;
}
@keyframes clp-peek {
    0% { filter: grayscale(0.6) contrast(0.72) brightness(1.04) opacity(0); }
    12% { filter: grayscale(0.6) contrast(0.72) brightness(1.04) opacity(1); }
    85% { filter: grayscale(0.6) contrast(0.72) brightness(1.04) opacity(1); }
    100% { filter: grayscale(0.6) contrast(0.72) brightness(1.04) opacity(0); }
}
@media (prefers-reduced-motion: reduce) {
    .clp-card > .card.clp-peekface { animation: none; }
}

/* ---- F4: where you could go next turn, from the hovered destination ---- */

/* A sibling of the card in its cell (never a child, so no flight ghost carries it). */
.clp-cell > .clp-mark {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}
.clp-mark > .clp-next {
    display: block;
    width: 52%;
    height: auto;
    aspect-ratio: 1;
    overflow: visible;
}
/* A destination with no move from it. */
.clp-mark > .clp-deadtag {
    padding: 2px 6px;
    border-radius: 999px;
    background: #8f1f1a;
    border: 1.5px solid #ffffff;
    color: #ffffff;
    font-size: clamp(9px, calc(var(--clp-cw) * 0.17), 14px);
    font-weight: 700;
    line-height: 1.2;
    white-space: nowrap;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
}

/* ---- F3: the notice over the board when you are out ---- */

.clp-stage > .panel.clp-board { position: relative; }
.clp-board > .clp-notice {
    position: absolute;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    z-index: 5;
    width: max-content;
    max-width: calc(100% - 24px);
    box-sizing: border-box;
    padding: 14px 18px;
    border-radius: 12px;
    background: rgba(20, 22, 27, 0.94);
    border: 2px solid var(--accent);
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.55);
    color: #ffffff;
    text-align: center;
}
.clp-notice > .clp-notice-text { font-size: 18px; font-weight: 700; margin-bottom: 10px; }
.clp-notice > .clp-notice-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
}
.clp-notice-actions > .btn { min-height: 44px; }
/* Hotseat: the next player's tap reaches the card under the notice; only its button takes the pointer. */
.clp-board > .clp-notice.clp-notice-pass { pointer-events: none; }
.clp-board > .clp-notice.clp-notice-pass .btn { pointer-events: auto; }

/* ---- previews, applied in place (The input state machine) ---- */

/* The ghost token on a hovered destination or pick, where the token would sit
   (F2: centered, 64% of the card), see-through so the card's rank still reads. */
.clp-cell > .card.clp-pv-ghost::after {
    content: '';
    position: absolute;
    z-index: 1;
    width: 58%;
    aspect-ratio: 1;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    border-radius: 50%;
    background: #ffffff;
    box-shadow: 0 0 0 2px #14161b;
    opacity: 0.5;
}
.clp-cell > .card.clp-pv-s1::after { border-radius: 14%; }
.clp-cell > .card.clp-pv-s2::after { border-radius: 0; clip-path: polygon(50% 6.7%, 94.2% 88.3%, 5.8% 88.3%); }
.clp-cell > .card.clp-pv-s3::after {
    border-radius: 0;
    clip-path: polygon(31.3% 6.7%, 68.8% 6.7%, 68.8% 31.3%, 93.3% 31.3%, 93.3% 68.8%, 68.8% 68.8%,
        68.8% 93.3%, 31.3% 93.3%, 31.3% 68.8%, 6.7% 68.8%, 6.7% 31.3%, 31.3% 31.3%);
}
/* One band item per card: the ghost outranks a badge. */
.clp-cell > .card.clp-pv-ghost > .clp-door,
.clp-cell > .card.clp-pv-ghost > .clp-gone { visibility: hidden; }
/* A pick's first-turn reach: a dashed ring inside the card. */
.clp-cell > .card.clp-pv-reach::before {
    content: '';
    position: absolute;
    inset: 3px;
    z-index: 1;
    border: 2px dashed rgba(20, 22, 27, 0.75);
    border-radius: 6%;
}

/* ---- the overlay ---- */

.clp-grid > .clp-paths {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 2;
    overflow: visible;
    pointer-events: none;
}

/* ---- the torus: tabs at both ends of every row and column (R17.5) ---- */

.clp-grid > .clp-tab {
    align-self: center;
    justify-self: center;
    border-radius: 2px;
    background: rgba(255, 255, 255, 0.28);
    pointer-events: none;
}
.clp-grid > .clp-tab-l,
.clp-grid > .clp-tab-r { width: 4px; height: 40%; }
.clp-grid > .clp-tab-t,
.clp-grid > .clp-tab-b { width: 40%; height: 4px; }

/* ---- the shift arrows (D3): 26px buttons, 44px hit areas ---- */

.clp-grid > .clp-arrow {
    position: relative;
    z-index: 4;
    align-self: center;
    justify-self: center;
    width: 26px;
    height: 26px;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--accent);
    color: var(--accent-ink);
    font: inherit;
    font-size: 16px;
    font-weight: 700;
    line-height: 26px;
    cursor: pointer;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.clp-grid > .clp-arrow:hover,
.clp-grid > .clp-arrow:focus-visible { outline: 2px solid #ffffff; outline-offset: 1px; }
/*
 * The hit area (design D3), a 44px square on two rules. ACROSS the lane it
 * reaches outward only, from the button's inner edge, so it never covers a
 * card. ALONG the lane it is clamped inside the cards' span: its offset from
 * the span's start is clamp(0, c - 22px, S - 44px), c the track's center and
 * S the span (def.ts writes the track index --clp-ti and count --clp-tn), so
 * a row arrow's square stays beside the card rows and a column arrow's above
 * or below the card columns, and no two squares meet near a corner (Task
 * 19.2 brought the square in line with D3; it was centered on the button,
 * which D3 does not allow near a corner). The button is centered on its
 * track, so its own start is c - 13px. The probe's ARROWS and CORNERS checks
 * measure it.
 */
.clp-grid > .clp-arrow::after {
    content: '';
    position: absolute;
    width: 44px;
    height: 44px;
}
.clp-grid > .clp-arrow-left,
.clp-grid > .clp-arrow-right {
    --clp-th: calc(var(--clp-cw) * 1.390625);
    --clp-tc: calc(var(--clp-ti) * (var(--clp-th) + var(--clp-gap)) + var(--clp-th) / 2);
    --clp-ts: calc(var(--clp-tn) * var(--clp-th) + (var(--clp-tn) - 1) * var(--clp-gap));
}
.clp-grid > .clp-arrow-up,
.clp-grid > .clp-arrow-down {
    --clp-tc: calc(var(--clp-ti) * (var(--clp-cw) + var(--clp-gap)) + var(--clp-cw) / 2);
    --clp-ts: calc(var(--clp-tn) * var(--clp-cw) + (var(--clp-tn) - 1) * var(--clp-gap));
}
.clp-grid > .clp-arrow-left::after,
.clp-grid > .clp-arrow-right::after {
    top: calc(clamp(0px, var(--clp-tc) - 22px, var(--clp-ts) - 44px) - (var(--clp-tc) - 13px));
}
.clp-grid > .clp-arrow-up::after,
.clp-grid > .clp-arrow-down::after {
    left: calc(clamp(0px, var(--clp-tc) - 22px, var(--clp-ts) - 44px) - (var(--clp-tc) - 13px));
}
.clp-grid > .clp-arrow-left::after { right: 0; }
.clp-grid > .clp-arrow-right::after { left: 0; }
.clp-grid > .clp-arrow-up::after { bottom: 0; }
.clp-grid > .clp-arrow-down::after { top: 0; }

/* ---- the flip (Animation step 4) ---- */

.clp-cell > .card.clp-flipface,
.clp-cell > .card.clp-flipback {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}
/* Hides the real back's paint and keeps its box, so a capture reads its rect. */
.clp-cell > .card.clp-flipping { visibility: hidden; }

/* ---- the action row (R17.11) ---- */

.clp-stage > .clp-actions {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 44px;
}
.clp-actions > .btn.clp-back { min-height: 44px; }

/* ---- status and seat chips (D5, R19.8) ---- */

/* Reservations, measured by the probe (2026-10-10): two status lines (48.6px)
   and a 30px chips row on desktop held over every repaint. */
.status.clp-status { line-height: 1.35; min-height: calc(2 * 1.35em); }
.progress.clp-scoreboard {
    opacity: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    flex-wrap: wrap;
    min-height: 30px;
}
.clp-scoreboard > .clp-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 9px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.22);
    white-space: nowrap;
}
.clp-scoreboard > .clp-chip.clp-chip-turn { box-shadow: inset 0 0 0 2px var(--accent); }
.clp-scoreboard > .clp-chip.clp-chip-out > .clp-chipname { text-decoration: line-through; }
/* F3: an out seat reads as out at a glance: struck through, its place, and dimmed with a filter. */
.clp-scoreboard > .clp-chip.clp-chip-out { filter: grayscale(0.85) brightness(0.86); }
.clp-chip > .clp-chiptok { display: inline-block; width: 18px; height: 18px; flex: none; }
.clp-chip > .clp-chiptok > svg { display: block; width: 100%; height: 100%; }
.clp-chip > .clp-chipname { font-weight: 600; }
.clp-chip > .clp-chipsuit { font-weight: 700; }
.clp-chip > .clp-chipsuit.red { color: var(--red); }
.clp-chip > .clp-chipsuit.red.diamond { color: var(--red-diamond); }
.clp-chip > .clp-chipsuit.club { color: var(--blue-club); }
.clp-chip > .clp-chipcolor { font-size: 12px; }
.clp-chip > .clp-chipcolor.clp-col-red { color: var(--red); }
.clp-chip > .clp-chippts { font-variant-numeric: tabular-nums; font-weight: 700; }
/* A fixed width, so the chip never changes size as its tag changes. */
.clp-chip > .clp-chiptag { min-width: 4.6em; font-size: 12px; text-align: left; }
.clp-chip > .clp-chiptag.clp-chiptag-none { visibility: hidden; }
.clp-scoreboard > .clp-round { font-size: 13px; white-space: nowrap; }

/* ---- the result panel (R12.9) ---- */

.clp-scores {
    border-collapse: collapse;
    margin: 0 auto 10px;
    font-variant-numeric: tabular-nums;
}
.clp-scores th,
.clp-scores td { padding: 3px 10px; }
.clp-scores th.clp-sc-label,
.clp-scores td.clp-sc-label { text-align: left; }
.clp-scores th.clp-sc-num,
.clp-scores td.clp-sc-num { text-align: right; }
.clp-scores td.clp-sc-won { color: var(--accent); font-weight: 700; }
.clp-scores tr.clp-sc-total td {
    border-top: 1px solid rgba(255, 255, 255, 0.3);
    font-weight: 700;
}

/* ---- the setup screen (D7): a setting that does not apply, disabled in place ---- */

#opt-clp-board .seg:disabled,
#opt-clp-shift .seg:disabled,
#opt-clp-choice .seg:disabled,
#opt-clp-diagonal .seg:disabled,
#opt-clp-match .seg:disabled,
#opt-clp-level .seg:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

/* ---- phones ---- */

@media (max-width: 560px) {
    /* Measured 266.5px at 390x844 and 360x740 (the probe, 2026-10-10). */
    .clp-stage > .panel.clp-board { --clp-chrome: 267px; }
    .status.clp-status { font-size: 15px; line-height: 1.3; min-height: calc(3 * 1.3em); }
    .clp-scoreboard > .clp-chip { padding: 2px 7px; gap: 4px; font-size: 13px; }
    .progress.clp-scoreboard { gap: 6px; min-height: 58px; }
}
/*
 * Forgotten Crime. Every selector begins with an fcr- class or id, and a rule
 * that touches a shared id goes through a game class (.status.fcr-status,
 * .fcr-result, never a bare #status-line). Rows of the board are centered.
 * Animations are named fcr-*, use the independent `scale` and `opacity`
 * properties, and are off under body.motion-off. gen-css.mjs appends this file
 * to site/webplay.css; its ORDER is not edited (R22.2).
 */

/* ----- Suit inks (R2.3, P2) -----
 * suitTextClass returns red / red diamond / club, and NONE of them colors
 * anything on its own outside the log (src/bookshop/style.css says so). Suits
 * are the suspects here, so every surface that names one opts in: the status
 * line (a review step's line included), the table's row heads, the role
 * picker's heading, the accuse bar (the armed label and the end line), and the
 * result panel. .diamond is one class deeper so it beats .red. */
.fcr-status .red, .fcr-table .red, .fcr-pick .red, .fcr-accusebar .red,
.fcr-result .red { color: var(--red); }
.fcr-status .red.diamond, .fcr-table .red.diamond, .fcr-pick .red.diamond,
.fcr-accusebar .red.diamond, .fcr-result .red.diamond { color: var(--red-diamond); }
.fcr-status .club, .fcr-table .club, .fcr-pick .club, .fcr-accusebar .club,
.fcr-result .club { color: var(--blue-club); }

/* ----- Sizing (D4, D3) -----
 * .fcr-board is a named size container. A custom property declared twice gives
 * no fallback (the later declaration always wins), and an unsupported round()
 * inside it blanks every card that reads it, so the plain values come first and
 * round() overrides them inside @supports (the Army Ants pattern). 100cqi is a
 * token: it resolves on the card, against the card's nearest size container,
 * which must be .fcr-board, so no element between .fcr-board and the cards may
 * set container-type. */
.fcr-board {
    container: fcr / inline-size;
    --fcr-pilecw: clamp(40px, calc(100cqi / 8), 72px);
}
/* The table's properties sit on .fcr-tablebox, not .fcr-board, because a
 * container cannot query itself; .fcr-tablebox is inside the container. */
.fcr-tablebox {
    --fcr-headw: 40px;
    --fcr-cw: clamp(24px, calc((100cqi - 81px) / 9), 60px);
    /* The card's own height is --cw * 89 / 64 (platform .card). A row sized with
     * the rounder 1.39 grew by 0.02px when its card arrived, and four rows of
     * that carried the accuse bar across a pixel boundary at 390px (offsetTop
     * 501 to 502; found by tools/forgottencrime/fcr-probe.mjs). The same ratio on both sides
     * keeps every row exactly as tall with a card as without. */
    --fcr-rowh: calc(var(--fcr-cw) * 89 / 64);
}
@container fcr (min-width: 560px) {
    .fcr-tablebox {
        --fcr-headw: 104px;
        --fcr-cw: clamp(24px, calc((100cqi - 145px) / 9), 60px);
    }
}
@supports (width: round(down, 1px, 1px)) {
    .fcr-board { --fcr-pilecw: clamp(40px, round(down, 100cqi / 8, 1px), 72px); }
    .fcr-tablebox { --fcr-cw: clamp(24px, round(down, (100cqi - 81px) / 9, 1px), 60px); }
    @container fcr (min-width: 560px) {
        .fcr-tablebox { --fcr-cw: clamp(24px, round(down, (100cqi - 145px) / 9, 1px), 60px); }
    }
}
.fcr-tablebox .card { --cw: var(--fcr-cw); }
.fcr-casefile .card { --cw: var(--fcr-pilecw); }

.fcr-board { max-width: 100%; }

/* ----- The casefile (D4) ----- */
.fcr-casefile {
    position: relative;
    display: flex; justify-content: center; align-items: flex-start; gap: 12px;
    --fcr-caph: 1.9em;              /* the score chip and the solution captions */
}
.fcr-casecol {
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    /* a card, the gap, one caption: fixed, so a label change moves nothing */
    height: calc(var(--fcr-pilecw) * 89 / 64 + 4px + var(--fcr-caph));
}
/* Three columns of ONE width (the deck, the Criminal card, the Accomplice card),
 * so the middle card stands on the board's axis and the outer two are the same
 * distance either side of it. They were 10.5em, 6.5em, and 6.5em wide, which put
 * the group's visual center 16px right of the table's at 1280 and 13px at 390
 * (found by tools/forgottencrime/fcr-probe.mjs). The widest caption, "12 points if right",
 * needs about 7.5em; a caption wider than its column spills evenly both sides,
 * and the neighbors' captions are short. */
.fcr-casecol { flex: 1 1 0; min-width: 0; max-width: max(var(--fcr-pilecw), 8.5em); }
/* The deck button is exactly its card, so the column formula above holds. */
.fcr-casefile .fcr-deck { padding: 0; border: 0; background: none; cursor: pointer; }
.fcr-casefile .fcr-deck[aria-disabled="true"] { cursor: default; }
.fcr-score, .fcr-solcap { height: var(--fcr-caph); white-space: nowrap; }
.fcr-score, .fcr-solcap {
    background: none; border: 0; color: var(--muted, inherit); font: inherit;
    text-decoration: underline dotted; cursor: pointer;
}
/* On a phone the three columns are about 105px wide, and "12 points if right"
 * at full size plus the button's own 6px side padding is 131px: it stuck 3px
 * out of a 360px screen (found by tools/forgottencrime/fcr-probe.mjs). A smaller caption with
 * no side padding is 101px and fits its column; the tap target is still 44px
 * tall under (pointer: coarse). */
@container fcr (max-width: 400px) {
    .fcr-score, .fcr-solcap { font-size: 0.85em; padding-inline: 0; }
}
.fcr-deckdone { opacity: 0.75; }
/* The empty deck's label sits in a <button>, whose text is black by default:
 * near-invisible on the felt (seen in the 1280 full-table screenshot). */
.fcr-casefile .fcr-deck .card.empty {
    display: flex; align-items: center; justify-content: center;
    color: var(--ink); font-size: 0.85em;
}
/* visibility, not display: the animator skips an element whose offsetParent is
 * null, and display: none (or position: fixed) would make it null. */
.fcr-casefile .fcr-anchor { position: absolute; left: 0; top: 0; width: 1px;
    height: 1px; visibility: hidden; pointer-events: none; }
.fcr-drawstrip {
    position: absolute; left: 0; right: 0; bottom: 10%;
    text-align: center; font-size: 0.8em; color: #fff;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); pointer-events: none;
}
.fcr-casefile .fcr-deck .card { position: relative; }

@media (pointer: coarse) {
    .fcr-casefile { --fcr-caph: 44px; }   /* the chip and captions are tap targets */
}

/* ----- The table (D3, P7): every cell holds still (R14.3) ----- */
.fcr-tablebox { display: flex; justify-content: center; }
.fcr-table {
    table-layout: fixed;
    border-collapse: separate; border-spacing: 0;
    /* An explicit width: fixed layout applies only to a table whose width is
       not auto. headw + 7 x (cw + 3px) + 2 x (cw + 8px). */
    width: calc(var(--fcr-headw) + 9 * var(--fcr-cw) + 37px);
    margin-inline: auto;
}
.fcr-table col.fcr-col-head { width: var(--fcr-headw); }
.fcr-table col.fcr-col-ans { width: calc(var(--fcr-cw) + 3px); }
.fcr-table col.fcr-col-ans.fcr-gstart { width: calc(var(--fcr-cw) + 8px); }
.fcr-table td, .fcr-table th { box-sizing: border-box; padding: 2px 1.5px; text-align: center; }
.fcr-table .fcr-gstart { padding-left: 6.5px; }   /* the 8px gap between groups */
.fcr-table td, .fcr-table th[scope="row"] { height: calc(var(--fcr-rowh) + 4px); }
.fcr-table thead tr.fcr-qrow th { height: 1.6em; white-space: nowrap; font-size: 0.85em; }

.fcr-anshead { position: relative; height: 2.6em; width: 100%; }
/* An answer head is a <button> in play and an inert <span> otherwise; a span
 * ignores width/height unless laid out, so both get flex-column centering and
 * share one box-model (NIT 3). */
.fcr-anshead { display: flex; flex-direction: column; align-items: center;
    justify-content: center; }
.fcr-anshead, .fcr-rolehead { box-sizing: border-box; padding: 0; border: 0;
    background: none; color: inherit; font: inherit; }
.fcr-anshead { cursor: pointer; }
.fcr-rolehead { width: 100%; height: var(--fcr-rowh); display: flex;
    flex-direction: column; align-items: center; justify-content: center;
    white-space: nowrap; overflow: hidden; cursor: pointer; }
.fcr-headmark { font-weight: 600; font-size: 0.85em; }
.fcr-rolename { display: none; font-size: 0.75em; }
@container fcr (min-width: 560px) {
    .fcr-rolename { display: inline; }
}
.fcr-mark { display: inline-block; width: 1.1em; height: 1.1em; line-height: 1.1em; }

.fcr-mk-criminal { color: var(--mk-criminal, #c0392b); }
.fcr-mk-accomplice { color: var(--mk-accomplice, #d9a441); }
.fcr-mk-innocent { color: var(--mk-innocent, #4aa564); }
.fcr-mk-true { color: var(--mk-true, #4aa564); }
.fcr-mk-false { color: var(--mk-false, #c58a8a); }

/* A card in a cell: a dashed slot where none is drawn. */
.fcr-slot { width: var(--fcr-cw); height: var(--fcr-rowh); margin-inline: auto;
    border: 2px dashed rgba(255, 255, 255, 0.18); border-radius: 4px;
    box-sizing: border-box; }
.fcr-slot.fcr-closed { border-color: transparent; }

/* Every tap target is at least 44px tall on touch (R11.6, R14.5). */
@media (pointer: coarse) {
    .fcr-tablebox { --fcr-rowh: max(44px, calc(var(--fcr-cw) * 89 / 64)); }
    .fcr-anshead { height: max(44px, 2.6em); }
    .fcr-rolehead { min-height: 44px; }
    .fcr-deck, .fcr-score, .fcr-solcap, .fcr-accuse, .fcr-pick button { min-height: 44px; }
}

/* ----- The accuse bar (P6) ----- */
.fcr-accusebar { height: 48px; display: flex; align-items: center; justify-content: center; }
.fcr-accuse { white-space: nowrap; }
.fcr-accuse[aria-disabled="true"] { opacity: 0.6; }
.fcr-accuseline { line-height: 1.35; text-align: center; }

/* ----- The status line (R12.6) ----- */
.status.fcr-status { line-height: 1.35; }
@media (max-width: 720px) {
    .status.fcr-status { min-height: calc(2 * 1.35em); }
}

/* ----- The pickers (Notes, D5) -----
 * A popover's user-agent surface is Canvas (white); the dark theme's suit inks
 * barely read on it, so .fcr-pick sets its own dark surface and its buttons are
 * ordinary .btn on it. */
.fcr-pick {
    background: var(--felt-dark); color: var(--ink); border: 1px solid var(--panel-border);
    border-radius: 10px; padding: 10px 12px; min-width: 10em;
}
/* The flex layout lives under :popover-open (the Mus precedent). An author
 * `display: flex` on the bare class beats the user agent's
 * `[popover]:not(:popover-open) { display: none }`, so a closed picker sat
 * painted over the table as an empty pill and swallowed the taps meant for the
 * cards under it (found by tools/forgottencrime/fcr-probe.mjs; the DOM fakes cannot see it). */
.fcr-pick:popover-open { display: flex; flex-direction: column; gap: 6px; }
.fcr-pickhead { font-weight: 600; text-align: center; margin-bottom: 2px; }
.fcr-pick button { width: 100%; }
.fcr-pick button[aria-pressed="true"] { outline: 2px solid var(--accent, #e2b53c); }

/* The help popovers (.opt-help-body, the light balloon) print no suit glyph,
 * so they need no inks of their own. */

/* ----- The end reveal (P10, D6) ----- */
.fcr-solution.fcr-turnover { animation: fcr-turnover 620ms ease both; }
@keyframes fcr-turnover { from { scale: 0 1; } to { scale: 1 1; } }

/* NIT 5: an undrawn lie card shows BOTH marks. Unseen is a dashed outline with
 * an offset; a lie is a box-shadow ring plus a badge, so neither overrides the
 * other. */
.fcr-unseen { outline: 2px dashed rgba(255, 255, 255, 0.55); outline-offset: 2px;
    filter: saturate(0.55) brightness(1.04); }
.fcr-lie { position: relative; box-shadow: 0 0 0 3px var(--red, #ff9c9c); }
.fcr-lie::after {
    content: "\2717"; position: absolute; top: -6px; right: -6px;
    width: 1.1em; height: 1.1em; line-height: 1.1em; text-align: center;
    border-radius: 50%; background: var(--red, #ff9c9c); color: #3a0d0d;
    font-size: 0.8em; font-weight: 700;
}
/* The truth bar under an answer head: absolutely positioned, so it adds no
 * height. */
.fcr-anshead.fcr-truth::after {
    content: ""; position: absolute; left: 10%; right: 10%; bottom: 0;
    height: 3px; background: var(--mk-true, #4aa564); border-radius: 2px;
}
.fcr-markin { animation: fcr-markin 400ms ease 700ms both; }
@keyframes fcr-markin { from { opacity: 0; } to { opacity: 1; } }

body.motion-off .fcr-turnover,
body.motion-off .fcr-markin { animation: none; }

/* ----- The result panel ----- */
/* The shared coarse-pointer rule gives .btn 36px to 38px; every action here is
 * 44px tall on touch, the summary's included (found by tools/forgottencrime/fcr-probe.mjs). */
@media (pointer: coarse) {
    .fcr-result .btn { min-height: 44px; }
}
.fcr-result .result-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.fcr-result .result-note { text-align: center; }
/* =========================================================================
   Oh Hell! (ohhell)
   An exact-bid trick taker for 3 to 7: bid the tricks you will take, and
   score 10 more only for taking exactly that many.
     the table as a table: the other seats round the trick in play order,
       clockwise from the viewer's left, this player South, and the trick a
       cross with each card on its seat's side    (.ohh-board, .ohh-trick, .pos-*)
     one center cell holding two layers, the trick and the bid picker, so the
       cell is the larger of the two all match long  (.ohh-center, .ohh-off)
     a three-line panel per seat: name, total and dealer chip, the bid box
       (.ohh-seat, .ohh-seat-head, .ohh-seat-score, .ohh-seat-line)
     the deal box: the trump card and the bid readout   (.ohh-deal)
     a finished trick held in the cross until the next lead   (.ohh-winning)
   CONTAINERS MUST NOT MOVE. Every track of the desktop board is fixed or a
   fixed share of a fixed width, the center is a fixed width, every panel is
   three lines whose heights derive from their own font-size property, the
   picker is three fixed rows whatever it holds, and the deal box is a fixed
   size. #hand-cards gets no min-height: src/shared/ui/handspace.ts reserves it
   at runtime.
   SHARED IDS only through a class this game owns: .ohh-handpanel on the hand
   panel, .ohh-status and .ohh-progress added by the renderers, .ohh-summary on
   the result panel.
   ========================================================================= */

/* Visually hidden words beside an aria-hidden glyph or short form. The seat
   panel, the deal box's tilt, and the bid readout are positioned, so this text
   takes no room on its line. */
.ohh-vh {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* --- status and progress ------------------------------------------------ */
/* Two lines reserved: the action wanted, and a hint when one is asked for. */
.status.ohh-status { min-height: 2.6em; }
.status.ohh-status.ohh-turn { color: var(--accent); }
.ohh-statusmain { display: block; }
.ohh-hintnote { display: block; font-size: 14px; font-weight: 400; opacity: 0.85; }
.progress.ohh-progress { min-height: 20px; white-space: nowrap; }
/* Card labels in prose outside the log. */
.ohh-suited .red { color: var(--red); }
.ohh-suited .red.diamond { color: var(--red-diamond); }
.ohh-suited .club { color: var(--blue-club); }

/* --- the board ----------------------------------------------------------- */
/* ONE GRID. #ohh-table and .ohh-foot are `display: contents`, so the panels
   and the deal box are cells of it and take the areas their seats sit in.
   THE CENTER TRACK IS A FIXED WIDTH, never `auto`, or every flank panel would
   move sideways as the picker's contents change. Derived once, rounded up:
     trick  = 3 * --ohh-playcw + 2 * --ohh-trickgap   (56px: 180px; 46px: 150px)
     picker = 6 * --ohh-bidw + 5 * --ohh-bidgap        (32px, 4px: 212px)
     --ohh-centerw = max(trick, picker) + 4px (the picker's side padding)
                     + 4px (slack, so six buttons never wrap to a seventh place)
   The picker is the wider layer at every table size, so one value serves all. */
.ohh-board {
    --ohh-playcw: 56px;
    --ohh-trickgap: 6px;
    --ohh-bidw: 32px;
    --ohh-bidh: 30px;
    --ohh-bidgap: 4px;
    --ohh-placeh: 32px;
    --ohh-notefs: 12px;
    --ohh-centerw: 220px;
    --ohh-namefs: 13px;
    --ohh-totalfs: 15px;
    --ohh-bidfs: 11px;
    --ohh-pad: 6px 9px 7px;
    --ohh-trumpcw: 46px;
    --ohh-dealfs: 12px;
    --ohh-dealw: 150px;
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--ohh-centerw) minmax(0, 1fr);
    column-gap: 12px;
    row-gap: 8px;
    align-items: center;
    margin-bottom: 10px;
}
.ohh-board.ohh-seats-6,
.ohh-board.ohh-seats-7 { --ohh-playcw: 46px; }
.ohh-board.ohh-seats-3 { grid-template-areas: "w center e" "deal mine ."; }
.ohh-board.ohh-seats-4 { grid-template-areas: ". n ." "w center e" "deal mine ."; }
.ohh-board.ohh-seats-5 { grid-template-areas: "nw center ne" "w center e" "deal mine ."; }
.ohh-board.ohh-seats-6 { grid-template-areas: "nw n ne" "w center e" "deal mine ."; }
.ohh-board.ohh-seats-7 { grid-template-areas: "nw center ne" "w center e" "sw center se" "deal mine ."; }
.ohh-table,
.ohh-foot { display: contents; }
/* The left flank hugs the trick from the left, the right flank from the right. */
.ohh-seat.pos-w { grid-area: w; justify-self: end; }
.ohh-seat.pos-nw { grid-area: nw; justify-self: end; }
.ohh-seat.pos-sw { grid-area: sw; justify-self: end; }
.ohh-seat.pos-e { grid-area: e; justify-self: start; }
.ohh-seat.pos-ne { grid-area: ne; justify-self: start; }
.ohh-seat.pos-se { grid-area: se; justify-self: start; }
.ohh-seat.pos-n { grid-area: n; justify-self: center; }
.ohh-seat.ohh-mine { grid-area: mine; justify-self: center; }
.ohh-deal { grid-area: deal; justify-self: end; }

/* --- the center: two layers in one cell ---------------------------------- */
/* Whichever layer is not in use keeps its box but is hidden, so the cell is
   always the larger of the two. */
.ohh-center {
    grid-area: center;
    display: grid;
    box-sizing: border-box;
    width: var(--ohh-centerw);
    justify-self: center;
    justify-items: center;
    align-items: center;
}
.ohh-center > * { grid-area: 1 / 1; }
.ohh-center > .ohh-off { visibility: hidden; }

/* --- the trick: a cross --------------------------------------------------- */
.ohh-trick {
    display: grid;
    grid-template-columns: repeat(3, var(--ohh-playcw));
    gap: var(--ohh-trickgap);
    justify-content: center;
    justify-items: center;
    align-items: center;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.ohh-board.ohh-seats-3 .ohh-trick { grid-template-areas: "w . e" ". s ."; }
.ohh-board.ohh-seats-4 .ohh-trick { grid-template-areas: ". n ." "w . e" ". s ."; }
.ohh-board.ohh-seats-5 .ohh-trick { grid-template-areas: "nw . ne" "w . e" ". s ."; }
.ohh-board.ohh-seats-6 .ohh-trick { grid-template-areas: "nw n ne" "w . e" ". s ."; }
.ohh-board.ohh-seats-7 .ohh-trick { grid-template-areas: "nw . ne" "w . e" "sw . se" ". s ."; }
.ohh-play.pos-s { grid-area: s; }
.ohh-play.pos-w { grid-area: w; }
.ohh-play.pos-n { grid-area: n; }
.ohh-play.pos-e { grid-area: e; }
.ohh-play.pos-nw { grid-area: nw; }
.ohh-play.pos-ne { grid-area: ne; }
.ohh-play.pos-sw { grid-area: sw; }
.ohh-play.pos-se { grid-area: se; }
.ohh-play .card { --cw: var(--ohh-playcw); }
/* A slot still owed a card: outlined, so it reads as waiting. */
.ohh-play .card.empty { opacity: 0.35; }
/* The seat to play, so the eye knows where the next card is coming from. */
.ohh-play.onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
/* The card taking the trick, or that took the finished trick still shown:
   ringed, and glowing, so it is not a matter of color alone. */
.ohh-play .card.ohh-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}

/* --- the bid picker -------------------------------------------------------- */
/* Three fixed rows, whatever they hold: up to two lines of numbers, the
   confirm button, and three lines of note. The same height with 2 numbers,
   11, or none. */
.ohh-picker {
    display: grid;
    grid-template-rows:
        calc(2 * var(--ohh-bidh) + var(--ohh-bidgap))
        var(--ohh-placeh)
        calc(var(--ohh-notefs) * 1.3 * 3);
    row-gap: 6px;
    justify-self: stretch;
    box-sizing: border-box;
    width: 100%;
    padding: 0 2px;
}
.ohh-bidnums {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-content: flex-start;
    gap: var(--ohh-bidgap);
    overflow: hidden;
}
/* Every number exactly --ohh-bidw wide, specific enough that no shared .seg
   padding or touch min-height can resize it. */
.ohh-picker .seg.ohh-bidnum {
    flex: 0 0 var(--ohh-bidw);
    width: var(--ohh-bidw);
    height: var(--ohh-bidh);
    min-height: 0;
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font: inherit;
    font-size: 14px;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
    background: transparent;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    cursor: pointer;
}
.ohh-picker .seg.ohh-bidnum.selected {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-ink);
    font-weight: 700;
}
.ohh-picker .seg.ohh-bidnum.hinted { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The dealer's refused number: struck through as well as faded. */
.ohh-picker .seg.ohh-bidnum:disabled {
    opacity: 0.35;
    text-decoration: line-through;
    cursor: default;
}
.ohh-bidplacerow { display: flex; justify-content: center; align-items: center; }
.ohh-picker .btn.ohh-bidplace {
    box-sizing: border-box;
    height: var(--ohh-placeh);
    padding: 0 16px;
    white-space: nowrap;
}
.ohh-bidnote {
    font-size: var(--ohh-notefs);
    line-height: 1.3;
    white-space: normal;
    text-align: center;
    overflow: hidden;
    opacity: 0.9;
}

/* --- a seat's panel: three lines at every size ---------------------------- */
.ohh-seat {
    position: relative;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 2px;
    width: 176px;
    max-width: 100%;
    min-width: 0;
    padding: var(--ohh-pad);
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default, so the accent ring never shifts the layout. */
    outline: 2px solid transparent;
}
.ohh-seat.ohh-active { outline-color: var(--accent); }
.ohh-seat-head {
    line-height: 1.3;
    min-height: calc(var(--ohh-namefs) * 1.3);
    font-size: var(--ohh-namefs);
}
.ohh-seat-score {
    line-height: 1.2;
    min-height: calc(var(--ohh-totalfs) * 1.2);
    font-size: var(--ohh-totalfs);
}
.ohh-seat-line {
    min-height: calc(var(--ohh-bidfs) * 1.6 + 2px);
    font-size: var(--ohh-bidfs);
}
.ohh-seat-head,
.ohh-seat-score,
.ohh-seat-line {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 3px;
    white-space: nowrap;
}
.ohh-seat-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: 700;
}
.ohh-name-short { display: none; }
/* Whose turn, as a shape as well as the ring. Always rendered, so the name
   never shifts sideways when the turn moves. */
.ohh-toact { visibility: hidden; color: var(--accent); }
.ohh-seat.ohh-active .ohh-toact { visibility: visible; }
.ohh-seat-total { font-weight: 800; font-variant-numeric: tabular-nums; }
/* Always rendered, so the total sits in one place in every deal. */
.ohh-dealer {
    visibility: hidden;
    font-size: calc(var(--ohh-totalfs) * 0.7);
    font-weight: 700;
    line-height: 1.2;
    padding: 0 3px;
    border: 1px solid var(--accent);
    border-radius: 4px;
    color: var(--accent);
}
.ohh-seat.ohh-isdealer .ohh-dealer { visibility: visible; }
/* The bid box: the bid while the table bids, then tricks taken of the bid. The
   standing is a shape as well as a color: filled when exact, struck through
   when over, outlined when short. */
.ohh-bidbox {
    line-height: 1.6;
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    padding: 0 4px;
    font-variant-numeric: tabular-nums;
}
/* While the table bids: a bid made (plain), the seat bidding now (accent),
   and a seat still to bid (dashed and faded). Same box, same line height. */
.ohh-bidbox.ohh-std-nobid { border-style: dashed; opacity: 0.75; }
.ohh-bidbox.ohh-std-bid { border-color: rgba(255, 255, 255, 0.45); }
.ohh-bidbox.ohh-std-bidding { border-color: var(--accent); color: var(--accent); }
.ohh-bidbox.ohh-std-short { border-color: rgba(255, 255, 255, 0.45); }
.ohh-bidbox.ohh-std-exact {
    border-color: rgba(60, 200, 120, 0.75);
    background: rgba(60, 160, 90, 0.3);
    font-weight: 700;
}
.ohh-bidbox.ohh-std-over {
    border-color: rgba(255, 138, 128, 0.8);
    color: #ffd7d2;
    text-decoration: line-through;
}

/* --- the deal box ---------------------------------------------------------- */
/* A fixed size at each breakpoint, both text lines nowrap. The trump card's
   size has one source, --ohh-trumpcw, so the card and the height reserving it
   cannot disagree. No .mini on the card (it sets its own --cw). */
.ohh-deal {
    position: relative;
    box-sizing: border-box;
    width: var(--ohh-dealw);
    height: calc(var(--ohh-dealfs) * 1.3 * 2 + var(--ohh-trumpcw) * 1.39 + 12px);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: 3px 0;
    font-size: var(--ohh-dealfs);
    line-height: 1.3;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.16);
    overflow: hidden;
}
/* Both text lines span the box, so a readout too wide for it overflows its own
   line (the probe reads scrollWidth) instead of shrink-wrapping past the edge. */
.ohh-deal-label,
.ohh-bidsline { align-self: stretch; }
.ohh-deal-label { white-space: nowrap; text-align: center; opacity: 0.75; }
.ohh-deal .card.ohh-trumpcard { --cw: var(--ohh-trumpcw); }
.ohh-bidsline {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 3px;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
/* Reserved whether or not it holds a glyph, so the readout never shifts. */
.ohh-tilt { position: relative; display: inline-block; width: 1em; text-align: center; }
.ohh-bidsline.ohh-over .ohh-tilt { color: #ff9c8f; }
.ohh-bidsline.ohh-under .ohh-tilt { color: #8fd6a4; }

/* --- the hand ---------------------------------------------------------- */
/* A card that cannot be played now: dimmed rather than removed. */
.ohh-mute { opacity: 0.4; }

/* --- the summary ------------------------------------------------------ */
/* A real table, one row per seat, centered as a block. Every cell carries its
   own alignment, because the lifted dialog centers text and cells inherit it. */
.ohh-result {
    border-collapse: collapse;
    margin: 6px auto 0;
    font-size: 14px;
}
.ohh-result th,
.ohh-result td {
    padding: 3px 8px;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    border-bottom: 1px solid var(--panel-border);
}
/* The same table is each finished deal's body in the grouped log, where nothing
   under the heading is bold (ui-conventions). So a header cell is normal weight
   by default (a th is bold in the browser's own sheet), and the header and Total
   weights belong to the summary alone. */
.ohh-result th { font-size: 12px; font-weight: 400; opacity: 0.8; }
.ohh-summary .ohh-result th { font-weight: 600; }
.ohh-result .ohh-name { text-align: left; }
.ohh-result .ohh-num { text-align: right; }
.ohh-summary .ohh-result td.ohh-total { font-weight: 800; }
.ohh-result tr.ohh-exactrow td.ohh-name { color: var(--accent); }
/* Took reads tricks over the bid (3/4). Every cell reserves the exact bid's
   check slot, so the numbers line up down the column, and the header is
   padded by the same slot so it sits over the numbers. */
.ohh-result { --ohh-markw: 11px; }
.ohh-result .ohh-took { position: relative; }
.ohh-result th.ohh-took { padding-right: calc(8px + var(--ohh-markw)); }
.ohh-result .ohh-exactmark {
    display: inline-block;
    width: var(--ohh-markw);
    text-align: right;
    color: var(--accent);
}
/* In the log the table sits in a narrow panel (270px on a desktop), so it is
   tightened there, and only there, to fit seven seats without a sideways
   scroll. The dialog keeps the sizes above. */
#log-entries .ohh-result { --ohh-markw: 9px; font-size: 12px; margin-top: 4px; }
#log-entries .ohh-result th,
#log-entries .ohh-result td { padding: 2px 4px; }
#log-entries .ohh-result th { font-size: 11px; }
#log-entries .ohh-result th.ohh-took { padding-right: calc(4px + var(--ohh-markw)); }
.ohh-log-win { color: var(--accent); font-weight: 700; }

/* --- a finished trick in the log ------------------------------------------ */
/* Hearts' log picture (.hearts-logtrick in src/shared/hearts/style.css): a
   one-line caption beside a small cross of the trick's cards, each on its
   seat's side, so a deal skims as a column of crosses. 19px cards, so the
   simplified atlas (34px and under). A FIXED footprint per table size: every
   track is a card wide or tall whether or not a cell is filled. At 7 seats the
   viewer's card sits between Southwest and Southeast, three rows like the rest. */
#log-entries .ohh-logtrick {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 3px 0;
}
.ohh-logtrick-cap {
    font-size: 12px;
    min-width: 0;
    flex: 1 1 auto;
    opacity: 0.85;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ohh-logcross {
    --ohh-lcw: 19px;
    flex: 0 0 auto;
    display: grid;
    grid-template-columns: repeat(3, var(--ohh-lcw));
    grid-template-rows: repeat(3, calc(var(--ohh-lcw) * 1.39));
    align-items: center;
    justify-items: center;
    gap: 1px 4px;
    /* Room for the caret and the check, which hang 8px outside the outer cards. */
    padding: 0 9px;
}
.ohh-logcross-3 { grid-template-rows: repeat(2, calc(var(--ohh-lcw) * 1.39)); grid-template-areas: "w . e" ". s ."; }
.ohh-logcross-4 { grid-template-areas: ". n ." "w . e" ". s ."; }
.ohh-logcross-5 { grid-template-areas: "nw . ne" "w . e" ". s ."; }
.ohh-logcross-6 { grid-template-areas: "nw n ne" "w . e" ". s ."; }
.ohh-logcross-7 { grid-template-areas: "nw . ne" "w . e" "sw s se"; }
.ohh-logcross .card { --cw: var(--ohh-lcw); }
.ohh-logcross-cell { position: relative; line-height: 0; }
.ohh-logcross-cell.pos-s { grid-area: s; }
.ohh-logcross-cell.pos-w { grid-area: w; }
.ohh-logcross-cell.pos-n { grid-area: n; }
.ohh-logcross-cell.pos-e { grid-area: e; }
.ohh-logcross-cell.pos-nw { grid-area: nw; }
.ohh-logcross-cell.pos-ne { grid-area: ne; }
.ohh-logcross-cell.pos-sw { grid-area: sw; }
.ohh-logcross-cell.pos-se { grid-area: se; }
/* Out of flow on the card's corners, so a mark never resizes the cross. Normal
   weight: nothing under a collapsed deal's heading is bold (ui-conventions). */
.ohh-logcross-led,
.ohh-logcross-won {
    position: absolute;
    font-size: 10px;
    line-height: 1;
    pointer-events: none;
}
.ohh-logcross-led { top: -1px; left: -8px; color: var(--ink); opacity: 0.85; }
.ohh-logcross-won { bottom: -1px; right: -8px; color: var(--accent); }

/* --- the schedule table in the Cheat_Sheet --------------------------- */
/* Centered as a block; the cells stay aligned by column (ui-conventions.md). */
.ohh-schedtable { border-collapse: collapse; margin: 6px auto 10px; }
.ohh-schedtable th,
.ohh-schedtable td {
    text-align: left;
    padding: 3px 7px;
    border-bottom: 1px solid var(--panel-border);
    font-size: 13px;
}
.ohh-schedtable .ohh-schednum { text-align: right; font-variant-numeric: tabular-nums; }

/* --- touch screens above the phone width ---------------------------------- */
/* A larger number button where the pointer is a finger. The picker's rows are
   derived from it, so they grow with it. The phone block below sets its own. */
@media (pointer: coarse) {
    .ohh-board { --ohh-bidh: 34px; --ohh-placeh: 36px; }
}

/* --- phones: one row across the top ------------------------------------- */
/* Every reservation is re-derived here. The board is a flex column: the other
   seats in one row in play order, the center at its fixed width, and the foot
   (the deal box left of this player's panel). The phone center:
     picker = 6 * 36px + 5 * 4px = 236px; trick at most 3 * 44px + 2 * 4px = 140px
     --ohh-centerw = 236px + 4px + 4px = 244px
   The foot is 132px + 8px + 150px = 290px, inside the 340px row at 360px. */
@media (max-width: 720px) {
    .ohh-board {
        --ohh-centerw: 244px;
        --ohh-bidw: 36px;
        --ohh-bidh: 36px;
        --ohh-placeh: 40px;
        --ohh-notefs: 11px;
        --ohh-trickgap: 4px;
        --ohh-playcw: 44px;
        --ohh-namefs: 12px;
        --ohh-totalfs: 14px;
        --ohh-bidfs: 11px;
        --ohh-pad: 4px 5px 5px;
        --ohh-trumpcw: 36px;
        --ohh-dealfs: 11px;
        --ohh-dealw: 132px;
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
    }
    .ohh-board.ohh-seats-6,
    .ohh-board.ohh-seats-7 { --ohh-playcw: 38px; }
    .ohh-table {
        display: flex;
        flex-wrap: nowrap;
        justify-content: center;
        gap: 4px;
    }
    .ohh-table .ohh-seat { flex: 1 1 0; width: auto; }
    .ohh-center { margin-inline: auto; }
    .ohh-foot {
        display: flex;
        justify-content: center;
        align-items: center;
        gap: 8px;
    }
    .ohh-deal { flex: 0 0 var(--ohh-dealw); }
    .ohh-seat.ohh-mine { flex: 0 0 150px; width: 150px; }
    .ohh-trick { padding: 6px 8px; }
    /* Compact panels at 5 to 7 seats: the short name, no marker (the ring and
       the status line still say who acts). The full name is clipped by the
       .ohh-vh properties rather than removed, so it is still read aloud. */
    .ohh-board.ohh-compact { --ohh-namefs: 10px; --ohh-pad: 3px 4px; }
    .ohh-board.ohh-compact.ohh-seats-7 {
        --ohh-namefs: 9px;
        --ohh-totalfs: 13px;
        --ohh-bidfs: 10px;
        --ohh-pad: 3px 2px;
    }
    .ohh-compact .ohh-name-full {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }
    .ohh-compact .ohh-name-short { display: inline; }
    .ohh-compact .ohh-toact { display: none; }
    .ohh-board.ohh-seats-7 .ohh-bidbox { padding: 0 2px; }
}
/* =========================================================================
   Big Two (bigtwo)
   A four-seat climbing game: beat the play on the table with a higher play of
   the same size, or pass, and be first to shed all 13 cards. The board is
   built around three facts a player reads every turn (design 5 and 6):
     the table as a table: the viewed seat South, the others West, North, and
       East in play order, each a small fixed panel with its total, its cards
       left as a count and a bar of backs, and Passed     (.b2-seat, .pos-*)
     the play to beat at full size and the trick's earlier plays small
                                                          (.b2-center)
     a finished trick held, ringed and starred, until the next lead
                                                          (.b2-winning)
   Under the hand, the 2s and Aces nobody has played yet with the viewer's own
   ringed, and the Unplayed view of every card still out  (.b2-outbox)
   On the viewer's own turn, a card no play can use is faded (.b2-dim).
   EVERY CARD SIZE is a custom property on .b2-board (or on .b2-controls for
   the Play button's width, or on .b2-outbox for the strip and the view, since
   neither box is inside the board), never a rule on .card or a shared id, so
   each reservation below follows it.
   ANTI-JITTER (ui-conventions.md, "Containers must not move"). Every box above
   the hand is reserved at its tallest ordinary shape, and re-derived at the
   phone breakpoint: the seat panels at one height in every state (the ▶ and
   Passed are switched by .b2-off, which keeps their space), the center label
   at two lines, the play to beat at one row of five, the earlier plays at one
   scrolling row, the strip under the hand at eight fixed slots on one line
   of reserved height, the controls at one row with
   both buttons always present, the note at two lines (three on phones), the
   status line likewise. Nothing on the board is ever `hidden` in a game.
   Card heights are written as the card's own aspect, 89 / 64, rather than
   the rounded 1.39: a reservation 0.04px short of a 56px card grew the
   center by that much whenever a play landed, and moved the hand 1px.
   SHARED IDS are reached only through a class this game owns: the hand panel
   through .b2-handpanel, the status and progress lines through .b2-status and
   .b2-progress. No rule here reserves the hand tray's height (handspace.ts
   does that at runtime).
   ========================================================================= */

/* --- sizes ------------------------------------------------------------- */
.b2-board {
    --b2-playcw: 56px;
    --b2-earliercw: 30px;
    --b2-backcw: 12px;
    --b2-backstep: 6px;
    --b2-gap: 6px;
    --b2-centerw: calc(5 * var(--b2-playcw) + 4 * var(--b2-gap) + 20px);
}

/* Switched on and off without moving anything: the space is kept. */
.b2-off { visibility: hidden; }

/* --- suit glyphs in prose ---------------------------------------------- */
/* Suits decide flushes and break every tie, so a glyph is always colored.
   The platform's own log rules cover the log; these cover the board, the
   controls, and the summary. */
.b2-suited .red { color: var(--red); }
.b2-suited .red.diamond { color: var(--red-diamond); }
.b2-suited .club { color: var(--blue-club); }

/* --- the board: the table as a table ------------------------------------ */
/* ONE GRID. #b2-table is `display: contents`, so the opponents' panels are
   cells of it and sit West, North, and East of the center. The middle column is
   the center's own fixed width, so the side panels never move sideways as a
   trick grows. */
.b2-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas:
        ".    up     ."
        "left center right"
        ".    down   .";
    align-items: center;
    gap: 10px 16px;
    margin-bottom: 8px;
}
.b2-table { display: contents; }
.b2-seat.pos-left { grid-area: left; justify-self: end; }
.b2-seat.pos-right { grid-area: right; justify-self: start; }
.b2-seat.pos-up { grid-area: up; justify-self: center; }
.b2-seat.pos-down { grid-area: down; justify-self: center; }
.b2-center { grid-area: center; }

/* --- a seat's panel ------------------------------------------------------ */
/* SMALL and FIXED: a long name ellipsizes, it never grows the panel. */
.b2-seat {
    box-sizing: border-box;
    width: 176px;
    max-width: 100%;
    min-width: 0;
    padding: 6px 9px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default, so the ring never shifts the layout. */
    outline: 2px solid transparent;
}
/* The seat to act: the ring AND the ▶, so turn does not rest on color. */
.b2-seat.b2-active { outline-color: var(--accent); }
.b2-seat-head {
    display: flex;
    align-items: baseline;
    gap: 5px;
    margin-bottom: 4px;
    white-space: nowrap;
}
.b2-toact { flex: none; color: var(--accent); font-size: 11px; }
.b2-seat-name {
    flex: 1 1 0;
    min-width: 0;
    font-weight: 700;
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* The match total: penalty points, lowest wins. */
.b2-seat-total {
    flex: none;
    font-size: 16px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.b2-seat-line {
    display: flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
    min-height: calc(var(--b2-backcw) * 89 / 64);
}
/* The hand as a bar of backs, in a FIXED box sized for 13, so the line is one
   width whatever a seat holds. A number says 4; a bar says "nearly out". */
.b2-backs {
    flex: none;
    display: flex;
    align-items: center;
    width: calc(var(--b2-backcw) + 12 * var(--b2-backstep));
    height: calc(var(--b2-backcw) * 89 / 64);
    overflow: hidden;
}
.b2-backs .card.b2-back {
    --cw: var(--b2-backcw);
    margin-left: calc(var(--b2-backstep) - var(--b2-backcw));
    border-radius: 2px;
    box-shadow: -1px 0 0 rgba(0, 0, 0, 0.35);
}
.b2-backs .card.b2-back:first-child { margin-left: 0; }
.b2-count {
    flex: none;
    min-width: 2ch;
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.b2-passed {
    flex: none;
    font-size: 11px;
    opacity: 0.8;
    font-style: italic;
    white-space: nowrap;
}

/* --- the play area ------------------------------------------------------- */
/* A FIXED box: three reserved rows, the same size empty or full. */
.b2-center {
    box-sizing: border-box;
    width: var(--b2-centerw);
    max-width: 100%;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
/* The combination to beat, in words, with who played it: TWO lines reserved.
   Only the name may ever be shortened, never the combination. */
.b2-label {
    height: 2.8em;
    line-height: 1.4;
    font-size: 14px;
    white-space: normal;
    text-align: center;
    overflow: hidden;
}
.b2-label-who {
    display: inline-block;
    max-width: 10em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: bottom;
    font-weight: 700;
}
/* The play to beat, at full size: one reserved row of five. */
.b2-top {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    min-height: calc(var(--b2-playcw) * 89 / 64);
    margin-top: 4px;
}
.b2-top .b2-group {
    position: relative;
    display: flex;
    flex-wrap: nowrap;
    gap: var(--b2-gap);
    border-radius: 8px;
}
.b2-top .card.b2-topcard { --cw: var(--b2-playcw); }
/* The play that won the finished trick still on the table: ringed and starred,
   so it is not a matter of color alone. */
.b2-top .b2-group.b2-winning {
    outline: 3px solid var(--accent);
    outline-offset: 3px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
.b2-star {
    position: absolute;
    top: -11px;
    right: -11px;
    width: 18px;
    height: 18px;
    line-height: 18px;
    border-radius: 50%;
    text-align: center;
    font-size: 12px;
    color: var(--accent-ink);
    background: var(--accent);
}
/* The trick's earlier plays, oldest left: ONE row that scrolls rather than
   wraps, NOT centered (a centered scrolling strip clips its left end). Its
   height is fixed with room for a scrollbar, so a scrollbar appearing cannot
   grow it. */
.b2-earlier {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: center;
    gap: 8px;
    height: calc(var(--b2-earliercw) * 89 / 64 + 8px);
    margin-top: 6px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}
.b2-earlier .b2-group { flex: none; display: flex; gap: 2px; }
.b2-earlier .card.b2-earliercard { --cw: var(--b2-earliercw); }

/* --- the action box ---------------------------------------------------- */
/* Controls first, prose after, each in its own reserved box. */
.b2-actions { margin-bottom: 8px; }
.b2-controls {
    --b2-playw: 19em;
    flex-wrap: nowrap;
    align-items: stretch;
    margin: 4px 0;
    min-height: calc(2 * 1.25em + 16px);
}
/* FIXED widths, so a changing label never moves the other button. Play holds
   two text lines at every width, which the longest label needs on a phone. */
.b2-controls .btn.b2-play {
    flex: none;
    box-sizing: border-box;
    width: var(--b2-playw);
    min-height: calc(2 * 1.25em + 16px);
    line-height: 1.25;
    white-space: normal;
    overflow-wrap: anywhere;
}
.b2-controls .btn.b2-pass {
    flex: none;
    box-sizing: border-box;
    width: 6em;
    white-space: nowrap;
}
.b2-controls .btn.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
/* Why a selection cannot be played, or the hint: two lines reserved. */
.b2-note {
    min-height: 2.6em;
    line-height: 1.3;
    font-size: 14px;
    text-align: center;
}

/* --- the hand ---------------------------------------------------------- */
/* The 3♦ at the opening: a dashed accent outline and a ▲, not color alone. */
.b2-handpanel .card.b2-must {
    position: relative;
    outline: 2px dashed var(--accent);
    outline-offset: 2px;
}
.b2-handpanel .card.b2-must.sel { outline-style: solid; }
.b2-handpanel .card.b2-must::after {
    content: '\25B2';
    position: absolute;
    top: -9px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 11px;
    line-height: 1;
    color: var(--accent);
}
/* On the viewer's own turn: a card no allowed play uses (not a button), or,
   with a selection, one that cannot join it (still tappable). Lightly faded,
   no filter, so rank and suit stay readable. */
.b2-handpanel .card.b2-dim { opacity: 0.45; }

/* --- under the hand: the 2s and Aces, and the Unplayed view ------------- */
.b2-outbox {
    --b2-outcw: 28px;
    --b2-upcw: 26px;
    margin: 0 0 8px;
}
/* The Unplayed button and eight fixed slots, a played one an empty dashed
   outline, so the strip is one size all deal. ONE line, centered, its height
   reserved at the taller of a card and the coarse-pointer button minimum. */
.b2-out {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 8px;
    width: max-content;
    max-width: 100%;
    min-height: max(calc(var(--b2-outcw) * 89 / 64), 36px);
    margin: 0 auto;
    white-space: nowrap;
}
.b2-out .btn.b2-unplayed-btn { flex: none; white-space: nowrap; }
/* Open or closed, said by a caret as well as aria-expanded. */
.b2-unplayed-btn::after { content: ' \25BE'; }
.b2-unplayed-btn[aria-expanded="true"]::after { content: ' \25B4'; }
/* Room between the slots for the ring. */
.b2-outslots { display: flex; flex: none; gap: 4px; }
.b2-out .card.b2-outcard,
.b2-out .card.b2-gone { --cw: var(--b2-outcw); }
.b2-outbox .card.b2-gone { border-color: rgba(255, 255, 255, 0.3); }
/* The viewer's own: a ring AND a dot, so it is not color alone. Both are
   drawn outside the layout (outline, an absolute ::after). */
.b2-outbox .card.b2-yours {
    position: relative;
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}
.b2-outbox .card.b2-yours::after {
    content: '';
    position: absolute;
    top: -3px;
    right: -3px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--accent);
}
/* The view: below the strip, so only what is under it moves, on the
   player's own press. Every row is the same fixed cells, so the ranks line
   up down the columns. */
.b2-unplayed {
    box-sizing: border-box;
    width: max-content;
    max-width: 100%;
    margin: 6px auto 0;
    padding: 8px 10px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.b2-uphead { display: flex; align-items: baseline; justify-content: center; gap: 10px; }
.b2-uplegend {
    margin-bottom: 6px;
    font-size: 12px;
    text-align: center;
    white-space: nowrap;
}
.b2-upgrid { display: flex; flex-direction: column; gap: 4px; }
.b2-uprow { display: flex; flex-wrap: nowrap; align-items: center; gap: 3px; }
.b2-upsuit {
    flex: none;
    width: 1.4em;
    text-align: center;
    font-size: 15px;
}
.b2-upsuit.red { color: var(--red); }
.b2-upsuit.red.diamond { color: var(--red-diamond); }
.b2-upsuit.club { color: var(--blue-club); }
.b2-uprow .card.b2-upcard,
.b2-uprow .card.b2-gone { --cw: var(--b2-upcw); flex: none; }

/* --- status and progress ----------------------------------------------- */
.status.b2-status { line-height: 1.35; min-height: 2.7em; }
/* Lit whenever this device may act. */
.status.b2-status.b2-turn { color: var(--accent); font-weight: 700; }
.progress.b2-progress { min-height: 1.4em; white-space: nowrap; }

/* --- the summary ------------------------------------------------------- */
/* A real table: each column sized once for every row, so the header sits over
   its own column. Names left, numbers right in tabular figures, and the table
   centered as a block (the lifted dialog centers text; every cell says its
   own alignment). */
.b2-result {
    border-collapse: collapse;
    margin: 0 auto 10px;
    font-size: 14px;
}
.b2-result th,
.b2-result td {
    padding: 4px 10px;
    border-bottom: 1px solid var(--panel-border);
    white-space: nowrap;
}
.b2-result th { font-size: 12px; font-weight: 600; opacity: 0.85; }
.b2-result .b2-name { text-align: left; }
.b2-result .b2-num { text-align: right; font-variant-numeric: tabular-nums; }
.b2-result tr.b2-best td { color: var(--accent); font-weight: 700; }

/* --- phones ------------------------------------------------------------ */
/* The three opponents go in ONE ROW across the top, still West, North, and East
   from left to right, the center under them, the viewer's panel under that.
   Every reservation above is re-derived here. */
@media (max-width: 720px) {
    .b2-board {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        grid-template-areas:
            "left   up     right"
            "center center center"
            "down   down   down";
        gap: 8px 6px;
        align-items: start;
    }
    .b2-seat.pos-left,
    .b2-seat.pos-up,
    .b2-seat.pos-right { width: auto; justify-self: stretch; padding: 5px 6px 6px; }
    .b2-center { justify-self: center; }
    /* Passed never shares the backs' row here: it always sits on a second,
       reserved row of the line, so a panel is one height whether or not anybody
       passed. */
    .b2-seat-line { flex-wrap: wrap; row-gap: 2px; }
    .b2-seat-line .b2-passed { flex-basis: 100%; }
    .b2-seat-total { font-size: 15px; }
    .b2-controls { --b2-playw: min(19em, 100% - 6em - 8px); }
    .b2-note { min-height: 3.9em; }
    .status.b2-status { min-height: 4.05em; }
    /* The Unplayed view on a phone: 13 x 22 + 13 x 2 + an 18px suit column
       and 12px of padding is 342px, inside a 412px or 390px screen. */
    .b2-outbox { --b2-upcw: 22px; }
    .b2-uprow { gap: 2px; }
    .b2-upsuit { width: 1.2em; }
    .b2-unplayed { padding: 6px 6px; }
}
@media (max-width: 400px) {
    .b2-board {
        --b2-playcw: 52px;
        --b2-earliercw: 26px;
        --b2-backcw: 8px;
        --b2-backstep: 3px;
    }
    /* 8 x 24 + 7 x 4 = 220px of slots beside the button fits 360px; the view
       is 13 x 20 + 13 x 2 + its suit column, about 320px with its padding. */
    .b2-outbox {
        --b2-outcw: 24px;
        --b2-upcw: 20px;
    }
}
/* ===================== Order vs. Chaos (ovc-) =====================
   design.md Components 4, 13, 15, 16, 18, 22. Every class is prefixed ovc-,
   and every rule that names a shared id (#hand-cards, #status-line) is
   scoped through a class this game owns (.ovc-handpanel, .ovc-status).
   No min-height on #hand-cards: src/shared/ui/handspace.ts reserves it. */

/* ---------- the table: side column + board from 951px up, one column below ---------- */
.ovc-table {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}
.ovc-table .ovc-side {
    align-self: stretch;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}
.ovc-table .ovc-side > .ovc-seat,
.ovc-table .ovc-side > .ovc-maybe {
    width: 100%;
    max-width: 360px;
    margin-inline: auto;
}
.ovc-side .panel { margin: 0; }
@supports (container-type: inline-size) {
    .ovc-side { container-type: inline-size; }
}
@media (min-width: 951px) {
    .ovc-table {
        display: grid;
        grid-template-columns: minmax(288px, 360px) auto;
        grid-template-rows: auto 1fr;
        grid-template-areas: "opp board" "me board";
        justify-content: center;
        column-gap: 16px;
        row-gap: 8px;
    }
    .ovc-table .ovc-opp-side { grid-area: opp; }
    .ovc-table .ovc-board { grid-area: board; }
    .ovc-table .ovc-me-side { grid-area: me; align-self: end; }
    .ovc-table .ovc-side > .ovc-seat,
    .ovc-table .ovc-side > .ovc-maybe { max-width: none; }
}

/* ---------- the board's card width: the smaller of a width and a height term ---------- */
.ovc-board {
    --ovc-cw: round(down, clamp(36px, min(calc((100vw - 44px) / 6.9),
                                          calc((100vh - 132px) / 8.06)), 64px), 1px);
}
@supports not (width: round(down, 1.5px, 1px)) {
    .ovc-board {
        --ovc-cw: clamp(36px, min(calc((100vw - 44px) / 6.9), calc((100vh - 132px) / 8.06)), 64px);
    }
}
@media (min-width: 951px) {
    .ovc-board {
        --ovc-cw: round(down, clamp(40px, min(calc((100vh - 354px) / 8.06),
                                              calc((100vw - 711px) / 6)), 72px), 1px);
    }
    @supports not (width: round(down, 1.5px, 1px)) {
        .ovc-board {
            --ovc-cw: clamp(40px, min(calc((100vh - 354px) / 8.06), calc((100vw - 711px) / 6)), 72px);
        }
    }
}
/* The summary's revealed mini-board (two classes, so it beats either rule above). */
.ovc-board.ovc-mini { --ovc-cw: 30px; margin: 8px auto; }

/* ---------- the fractal: two bands of three two-column boxes ---------- */
.ovc-board {
    --ovc-ch: calc(var(--ovc-cw) * 1.39);
    display: grid;
    grid-template-columns: repeat(6, var(--ovc-cw));
    column-gap: 6px;
    row-gap: 12px;
    padding: 6px;
    width: max-content;
    justify-self: center;
    margin-inline: auto;
    box-sizing: content-box;
}
.ovc-board > * { grid-column: span 2; }
.ovc-board .card { --cw: var(--ovc-cw); }

/* A triangle: two rows, 6px apart, over a rounded upright outline. */
.ovc-tri {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 6px;
    width: calc(2 * var(--ovc-cw) + 6px);
}
.ovc-tri::before {
    content: '';
    position: absolute;
    inset: -3px;
    pointer-events: none;
    background: center / 100% 100% no-repeat
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M50 2 L98 98 L2 98 Z' fill='rgba(255,255,255,0.04)' stroke='rgba(255,255,255,0.28)' stroke-width='1' vector-effect='non-scaling-stroke' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.ovc-tri > * { position: relative; }
/* Bid frames (D3; F2's words): solid = can still align, dashed and dimmed = can no longer align,
   a thick ring = matches. State is never shown by color alone. */
.ovc-tri.ovc-live::before {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M50 2 L98 98 L2 98 Z' fill='rgba(255,255,255,0.07)' stroke='rgba(255,255,255,0.75)' stroke-width='2' vector-effect='non-scaling-stroke' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.ovc-tri.ovc-ruledout::before,
.ovc-tri.ovc-nomatch::before {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M50 2 L98 98 L2 98 Z' fill='rgba(0,0,0,0.18)' stroke='rgba(255,255,255,0.55)' stroke-width='1.5' stroke-dasharray='5 4' vector-effect='non-scaling-stroke' stroke-linejoin='round'/%3E%3C/svg%3E");
    filter: brightness(0.8);
}
.ovc-tri.ovc-match::before {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M50 2 L98 98 L2 98 Z' fill='rgba(255,209,102,0.10)' stroke='rgb(255,209,102)' stroke-width='3' vector-effect='non-scaling-stroke' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.ovc-trirow { display: flex; gap: 6px; }
.ovc-apexrow { align-items: stretch; }
.ovc-inner { align-self: center; }

/* The label strips: the empty upper corners beside the apex (no height cost). */
.ovc-trilabel,
.ovc-tribid {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    white-space: nowrap;
    overflow: hidden;
}
.ovc-trilabel {
    padding: 0;
    border: 0;
    background: none;
    min-height: 0;
    font-family: inherit;
    color: inherit;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
}
.ovc-striptext,
.ovc-tribid {
    writing-mode: vertical-rl;
    /* Every glyph turns with the line: under the default `mixed`, the
       apostrophe of "can't align" (F2) stood upright and read "can ` t". */
    text-orientation: sideways;
    font-size: 12px;
    line-height: 14px;
    font-weight: 600;
}
.ovc-tribid { cursor: default; }
/* The 12px font's content area is 16px across, wider than the 15px strip at
   the smallest card width (36px, a landscape phone), though its ink is under
   10px. Clipping the text box keeps that empty margin out of the strip's
   scrollable overflow; nothing visible changes (measured by tools/ordervschaos/ovc-probe.mjs). */
.ovc-striptext,
.ovc-bidtext { overflow: hidden; }
.ovc-ruledout .ovc-tribid,
.ovc-nomatch .ovc-tribid { opacity: 0.9; font-style: italic; }
.ovc-match .ovc-tribid { color: var(--accent); }
/* The phase digit stands upright inside the vertical text (C5). */
.ovc-triphase {
    writing-mode: horizontal-tb;
    display: inline-block;
    box-sizing: border-box;
    width: 14px;
    height: 14px;
    margin-top: 4px;
    border: 1px solid currentColor;
    border-radius: 50%;
    font-size: 10px;
    line-height: 12px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

/* ---------- spaces and tricks ---------- */
.ovc-space,
.ovc-ispace {
    position: relative;
    flex: none;
    width: var(--ovc-cw);
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    -webkit-appearance: none;
    appearance: none;
}
.ovc-space { height: calc(var(--ovc-ch) * 1.45); }
.ovc-ispace { height: var(--ovc-ch); }
.ovc-space::before,
.ovc-ispace::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: var(--ovc-ch);
    box-sizing: border-box;
    border: 1px dashed rgba(255, 255, 255, 0.3);
    border-radius: 6%;
}
.ovc-space .card.ovc-cardon,
.ovc-ispace .card.ovc-marker {
    position: absolute;
    left: 0;
    top: 0;
}
/* The leader's card below, the follower's on top, offset so the leader's rank shows. */
.ovc-space .card.ovc-followcard { top: calc(var(--ovc-ch) * 0.45); }
button.ovc-space { cursor: pointer; }
.ovc-space.ovc-open::before {
    border-color: rgba(255, 209, 102, 0.65);
    box-shadow: 0 0 6px rgba(255, 209, 102, 0.35);
}
.ovc-space.ovc-lit::before {
    border: 2px solid var(--accent);
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.75);
}
.ovc-space.ovc-last::after {
    content: '';
    position: absolute;
    inset: -3px;
    border: 2px solid rgba(255, 255, 255, 0.55);
    border-radius: 6px;
    pointer-events: none;
}
/* No z-index here: the follower's card always paints over the leader's, so a
   ringed leader keeps its ring on its exposed top and sides and never covers
   the follower's rank (build review 3, finding 2). */
.ovc-space .card.ovc-won { outline: 3px solid var(--accent); outline-offset: 1px; }
.ovc-space.hinted::before { animation: hintpulse 0.9s ease-in-out infinite alternate; border-color: var(--accent); }
/* The flip of a held Chaos-won trick, on the independent `scale` property so it
   composes with any transform the animator writes (ui-conventions.md). */
@keyframes ovc-flip {
    from { scale: 0 1; }
    to { scale: 1 1; }
}
.ovc-space .card.ovc-flip { animation: ovc-flip 320ms ease-out; }
/* F5: a marker fades in where it is placed (an Inner Space, a score pile) and
   never slides in. def.ts MARKER_FADE_MS is this duration; a repaint during the
   fade carries it on with a negative animation-delay. */
@keyframes ovc-fadein {
    from { opacity: 0; }
    to { opacity: 1; }
}
.ovc-ispace .card.ovc-marker.ovc-fadein,
.ovc-pile .card.ovc-pilecard.ovc-fadein { animation: ovc-fadein 450ms ease-out both; }
body.motion-off .ovc-ispace .card.ovc-marker.ovc-fadein,
body.motion-off .ovc-pile .card.ovc-pilecard.ovc-fadein { animation: none; }

/* ---------- the corner boxes: explanations and the round ---------- */
.ovc-explain {
    box-sizing: border-box;
    width: calc(2 * var(--ovc-cw) + 6px);
    height: calc(4.031 * var(--ovc-cw) + 6px);
    padding: 3px 4px;
    font-size: 12px;
    line-height: 15px;
    overflow: hidden auto;
    overflow-wrap: break-word;
    color: rgba(242, 239, 233, 0.9);
    text-align: left;
}
.ovc-roundbox {
    box-sizing: border-box;
    width: calc(2 * var(--ovc-cw) + 6px);
    height: calc(4.031 * var(--ovc-cw) + 6px);
    padding: 3px 4px;
    text-align: center;
    overflow: hidden;
    overflow-wrap: break-word;
}
.ovc-roundno { font-weight: 700; white-space: nowrap; }
.ovc-roundnote { font-size: 12px; line-height: 15px; margin-top: 4px; }

/* ---------- seats (D9, D5) ---------- */
.ovc-seat {
    box-sizing: border-box;
    height: 71px;
    padding: 8px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    background: var(--panel);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
/* Light edge for Order (face up), dark edge for Chaos (face down). */
.ovc-seat.ovc-role-order { border-left: 5px solid #f2efe9; }
.ovc-seat.ovc-role-chaos { border-left: 5px solid #11161c; }
.ovc-seat.ovc-acting { box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.45); }
.ovc-seatid {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}
.ovc-badge {
    display: flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
}
.ovc-seat .card.ovc-emblem { --cw: 22px; box-shadow: none; }
.ovc-roleword { font-weight: 700; }
.ovc-name {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 13px;
}
.ovc-pilebox {
    display: flex;
    align-items: center;
    gap: 6px;
    flex: none;
}
/* The pile reserves six fanned 38px cards, so a banked marker grows nothing. */
.ovc-pile {
    display: flex;
    width: 143px;
    height: 53px;
    align-items: flex-start;
}
.ovc-pile .card.ovc-pilecard { --cw: 38px; }
.ovc-pile .card.ovc-pilecard + .card.ovc-pilecard { margin-left: calc(-0.45 * 38px); }
.ovc-pilecap {
    white-space: nowrap;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}

/* ---------- the "might be holding" panel (Component 15; C3, C4, C7) ---------- */
.ovc-maybe { --ovc-mcw: 28px; box-sizing: border-box; }
@supports (container-type: inline-size) {
    .ovc-maybe { --ovc-mcw: clamp(22px, calc((100cqw - 48px) / 10.6), 30px); }
}
@supports (width: round(down, 1.5px, 1px)) and (container-type: inline-size) {
    .ovc-maybe { --ovc-mcw: round(down, clamp(22px, calc((100cqw - 48px) / 10.6), 30px), 1px); }
}
.ovc-maybe .panel-head {
    flex-wrap: nowrap;
    height: 31px;
    overflow: hidden;
}
.ovc-maybe .panel-head > * { white-space: nowrap; }
.ovc-maybe .ovc-maybetitle {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ovc-maybe .ovc-maybecap {
    flex: none;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}
.ovc-maybegrid {
    display: flex;
    flex-direction: column;
    gap: 3px;
    align-items: center;
}
.ovc-mayberow {
    display: flex;
    align-items: center;
    column-gap: calc(var(--ovc-mcw) / 15);
}
.ovc-suitlabel {
    width: 14px;
    flex: none;
    text-align: center;
    font-weight: 700;
}
.ovc-suitlabel.red { color: var(--red); }
.ovc-suitlabel.red.diamond { color: var(--red-diamond); }
.ovc-suitlabel.club { color: var(--blue-club); }
.ovc-maybe .card.ovc-maybecard { --cw: var(--ovc-mcw); box-shadow: none; }
/* Dimmed in place by filter, never opacity, so the grid never changes shape
   and nothing behind shows through. Feedback round 2 (F1, "Fade the impossible
   cards much more"): a card that cannot be in the opponent's hand recedes to
   about a third of full strength, so the possible cards stand out at a glance.
   This panel only: the ~0.85 legibility floor is for cards being decided
   about, and these are out of the question. Picked from side-by-side
   screenshots at 1280 and 390 (final-report.md, Feedback round 2). */
.ovc-maybe .card.ovc-out { filter: grayscale(1) brightness(0.4); }
.ovc-maybenote {
    height: 17px;
    line-height: 17px;
    font-size: 12px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-align: center;
    margin-top: 2px;
}

/* ---------- the hand row and the Chaos decision (D8, D10) ---------- */
.ovc-handpanel #hand-cards .card.ovc-suitgap { margin-left: 6px; }
/* A card you cannot play on this follow (F7, "fade the cards that you can't
   play"). Opaque, by filter, with its rank and suit still readable. The old
   grayscale(0.7) brightness(0.9) read as no fade at all: a Spade is already
   black and white, so the grayscale changed nothing on it, and a 10% dim on a
   white card does not show (final-report.md, Feedback round 2). */
.ovc-handpanel #hand-cards .card.ovc-unplayable { filter: grayscale(1) brightness(0.55); }
.ovc-handpanel .ovc-actions { display: none; }
.ovc-handpanel.ovc-decider .ovc-actions {
    display: flex;
    flex-direction: row;
    gap: 8px;
    justify-content: center;
    min-height: 44px;
}
.ovc-handpanel.ovc-decider .ovc-handrow {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.ovc-handpanel.ovc-decider .ovc-decide { flex: 1 1 0; max-width: 200px; }
@media (min-width: 951px) {
    .ovc-handpanel.ovc-decider .ovc-handrow {
        flex-direction: row;
        column-gap: 8px;
        align-items: flex-start;
        min-height: calc(2 * 44px + 8px);
    }
    .ovc-handpanel.ovc-decider .ovc-handrow > #hand-cards { flex: 1 1 auto; min-width: 0; }
    .ovc-handpanel.ovc-decider .ovc-actions {
        flex: 0 0 128px;
        order: -1;
        flex-direction: column;
        min-height: calc(2 * 44px + 8px);
    }
    .ovc-handpanel.ovc-decider .ovc-decide { flex: none; max-width: none; }
    /* The hand is centered under its title (ui-conventions.md, final2-review
       F2-2): the title centers over the cards' column rather than the whole
       row, by an inset equal to the decision column plus its gap, so the two
       midpoints agree. A matching spacer on the right would leave #hand-cards
       898 - 2 * 136 = 626px at 1280x800, narrower than the 736px nine-card
       hand, which would then wrap to two rows and break the D3 fit. The
       inset is fixed for the game, like the column, so nothing moves when
       the decision buttons appear or go. */
    .ovc-handpanel.ovc-decider > .panel-head { padding-left: calc(128px + 8px); }
}
/* Exactly 44px with .btn's 1px border: 18 + 16 + 2 * 4 + 2 (border-box). */
.ovc-actions .btn.ovc-decide {
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    min-height: 44px;
    padding: 4px 6px;
    min-width: 0;
}
.ovc-decidelabel,
.ovc-decidenote {
    display: block;
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ovc-decidelabel { font-size: 14px; font-weight: 600; line-height: 18px; }
.ovc-decidenote { font-size: 12px; line-height: 16px; opacity: 0.85; }
.ovc-actions .btn.ovc-decide.hinted { animation: hintpulse 0.9s ease-in-out infinite alternate; }

/* ---------- the status line: two lines reserved at every breakpoint ---------- */
.status.ovc-status {
    line-height: 1.35;
    min-height: calc(2 * 1.35em);
}

/* ---------- the summaries (Component 18; C2) ---------- */
.ovc-summary .result-actions .btn {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
/*
 * The actions row is always in view. Lifted over the board, the summary is its
 * own scroll box (max-height min(80vh, 680px)), and the final summary (every
 * round's picture and rows) is far taller than that, so "Play again" would sit
 * below the dialog's fold (it did for the busier first round summary too). The
 * row sticks to the bottom of the box and the summary scrolls under it (the
 * Expeditions recipe, also in Counterbluff and Draft Cribbage). Scoped to
 * `.result-overlay`, so the inline summary in a review is untouched. The
 * offsets are the panel's own padding (14px below, 16px at the sides), and the
 * background is the dialog's color as it appears under the shell's dimming
 * backdrop. The shell appends "Review game" into this same row.
 */
.panel.result-overlay .ovc-summary .result-actions {
    position: sticky;
    bottom: -14px;
    z-index: 1;
    margin: 8px -16px 0;
    padding: 10px 16px 14px;
    background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)),
        linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
    box-shadow: 0 -8px 12px -6px rgba(0, 0, 0, 0.5);
}
/* F6: a summary is its heading, the round's picture, and the points table;
   the final summary is one points table holding every round's heading,
   picture, and rows, so the numbers line up under one head. */
.ovc-points {
    border-collapse: collapse;
    margin: 8px auto;
    font-size: 13px;
}
.ovc-points th,
.ovc-points td {
    text-align: left;
    padding: 3px 8px;
    border-bottom: 1px solid var(--panel-border);
    white-space: nowrap;
}
.ovc-points .ovc-num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.ovc-points tr.ovc-winrow td { font-weight: 700; }
.ovc-final tr.ovc-roundrow th {
    padding-top: 12px;
    font-weight: 700;
    border-bottom: 0;
}
.ovc-final tr.ovc-picrow td { padding: 0 0 4px; }
.ovc-final tr.ovc-picrow .ovc-board.ovc-mini { margin: 0 auto; }
/* On a phone the table wraps its text cells rather than widen the dialog:
   the lifted dialog's content box is about 325px at 390 and 297px at 360,
   narrower than the nowrap points table (measured by tools/ordervschaos/ovc-probe.mjs).
   Numbers and "2 of 4" never wrap. */
@media (max-width: 480px) {
    .ovc-points { font-size: 12px; }
    .ovc-points th,
    .ovc-points td {
        padding: 3px 4px;
        white-space: normal;
    }
    .ovc-points .ovc-num { white-space: nowrap; }
}
.ovc-board.ovc-mini .ovc-cornerpad { width: calc(2 * var(--ovc-cw) + 6px); }
/* =========================================================================
   Dilemma (dilemma)
   A two-player trick game: four cards a trick, one of each suit, two from
   each player. design.md "Board" (D3, D4, D5) and "Definition" (D6, D7) are
   the source for every figure below.
     one centered column: the opponent's band, the table (the stock and the
       trick, the trick on the board's axis), this player's band with the
       action row, the hand (.dlm-wrap, #dlm-board, .dlm-side, .dlm-table)
     the trick as two rows, each player's two cards on that player's side,
       an empty slot showing a glyph per suit still allowed there, the next
       slot outlined, each row's total at its end (.dlm-row, .dlm-slot,
       .dlm-glyph, .dlm-next, .dlm-total)
     the held trick: the winner's row ringed, the last card marked as the
       card kept (.dlm-won, .dlm-scoredslot)
     the opponent's hand as small backs, face up from the reveal
       (#dlm-opp-hand, .dlm-note)
     the score piles beside the names, wrapping onto reserved rows and
       hidden on phones, and the stock (.dlm-strip, .dlm-stock), all small
       simplified cards; no discard pile (Playtest round 1, G4, G5)
     the exchange's suit buttons (.dlm-actions, .dlm-suitbtn)
     the explanation balloon (.dlm-explain) and its button reset (.dlm-xbtn)
     the deck viewer the stock opens (.dlm-deck, round 1, G3)
     the summaries (.dlm-sum, .dlm-deals: real tables)
   LAYOUT KEYS ON THE BOARD'S OWN WIDTH. .dlm-wrap is the size container and
   every size is a custom property on #dlm-board set by a container query
   (Agony Aunt's mechanism). The hand panel sits outside the wrapper, so its
   card size keys on the viewport.
   ANTI-JITTER. Every band row reserves its height in small-card units, the
   trick is the same size empty or full, the note line is reserved before
   the reveal, the action row reserves one 44px button, and the status line
   reserves its tallest line count at each width.
   Shared ids are scoped through a class this game owns (.dlm-status,
   .dlm-progress, .dlm-handpanel, .dlm-result), never named bare.
   ========================================================================= */

/* --- colors ------------------------------------------------------------- */
/* Suit glyphs on the felt, colored wherever they appear (suits decide every
   play). `#log-entries .red` and its companions already cover the log. */
.dlm-status .red, .dlm-progress .red, .dlm-board .red, .dlm-actions .red,
.dlm-result .red, .dlm-deck .red { color: var(--red); }
.dlm-status .red.diamond, .dlm-progress .red.diamond, .dlm-board .red.diamond,
.dlm-actions .red.diamond, .dlm-result .red.diamond, .dlm-deck .red.diamond { color: var(--red-diamond); }
.dlm-status .club, .dlm-progress .club, .dlm-board .club, .dlm-actions .club,
.dlm-result .club, .dlm-deck .club { color: var(--blue-club); }
/* The balloon is light: the cream inks (Agony Aunt's, 4.8:1 or better). */
.dlm-explain { --dlm-ink-red: #b3202a; --dlm-ink-diamond: #b13f12; --dlm-ink-club: #1d4f91; }
.dlm-explain .red { color: var(--dlm-ink-red); }
.dlm-explain .red.diamond { color: var(--dlm-ink-diamond); }
.dlm-explain .club { color: var(--dlm-ink-club); }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .dlm-explain { --dlm-ink-diamond: var(--fc-diamond-ink); --dlm-ink-club: var(--fc-club-ink); }

/* --- the status line and the progress line ----------------------------- */
/* Two lines reserved at every width above 560px (the position-2 sentence and
   a hint); three below it. */
.status.dlm-status { line-height: 1.3; min-height: 2.6em; }
.dlm-hintnote { opacity: 0.85; font-weight: 400; }
/* One nowrap line; "of 100" is an inline button styled as text, so it adds
   no height. */
.progress.dlm-progress {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    line-height: 20px;
}
.dlm-progress .dlm-of100 {
    line-height: inherit;
    text-decoration: underline dotted;
    text-underline-offset: 3px;
    cursor: help;
}

/* --- the explanation trigger reset -------------------------------------- */
/* Turning a box into a button changes neither its look nor its size. */
.dlm-xbtn {
    appearance: none;
    -webkit-appearance: none;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    text-align: inherit;
    cursor: help;
}
.dlm-xbtn:focus-visible, .dlm-board .card:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
/* Visually hidden, read by assistive technology. */
.dlm-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* --- the board ----------------------------------------------------------- */
.dlm-wrap { container: dlm / inline-size; }
/* The narrowest tier is the base; each wider tier is a container query. */
#dlm-board {
    --dlm-trickcw: 50px;
    --dlm-smallcw: 24px;
    --dlm-oppgap: 1px;
    --dlm-smallh: calc(var(--dlm-smallcw) * 89 / 64);
    /* The score piles' cards, one size at every width that shows them. */
    --dlm-pilecw: 24px;
    --dlm-pileh: calc(var(--dlm-pilecw) * 89 / 64);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    margin: 0 auto;
    padding: 0;
}

/* The two bands. Every area reserves its height and every label is nowrap,
   so no band changes height during a game. */
.dlm-side {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 8px;
    row-gap: 2px;
    align-items: center;
}
.dlm-side.dlm-far { grid-template-areas: "head strip" "hand hand" "note note"; }
.dlm-side.dlm-near { grid-template-areas: "head strip" "acts acts"; }
.dlm-head {
    grid-area: head;
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    min-height: calc(var(--dlm-smallh) + 8px);
    white-space: nowrap;
}
.dlm-name {
    font-weight: 700;
    font-size: 14px;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 120px;
}
.dlm-pts {
    display: inline-block;
    min-width: 2.4em;
    padding: 0 7px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    line-height: 20px;
    font-size: 13px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}
.dlm-note {
    grid-area: note;
    min-height: 16px;
    line-height: 16px;
    font-size: 12px;
    white-space: nowrap;
    text-align: center;
}
.dlm-note .dlm-notebtn { line-height: 16px; opacity: 0.9; text-decoration: underline dotted; text-underline-offset: 2px; }
.dlm-opphand {
    grid-area: hand;
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: var(--dlm-oppgap);
    min-height: calc(var(--dlm-smallh) + 8px);
    min-width: 0;
}
/* A score pile sits beside its name and WRAPS onto rows reserved for the
   most it can hold, never a scroll bar (Playtest round 1, G5). A side keeps
   one card a trick it wins, so at most 13 in a deal:
     a board under 500px (phones): hidden, the points chip still shows and
       the deck viewer lists every played card;
     500 to 899px: one row, 13 x 24 + 12 x 2 = 336px, which fits beside the
       name at every width in the range (the name truncates at 90 or 120px);
     900px and up: the 220px column under the name holds 8 a row, so 2 rows
       (200px and 7 a row below a 930px board, still 2 rows).
   The pile is left-aligned with its name. The height is fixed, and the box
   clips, so no card count can make the band grow. */
.dlm-strip {
    grid-area: strip;
    display: none;
    min-width: 0;
    height: var(--dlm-pileh);
    overflow: hidden;
    align-items: flex-start;
}
.dlm-stripbtn {
    display: flex;
    flex-wrap: wrap;
    align-content: flex-start;
    gap: 2px;
    width: 100%;
    height: 100%;
    border-radius: 6px;
}
.dlm-strip .card.dlm-pilecard { --cw: var(--dlm-pilecw); }
@container dlm (min-width: 500px) {
    .dlm-strip { display: flex; }
}

/* Small cards: the opponent's row, the piles, the stock, the deck viewer.
   Every one is 34px or less, so every one is simplified (ui-conventions). */
.card.dlm-small { --cw: var(--dlm-smallcw, 24px); }

/* The table: the trick in the middle track of three, so it sits exactly on
   the board's axis with no discard pile to balance the stock (G4); the stock
   sits at the inner edge of the left track. */
.dlm-table {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: center;
    column-gap: 8px;
}
.dlm-table .dlm-stock { grid-column: 1; justify-self: end; }
.dlm-table .dlm-trick { grid-column: 2; }
.dlm-stock {
    width: calc(var(--dlm-smallcw) + 12px);
    display: flex;
    justify-content: center;
}
/* One footprint, full or empty: the pile (a small card and its 4px offset),
   the 3px gap, and the 18px count chip. The empty outline sits where the
   pile was, and the chip's place stays blank (an empty chip says nothing). */
.dlm-stockbtn {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: center;
    gap: 3px;
    min-height: calc(var(--dlm-smallh) + 25px);
    border-radius: 6px;
}
/* The stock opens the deck viewer: it looks pressable (a pointer, a ring on
   hover, the ring kept while the viewer is open), but it is a view toggle,
   never .clickable. An outline paints outside the box, so nothing moves. */
.dlm-stockbtn.dlm-decktoggle {
    cursor: pointer;
    outline: 2px solid transparent;
    outline-offset: 3px;
}
@media (hover: hover) {
    .dlm-stockbtn.dlm-decktoggle:hover { outline-color: rgba(255, 209, 102, 0.55); }
    .dlm-stockbtn.dlm-decktoggle:hover .dlm-pile .card { filter: brightness(1.15); }
}
.dlm-stockbtn.dlm-decktoggle[aria-expanded="true"] { outline-color: var(--accent); }
.dlm-pile {
    position: relative;
    display: block;
    width: var(--dlm-smallcw);
    height: calc(var(--dlm-smallh) + 4px);
}
.dlm-pile .dlm-stk1 { position: absolute; left: 0; top: 4px; }
.dlm-pile .dlm-stk2 { position: absolute; left: 3px; top: 0; }
.dlm-emptypile {
    border: 1.5px dashed var(--panel-border);
    border-radius: 3px;
    height: var(--dlm-smallh);
}
.dlm-count {
    min-width: 2em;
    padding: 0 5px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    font-size: 12px;
    line-height: 16px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

/* The trick (U4, U5, U6): two rows, each [slot 1][slot 2][total]. */
.dlm-trick {
    display: grid;
    grid-template-rows: auto auto;
    row-gap: 6px;
    justify-content: center;
}
.dlm-row {
    display: grid;
    grid-template-columns: var(--dlm-trickcw) var(--dlm-trickcw) 2.6em;
    column-gap: 6px;
    align-items: center;
    padding: 2px;
    border-radius: 8px;
    outline: 2px solid transparent;
    outline-offset: 0;
}
.dlm-row.dlm-won { outline-color: var(--accent); }
.dlm-slot {
    position: relative;
    width: var(--dlm-trickcw);
    height: calc(var(--dlm-trickcw) * 89 / 64);
    border-radius: 6%;
}
.dlm-slot.dlm-open { border: 1px dashed rgba(255, 255, 255, 0.28); box-sizing: border-box; }
.dlm-trick .card.dlm-trickcard { --cw: var(--dlm-trickcw); }
.dlm-slot.dlm-next { outline: 2px dashed var(--accent); outline-offset: 2px; }
.dlm-slotbtn {
    width: 100%;
    height: 100%;
    display: grid;
    grid-template-columns: repeat(2, auto);
    place-content: center;
    gap: 2px;
}
.dlm-glyph {
    font-size: max(15px, calc(var(--dlm-trickcw) * 0.3));
    line-height: 1;
    opacity: 0.6;
    text-align: center;
}
/* The card kept: a gold ring on the card and a coin mark drawn by the slot. */
.dlm-scoredslot .card { box-shadow: 0 0 0 3px #c99a1e, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
.dlm-scoredslot::after {
    content: '';
    position: absolute;
    right: -5px;
    top: -5px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #e2b33a;
    border: 1.5px solid #7a5a00;
    pointer-events: none;
}
.dlm-total {
    display: block;
    width: 2.6em;
    text-align: center;
    font-weight: 700;
    font-size: 17px;
    line-height: 24px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* The tiers: 380, 600, and 900px of board (design, "Layout"). */
@container dlm (min-width: 380px) {
    #dlm-board { --dlm-trickcw: 56px; --dlm-smallcw: 26px; --dlm-oppgap: 2px; }
}
@container dlm (min-width: 600px) {
    #dlm-board { --dlm-trickcw: 66px; --dlm-smallcw: 30px; }
}
@container dlm (min-width: 900px) {
    #dlm-board { --dlm-trickcw: clamp(64px, min(8cqi, 10.5vh), 96px); --dlm-smallcw: 34px; }
    @supports (width: round(down, 1.5px, 1px)) {
        #dlm-board { --dlm-trickcw: round(down, clamp(64px, min(8cqi, 10.5vh), 96px), 1px); }
    }
    /* Equal side tracks, so the opponent's row and the action row sit on the
       board's axis. The left track is the name with its score pile under it
       (two reserved rows); the reveal note takes the right track. */
    .dlm-side { grid-template-columns: 220px minmax(0, 1fr) 220px; column-gap: 12px; }
    .dlm-side.dlm-far { grid-template-areas: "head hand note" "strip hand note"; }
    .dlm-side.dlm-near { grid-template-areas: "head acts ." "strip acts ."; }
    .dlm-side .dlm-head { min-height: 0; }
    .dlm-strip { height: calc(2 * var(--dlm-pileh) + 2px); }
    .dlm-note { text-align: left; }
    .dlm-side .dlm-actions { margin: 0; }
    /* The near band's pile is now its bottom edge, so the board keeps 6px
       under it before the hand panel; the gaps between rows give 2px each
       back, which keeps the 1280x720 fit. */
    #dlm-board { gap: 6px; padding-bottom: 6px; }
}
/* The bottom of the 900px tier (round 1 follow-up). 220px side tracks leave
   the middle track the board minus 464px, short of the 466px a 13-card
   opponent row needs (13 x 34 + 12 x 2, only while stuck at position 2)
   below a 930px board. 200px tracks leave 476px at 900. The pile then holds
   7 a row (7 x 24 + 6 x 2 = 180px), so its 2 reserved rows still hold 13,
   and the head (a name capped at 120px and its points) still fits. */
@container dlm (min-width: 900px) and (max-width: 929.98px) {
    .dlm-side { grid-template-columns: 200px minmax(0, 1fr) 200px; }
}

/* --- the action row (D6) ------------------------------------------------- */
/* One 44px button high whether or not a button is in it. It is the near
   band's last row, or, from a 900px board, the middle of that band. */
.dlm-actions {
    grid-area: acts;
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 6px;
    min-height: 44px;
    margin: 6px 0;
}
.dlm-actions .btn.dlm-suitbtn { min-height: 44px; padding: 0 12px; white-space: nowrap; }
.dlm-suitglyph { font-size: 1.15em; }
.dlm-result .result-actions .btn { min-height: 44px; }

/* --- the hand (U3, D7) --------------------------------------------------- */
/* Game-scoped hand size: the site's --cw would wrap 12 cards to two rows at
   1280. One row of 13 from a 1,237px window up. */
@media (min-width: 951px) {
    .dlm-handpanel #hand-cards .card { --cw: clamp(56px, min(7.5vw - 38px, 9vh), 88px); }
    @supports (width: round(down, 1.5px, 1px)) {
        .dlm-handpanel #hand-cards .card { --cw: round(down, clamp(56px, min(7.5vw - 38px, 9vh), 88px), 1px); }
    }
}
.dlm-handpanel #hand-cards .card.dlm-suitgap { margin-left: calc(var(--cw) * 0.18); }
/* U3: a card that cannot be picked now is faded with a filter, never opacity. */
.card.dlm-mute { filter: saturate(0.6) brightness(0.88); }
/* The eye badge on a card the opponent knows: positioned by its card, and a
   hover and tap target of its own (the shared badge ignores the pointer). */
.card.dlm-badged { position: relative; }
.card.dlm-badged .dlm-eye { pointer-events: auto; cursor: help; }

/* --- the reveal ---------------------------------------------------------- */
/* The opponent's row turns face up in place, on the independent `scale`
   (never `transform`, which the pop-in and the lift write). */
@keyframes dlm-turn {
    0% { scale: 0.2 1; }
    100% { scale: 1 1; }
}
.dlm-opphand .card.dlm-turn { animation: dlm-turn 360ms ease-out; }
@media (prefers-reduced-motion: reduce) {
    .dlm-opphand .card.dlm-turn { animation: none; }
}

/* --- the explanation balloon -------------------------------------------- */
/* A manual popover in the top layer, placed with fixed coordinates by
   board.ts: the UA's inset and margin are reset. */
.dlm-explain {
    inset: auto;
    margin: 0;
    width: max-content;
    max-width: min(260px, calc(100vw - 24px));
    padding: 10px 12px;
    background: var(--balloon);
    border: 1px solid var(--balloon-edge);
    border-radius: 10px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--balloon-ink);
    text-align: left;
}
.dlm-explain .dlm-x-head { margin: 0 0 4px; font-weight: 700; }
.dlm-explain .dlm-x-line { margin: 0 0 3px; }
.dlm-explain .dlm-x-line:last-child { margin-bottom: 0; }

/* --- the deck viewer (round 1, G3) --------------------------------------- */
/* A manual popover in the top layer, placed with fixed coordinates by
   board.ts beside the stock, so opening it moves nothing. Two sections, each
   one row per suit in the hand's suit order, the cards in the hand's rank
   order. Every card is 24px and simplified. A row wraps inside the viewer on
   the narrowest phones, which moves nothing outside it. */
.dlm-deck {
    inset: auto;
    margin: 0;
    width: max-content;
    max-width: min(392px, calc(100vw - 16px));
    max-height: calc(100vh - 16px);
    overflow-y: auto;
    box-sizing: border-box;
    padding: 10px 12px 12px;
    background: var(--felt-dark);
    color: var(--ink);
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
    font-size: 13px;
    line-height: 1.35;
    text-align: left;
}
/* Above the lines: a `.dlm-dline` (opacity under 1) paints in the
   positioned layer, and the top line's padding sits under the button. */
.dlm-deck .dlm-dhead { position: absolute; top: 2px; right: 2px; z-index: 1; }
.dlm-deck .dlm-dclose {
    appearance: none;
    -webkit-appearance: none;
    min-width: 40px;
    min-height: 40px;
    padding: 0;
    border: 0;
    border-radius: 8px;
    background: none;
    color: inherit;
    font: inherit;
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
}
.dlm-deck .dlm-dclose:hover { background: rgba(255, 255, 255, 0.08); }
.dlm-deck .dlm-dclose:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.dlm-deck .dlm-dsec + .dlm-dsec { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--panel-border); }
.dlm-deck .dlm-dtitle { font-weight: 700; margin: 0 0 4px; }
.dlm-deck .dlm-dplayed .dlm-dtitle { padding-right: 40px; }
.dlm-deck .dlm-dline { margin: 0 0 5px; opacity: 0.9; }
/* The stock's count and draw order, the viewer's top line, clear of the
   close button (round 1 follow-up). */
.dlm-deck .dlm-dstock { padding-right: 40px; margin-bottom: 8px; }
.dlm-deck .dlm-dkey { margin: 5px 0 0; }
.dlm-deck .dlm-drows { display: flex; flex-direction: column; gap: 3px; }
.dlm-deck .dlm-drow {
    display: grid;
    grid-template-columns: 16px minmax(0, 1fr);
    column-gap: 6px;
    align-items: center;
    min-height: calc(24px * 89 / 64);
}
.dlm-deck .dlm-dsuit { font-size: 15px; text-align: center; }
.dlm-deck .dlm-dcards { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; }
.dlm-deck .card.dlm-deckcard { --cw: 24px; }
.dlm-deck .card.dlm-certain { box-shadow: 0 0 0 2px var(--accent); }
.dlm-deck .dlm-dnone { font-size: 12px; opacity: 0.7; }

/* --- the summaries (D8) -------------------------------------------------- */
/* Real tables with the game's own class: each column sized once for every
   row, names left, numbers right, tabular figures (ui-conventions). */
.dlm-sumtitle { margin: 6px 0 4px; text-align: center; }
.dlm-sumline { margin: 0 0 8px; text-align: center; }
.dlm-sum, .dlm-deals {
    border-collapse: collapse;
    margin: 0 auto;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
}
.dlm-sum th, .dlm-sum td, .dlm-deals th, .dlm-deals td {
    padding: 3px 8px;
    border-bottom: 1px solid var(--panel-border);
    vertical-align: middle;
}
.dlm-sum thead th, .dlm-deals thead th { font-size: 12px; opacity: 0.85; }
.dlm-sum .dlm-namecol, .dlm-deals .dlm-namecol { text-align: left; white-space: nowrap; }
.dlm-sum .dlm-cardscol, .dlm-sum .dlm-cardscell { text-align: left; }
.dlm-sum .dlm-num, .dlm-deals .dlm-num { text-align: right; white-space: nowrap; }
.dlm-deals .dlm-totalrow th, .dlm-deals .dlm-totalrow td { font-weight: 700; border-bottom: 0; }
.dlm-sumcards { display: flex; flex-wrap: wrap; gap: 2px; max-width: 220px; }
.dlm-sum .card.dlm-small { --cw: 24px; }
.dlm-logtable { margin: 4px 0; }

/* --- phones -------------------------------------------------------------- */
@media (max-width: 560px) {
    .status.dlm-status { min-height: 3.9em; }
    .dlm-name { max-width: 90px; }
}
/* ------------------------------------------------------------------------- */
/* Gin-Go                                                                    */
/* ------------------------------------------------------------------------- */
/*
 * Prefix policy: every class is `gng-` and every id `#gng-` or `#opt-gng-`.
 * Shared ids are reached only through the game's own classes
 * (`.gng-handpanel #hand-cards`, `#status-line.gng-status`,
 * `#progress-line.gng-progress`, `#result-panel.gng-result`), and no
 * `container-type` sits on a shared id (the container query is on
 * `.gng-table`, the game's own box).
 *
 * LAYOUT (design "Layout and fit"): `.gng-layout` is a grid with the board
 * in the left column and the rummy table in the right one at every width.
 * The board's cells follow two custom properties on `.gng-board`,
 * `--gng-ch` (cell height) and `--gng-cw` (cell width), on the SMALL
 * viewport height (`svh`), so a phone's URL bar never resizes the board
 * under a finger. The levers if 1280 x 720 does not fit: `--gng-meldcw` at
 * 951 px and up, then the hand's `--cw`, then `--gng-ch`'s minimum.
 *
 * THE CELL AREA HAS NO GAP (C2): the four cell tracks and the thirteen cell
 * rows sit edge to edge, each chip inset and centered inside its cell, and
 * the label tracks are spaced by their own padding. So the link overlay's
 * 4 x 13 box is exactly the cell area and every cell center is at (i + 0.5).
 *
 * Fades are filters, never opacity (Req 23.7): `gng-dim` and `gng-faded`
 * stay above about 0.85 legibility.
 *
 * INK (build review 7): the page is light ink on the dark felt, so every rule
 * here that paints a LIGHT surface (a seat strip, a table meld, a suit disc)
 * sets its own dark `color`, and text on the felt uses light tints. Suit
 * glyphs are colored wherever they appear, because a suit picks a column:
 * on the felt with the site's suit inks (--red, --red-diamond, --blue-club,
 * the log's), on a light surface with their darker relatives (Dilemma's and
 * Agony Aunt's cream inks, the card sprites' hues). The probe's style pass
 * reads every painted text against its painted background.
 */

/* Suit glyphs on the felt (`#log-entries .red` and its companions already cover the log). */
#status-line.gng-status .red, #progress-line.gng-progress .red, .gng-setup .red, .gng-actions .red,
.gng-explain .red, .gng-key .red, .gng-turnupcap .red, #result-panel.gng-result .red { color: var(--red); }
#status-line.gng-status .red.diamond, #progress-line.gng-progress .red.diamond, .gng-setup .red.diamond,
.gng-actions .red.diamond, .gng-explain .red.diamond, .gng-key .red.diamond, .gng-turnupcap .red.diamond,
#result-panel.gng-result .red.diamond { color: var(--red-diamond); }
#status-line.gng-status .club, #progress-line.gng-progress .club, .gng-setup .club, .gng-actions .club,
.gng-explain .club, .gng-key .club, .gng-turnupcap .club, #result-panel.gng-result .club { color: var(--blue-club); }
/* On a light surface: the cream inks (4.8:1 or better on #f1ece0). */
.gng-seat, .gng-meld { --gng-ink-red: #b3202a; --gng-ink-diamond: #b13f12; --gng-ink-club: #1d4f91; }
.gng-seat .red, .gng-meld .red { color: var(--gng-ink-red); }
.gng-seat .red.diamond, .gng-meld .red.diamond { color: var(--gng-ink-diamond); }
.gng-seat .club, .gng-meld .club { color: var(--gng-ink-club); }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .gng-seat, .deck-fourcolor .gng-meld {
    --gng-ink-diamond: var(--fc-diamond-ink);
    --gng-ink-club: var(--fc-club-ink);
}

.gng-layout {
    display: grid;
    grid-template-areas: "board table" "board actions" "board hand";
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto auto 1fr;
    gap: 10px 14px;
    align-items: start;
}
.gng-layout > * { align-self: start; min-width: 0; }
.gng-board { grid-area: board; }
.gng-table { grid-area: table; container-type: inline-size; display: flex; flex-direction: column; gap: 8px; }
.gng-actions { grid-area: actions; }
.gng-layout > .gng-handpanel { grid-area: hand; }

/* The board ---------------------------------------------------------------- */

.gng-board {
    --gng-ch: clamp(26px, calc((100svh - 290px) / 16), 46px);
    --gng-cw: calc(var(--gng-ch) * 1.3);
    --gng-c0: #d9a520;          /* side 0: a filled gold disc */
    --gng-c1: #4a6890;          /* side 1: a filled slate-blue diamond */
    padding: 8px;
    text-align: center;
}
/*
 * The key never sets the board's width (width 0 with a 100% floor: it takes
 * the grid's width and adds none of its own), so a long side name ("Bot West
 * and Bot East", an online team) cannot push the table past the window; its
 * entries wrap instead. Each entry keeps its Gin badge's slot while unlinked
 * (`gng-off`, hidden), so the key's shape is fixed for the whole game and a
 * Gin coming or going moves nothing.
 */
.gng-key {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 2px 14px;
    width: 0;
    min-width: 100%;
    min-height: 26px;
    margin-bottom: 6px;
    font-size: 0.9em;
}
.gng-keyitem { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; cursor: default; }
.gng-keylabel { min-width: 0; text-align: left; overflow-wrap: anywhere; }
.gng-gin.gng-off { visibility: hidden; }
.gng-keyitem .gng-chip { position: static; width: 16px; height: 16px; transform: none; }
.gng-gin {
    font-weight: 700;
    padding: 0 6px;
    border-radius: 8px;
    color: #fff;
    background: #2b2b2b;
}
.gng-gin.gng-s0 { box-shadow: 0 0 0 2px var(--gng-c0); }
.gng-gin.gng-s1 { box-shadow: 0 0 0 2px var(--gng-c1); }

.gng-grid {
    display: grid;
    grid-template-columns: auto repeat(4, var(--gng-cw)) auto;
    grid-template-rows: auto auto repeat(13, var(--gng-ch)) auto auto;
    gap: 0;
    justify-content: center;
    position: relative;
}
/* A long side name wraps inside the four columns (its height is fixed for the game). */
.gng-rowcap { font-size: 0.72em; line-height: 14px; color: #dde5dc; overflow-wrap: anywhere; }
/* Each side's captions and rank labels in a light tint of its chip color, which reads on the felt (4.7:1 or better). */
.gng-rowcap.gng-s0, .gng-lab.gng-s0 { color: #ffd77a; }
.gng-rowcap.gng-s1, .gng-lab.gng-s1 { color: #c8dcf7; }
.gng-suitrow {
    display: grid;
    grid-template-columns: repeat(4, var(--gng-cw));
    height: var(--gng-ch);
    align-items: center;
    justify-items: center;
    cursor: default;
}
.gng-suit {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: calc(var(--gng-ch) * 0.8);
    height: calc(var(--gng-ch) * 0.8);
    border-radius: 50%;
    font-size: calc(var(--gng-ch) * 0.55);
    background: #fff;
    color: #1d1d1d;
}
.gng-suit.gng-s0 { box-shadow: inset 0 0 0 2px var(--gng-c0); }
.gng-suit.gng-s1 { box-shadow: inset 0 0 0 2px var(--gng-c1); }
/* The white discs take the cream inks, the same hues as the card sprites and the text glyphs. */
.gng-suit.red { color: #b3202a; }
.gng-suit.red.diamond { color: #b13f12; }
.gng-suit.club { color: #1d4f91; }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .gng-suit.red.diamond { color: var(--fc-diamond-ink); }
.deck-fourcolor .gng-suit.club { color: var(--fc-club-ink); }
.gng-suit.gng-blank { background: #eee; }
.gng-lab {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 6px;
    font-size: 0.78em;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.gng-lab.gng-far { transform: rotate(180deg); }
.gng-cell {
    position: relative;
    box-sizing: border-box;
    background: #f6f1e4;
    border: 1px solid #d3c9b2;
    cursor: default;
}
.gng-cell:focus-visible { outline: 2px solid #222; outline-offset: -2px; }
/* Sized by the cell's height on a square box (final review: 62% of each side made side 0's disc an oval in a wider-than-tall cell, unlike its key). */
.gng-chip {
    position: absolute;
    left: 50%;
    top: 50%;
    width: auto;
    height: 62%;
    aspect-ratio: 1 / 1;
    transform: translate(-50%, -50%);
    box-sizing: border-box;
    pointer-events: none;
}
.gng-chip.gng-s0 { border-radius: 50%; background: var(--gng-c0, #d9a520); box-shadow: 0 0 0 1px #6b5000; }
.gng-chip.gng-s1 {
    background: var(--gng-c1, #4a6890);
    transform: translate(-50%, -50%) rotate(45deg) scale(0.82);
    box-shadow: 0 0 0 1px #1c2d44, inset 0 0 0 4px var(--gng-c1, #4a6890), inset 0 0 0 7px #dfe7f1;
}
/* The key's chips are static flex items: the side-1 diamond keeps its turn but not the cell's centering translate. */
.gng-keyitem .gng-chip.gng-s1 { transform: rotate(45deg) scale(0.82); }
.gng-chip-in { animation: gng-chip-in 360ms ease-out both; }
.gng-chip-out { animation: gng-chip-out 420ms ease-in forwards; }
@keyframes gng-chip-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes gng-chip-out { from { opacity: 1; } to { opacity: 0; } }
body.motion-off .gng-chip-in, body.motion-off .gng-chip-out { animation: none; }
body.motion-off .gng-chip-out { display: none; }

/*
 * The previews (D3): a solid ring in the viewed side's color where a card
 * would go for you, a dashed hollow ring in the other side's color where the
 * same card would go for them. Each carries a dark casing, because the gold
 * on the cream cell is too faint alone: layered inset shadows for the solid
 * ring, a drop-shadow that follows the dashes for the dashed one (so its gaps
 * stay open and it still reads as dashed). The grid carries the viewed side
 * (`gng-view0` or `gng-view1`).
 */
.gng-grid.gng-view0 { --gng-near: var(--gng-c0); --gng-far: var(--gng-c1); }
.gng-grid.gng-view1 { --gng-near: var(--gng-c1); --gng-far: var(--gng-c0); }
.gng-cell.gng-prev-near {
    box-shadow: inset 0 0 0 1px #1d1d1d, inset 0 0 0 4px var(--gng-near, #1d1d1d), inset 0 0 0 5px #1d1d1d;
}
.gng-board .gng-cell.gng-prev-far::after {
    content: '';
    position: absolute;
    inset: 3px;
    border: 3px dashed var(--gng-far, #6a6a6a);
    border-radius: 3px;
    filter: drop-shadow(0 0 0.7px #1d1d1d) drop-shadow(0 0 0.7px #1d1d1d);
    pointer-events: none;
}

.gng-link {
    position: relative;
    width: 100%;
    height: 100%;
    pointer-events: none;
    z-index: 2;
    overflow: visible;
}
.gng-linkcase { fill: none; stroke: #1b1b1b; stroke-width: 7px; vector-effect: non-scaling-stroke; stroke-linejoin: round; stroke-linecap: round; }
.gng-linkline { fill: none; stroke-width: 3.5px; vector-effect: non-scaling-stroke; stroke-linejoin: round; stroke-linecap: round; }
.gng-linkline.gng-s0 { stroke: #f2c94c; }
.gng-linkline.gng-s1 { stroke: #9cc0ec; }

/* The table: the other hands, the melds, the piles, and the explanation ---- */

.gng-seats { display: flex; justify-content: center; gap: 8px; min-height: 58px; }
.gng-seats4 { min-height: 58px; }
.gng-seat {
    --gng-seatcw: 22px;
    flex: 1 1 0;
    min-width: 0;
    padding: 4px 6px;
    border-radius: 6px;
    background: #f3f3f3;
    color: #1d1d1d;
}
.gng-seat.gng-acting { box-shadow: 0 0 0 2px #1d1d1d; }
.gng-seathead { display: flex; justify-content: center; gap: 6px; white-space: nowrap; font-size: 0.85em; }
/* The name and its "partner" label: one unit whose name gives way first (an online name too long for the strip). */
.gng-seatid { display: flex; gap: 6px; min-width: 0; max-width: 100%; }
.gng-seatname { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.gng-role { flex: none; }
/*
 * THE HEAD ON TWO LINES (final review): the name and its "partner" label on
 * the first, the count on the second. A one-line head that is too narrow for
 * "Bot North partner Captured 15" (201 px of text) let the bold count crush
 * the name to nothing once a hand was over. Two lines are used wherever a
 * strip is narrower than that: every phone strip (below), and the three
 * 4-seat strips while the table is under 658 px wide (each strip under 202 px
 * of content; 768 px windows). The choice depends on the strip's width only,
 * never on what the head says, so the head's shape is fixed for the game and
 * the end of a hand moves nothing. Only a name longer than the strip allows
 * (an online one) is cut.
 */
@container (max-width: 657px) {
    .gng-seats4 .gng-seathead { flex-direction: column; align-items: center; gap: 0; line-height: 1.15; }
}
.gng-role { font-size: 0.85em; color: #555; font-style: italic; }
.gng-count { font-weight: 700; font-size: 1.1em; }
.gng-seatcards {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    container-type: inline-size;
    min-height: calc(var(--gng-seatcw) * 1.39 + 2px);
    scrollbar-width: none;
}
.gng-seatcards::-webkit-scrollbar { display: none; }
.gng-seatcards .card { --cw: var(--gng-seatcw); flex: 0 0 auto; position: relative; }
.gng-seatcards .gng-known + .gng-known { margin-left: calc(14px - var(--gng-seatcw)); }
.gng-seatcards .gng-back:not(:first-child) {
    margin-left: calc(clamp(3px,
        (100cqw - var(--gng-seatcw) - var(--gng-nk) * 14px) / max(1, var(--gng-nb)),
        14px) - var(--gng-seatcw));
}

.gng-melds {
    --gng-meldcw: 30px;
    display: grid;
    grid-template-columns: 1fr;
    gap: 6px;
    height: calc(4 * (var(--gng-meldcw) * 1.39 + 22px) + 40px);
    overflow: hidden;
    padding: 6px;
}
@container (min-width: 460px) {
    .gng-melds { grid-template-columns: 1fr 1fr; height: calc(2 * (var(--gng-meldcw) * 1.39 + 22px) + 30px); }
}
.gng-meldstrip { min-width: 0; display: flex; flex-direction: column; min-height: 0; }
/*
 * Never shrinks: a crowded list below it scrolls instead of squeezing the
 * heading (390 px, build iteration 8). A heading too long for its strip
 * ("Bot West and Bot East's melds" at 390 px, an online team) wraps instead of
 * losing "melds" to an ellipsis (final review); the names never change during
 * a game, so its height is fixed for the game, as the suit-row captions' are.
 */
.gng-striphead { flex: 0 0 auto; font-weight: 700; font-size: 0.85em; text-align: center; white-space: normal; overflow-wrap: anywhere; text-wrap: balance; }
.gng-meldlist {
    display: flex;
    flex-wrap: wrap;
    align-content: flex-start;
    justify-content: flex-start;
    gap: 6px;
    overflow-y: auto;
    scrollbar-gutter: stable;
    min-height: 0;
    flex: 1 1 auto;
}
.gng-meld {
    position: relative;
    display: flex;
    flex-direction: column;
    padding: 2px 3px 3px;
    border-radius: 5px;
    background: #f1ece0;
    color: #1d1d1d;
}
/* A lit target's ring is a light mint that reads on the felt (4:1 or better), apart from the accent of a selection and the gold of a hint. */
.gng-meld.gng-target { box-shadow: 0 0 0 3px var(--gng-lit, #7fe0a0); cursor: pointer; }
.gng-meldcards { display: flex; }
.gng-mcard { display: flex; flex-direction: column; align-items: center; }
.gng-mcard { position: relative; }   /* one paint layer in meld order, filters or not (as the pile) */
.gng-mcard .card { --cw: var(--gng-meldcw); }
.gng-mcard + .gng-mcard { margin-left: calc(14px - var(--gng-meldcw) * 0.5); }
.gng-jokeras { font-size: 0.7em; white-space: nowrap; line-height: 1.2; }
.gng-maker { font-size: 0.68em; color: #555; white-space: nowrap; text-align: left; }
.gng-turnup { display: flex; flex-wrap: wrap; gap: 3px; justify-content: flex-start; }
.gng-turnup .card { --cw: 26px; }
/* The first card of each suit is ringed in light ink, which reads on the felt. */
.gng-turnup .gng-firstsuit { box-shadow: 0 0 0 2px #f2efe9; }
/* Hand 1's turn-up under the viewed side's meld strip says what it is, so it never reads as a meld. */
.gng-turnupblock { display: flex; flex-direction: column; gap: 3px; }
.gng-turnupcap { font-size: 0.78em; line-height: 1.25; text-align: left; }
.gng-empty { color: #d4ddd5; font-size: 0.85em; padding: 4px; }

/*
 * The setup view (D7) takes the whole reserved box: one column whatever the
 * meld layout, so the turn-up and the calls never squeeze into one meld
 * strip's half. On a wide box the turn-up and its lines share a row and the
 * three calls sit in one row under them; on a narrow one everything stacks.
 * The box keeps its reserved height; it scrolls inside only if a rare long
 * turn-up wraps past it (the probe's setup pass checks every call and suit
 * button lies inside it and inside the window).
 */
.gng-melds.gng-setupmode { grid-template-columns: minmax(0, 1fr); overflow-y: auto; align-content: start; }
.gng-setup { display: flex; flex-direction: column; gap: 4px; align-items: stretch; min-width: 0; }
.gng-setuphead { display: flex; flex-direction: column; gap: 3px; align-items: center; }
.gng-setup .gng-turnup { justify-content: center; }
.gng-setup .gng-turnup .card { --cw: 22px; }
.gng-setuplines { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.gng-setupline { font-size: 0.8em; line-height: 1.25; text-align: center; }
.gng-callrow { display: grid; grid-template-columns: minmax(0, 1fr); gap: 5px; }
.gng-suitpick { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
.gng-callbtn { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 44px; padding: 3px 8px; line-height: 1.2; }
.gng-callname { font-weight: 700; font-size: 0.9em; }
.gng-callcap { font-size: 0.72em; white-space: normal; }
@container (min-width: 460px) {
    .gng-setuphead { flex-direction: row; justify-content: center; gap: 12px; }
    .gng-setuplines .gng-setupline { text-align: left; }
    .gng-callrow { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.gng-suitbtn { min-width: 52px; min-height: 52px; font-size: 1.7em; }
/* The suit buttons are .btn on the felt: the felt inks. */
.gng-suitbtn.red { color: var(--red); }
.gng-suitbtn.red.diamond { color: var(--red-diamond); }
.gng-suitbtn.club { color: var(--blue-club); }

/* The draw pile before the first deal: an empty slot the size of the pile's card. */
.gng-drawslot {
    width: var(--gng-pilecw);
    height: calc(var(--gng-pilecw) * 1.39);
    box-sizing: border-box;
    border: 2px dashed rgba(242, 239, 233, 0.45);
    border-radius: 4px;
}

.gng-piles { --gng-pilecw: 40px; display: flex; flex-direction: column; gap: 3px; }
.gng-pilehead { font-size: 0.85em; font-weight: 700; text-align: center; white-space: nowrap; }
.gng-pilerow { display: flex; gap: 10px; align-items: center; }
.gng-drawpile {
    position: relative;
    flex: 0 0 auto;
    min-width: 44px;
    min-height: 44px;
    padding: 2px;
    border: 0;
    background: none;
}
.gng-drawpile .card { --cw: var(--gng-pilecw); }
.gng-drawpile.gng-armed { box-shadow: 0 0 0 3px var(--gng-lit, #7fe0a0); border-radius: 6px; cursor: pointer; }
.gng-stockcount {
    position: absolute;
    right: -4px;
    bottom: -4px;
    min-width: 22px;
    padding: 0 4px;
    border-radius: 10px;
    background: #1d1d1d;
    color: #fff;
    font-size: 0.8em;
    font-variant-numeric: tabular-nums;
}
.gng-pilestrip {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: scroll;
    min-width: 0;
    flex: 1 1 auto;
    padding: 2px 2px 2px;
    min-height: calc(var(--gng-pilecw) * 1.39 + 12px);
}
.gng-pilestrip::-webkit-scrollbar { height: 8px; }
.gng-pilestrip::-webkit-scrollbar-thumb { background: #aaa; border-radius: 4px; }
@supports not selector(::-webkit-scrollbar) { .gng-pilestrip { scrollbar-width: thin; } }
/*
 * Every pile card is positioned, so all of them paint in one layer in pile
 * order. A faded card's `filter` makes it a stacking context, which otherwise
 * paints it ABOVE its unfiltered neighbors: at 1280 px a lit card between two
 * faded ones was covered from both sides and could not be pressed (the
 * probe's games pass found it).
 */
.gng-pilestrip .card { --cw: var(--gng-pilecw); flex: 0 0 auto; position: relative; }
.gng-pilestrip .card + .card { margin-left: calc(18px - var(--gng-pilecw)); }
.gng-pilestrip .gng-takeable { box-shadow: 0 0 0 2px var(--gng-lit, #7fe0a0); cursor: pointer; }
.gng-pilestrip.gng-droptarget { box-shadow: inset 0 0 0 2px var(--gng-lit, #7fe0a0); border-radius: 6px; cursor: pointer; }

.gng-explain {
    height: 2.6em;
    overflow: hidden;
    font-size: 0.88em;
    line-height: 1.3em;
    text-align: left;
    white-space: normal;
    overflow-wrap: anywhere;
}
.gng-ex-short { display: none; }
.gng-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* The controls row: a scroller that never centers, and a row that centers while it fits. */
.gng-actions {
    display: flex;
    justify-content: flex-start;
    overflow-x: auto;
    flex-wrap: nowrap;
    min-height: 48px;
    scrollbar-width: none;
}
.gng-actions::-webkit-scrollbar { display: none; }
.gng-actions-row { display: flex; gap: 8px; width: max-content; margin-inline: auto; align-items: center; }
.gng-actions-row .btn { min-height: 44px; white-space: nowrap; }

/* The hand ------------------------------------------------------------------- */

.gng-handcard.sel { transform: translateY(-8px); }
.gng-handcard.gng-needed { box-shadow: 0 0 0 3px #b0331f; }
.gng-handcard.gng-new::after {
    content: '';
    position: absolute;
    top: 3px;
    right: 3px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #1f6fc4;
}
/*
 * Scoped under the hand (ui-conventions.md "A sideways card turns with
 * rotate"): a flight ghost is a clone appended to <body>, and an unscoped
 * `.card.gng-handcard { position: relative }` out-specified `.card-fly`'s
 * `position: fixed`, so every hand card's ghost flew in the page's flow below
 * the fold and the card itself sat hidden for the flight (build review 7's
 * blank Joker in the designation shots).
 */
#hand-cards .card.gng-handcard { position: relative; }
.gng-hint { box-shadow: 0 0 0 3px #e0a800; }

/* Fades: filters, never opacity, never below about 0.85 legibility (Req 23.7). */
.gng-dim { filter: brightness(0.92) saturate(0.85); }
.gng-faded { filter: brightness(0.86) saturate(0.6); }

/* The status and progress lines reserve their tallest shape. */
/* The line height is set, so 2.6em (3.9em on a phone) is exactly two (three) lines (longStatus()). */
#status-line.gng-status { min-height: 2.6em; line-height: 1.3; overflow-wrap: anywhere; }
#progress-line.gng-progress { min-height: 1.3em; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* The summaries. */
#result-panel.gng-result .gng-sumline { text-align: left; margin: 2px 0; }
/*
 * THE ACTIONS ROW IS ALWAYS IN VIEW (final review): lifted over the board,
 * the summary is its own scroll box (max-height min(80vh, 680px)), and a
 * final summary lists one line per hand under the mini board, so after about
 * eight hands at 390 px Review game and Share game sat below the dialog's
 * fold. The row sticks to the bottom of the box and the hand lines scroll
 * under it: Expeditions' recipe (expeditions/style.css), as Counterbluff and
 * Draft Cribbage use it. Scoped to `.result-overlay`, so the inline summary
 * (review, tutorial) is untouched. The negative offsets are the panel's own
 * padding (14px below, 16px at the sides), so nothing scrolls by below or
 * beside the bar; its background is the dialog's color as it APPEARS, the
 * panel fill under the shell's dimming backdrop. z-index 3 is above the mini
 * board's link line (`.gng-link`, 2).
 */
.panel.result-overlay.gng-result .result-actions {
    position: sticky;
    bottom: -14px;
    z-index: 3;
    margin: 6px -16px 0;
    padding: 10px 16px 14px;
    background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)),
        linear-gradient(var(--panel), var(--panel)) var(--felt-dark);
    box-shadow: 0 -8px 12px -6px rgba(0, 0, 0, 0.5);
}
.gng-handlist { margin-top: 6px; }
.gng-miniboard {
    display: grid;
    grid-template-columns: repeat(4, 12px);
    grid-template-rows: repeat(13, 10px);
    gap: 0;
    justify-content: center;
    margin: 8px auto;
    width: max-content;
}
.gng-minicell { position: relative; background: #f6f1e4; border: 1px solid #e0d8c5; box-sizing: border-box; }
.gng-miniboard .gng-link { grid-row: 1 / 14; grid-column: 1 / 5; }

/* Bands ----------------------------------------------------------------------- */

@media (min-width: 951px) {
    /*
     * The fit at 1280 x 720 (Req 21.5), by the design's lever order: the meld
     * cards first (30 px from 800 px of height up, 24 px at 720), then the
     * hand's card (its width curve, capped by the height: unchanged at 800,
     * 51 px at 720). Both follow the small viewport height, as the board does.
     */
    .gng-handpanel #hand-cards .card { --cw: clamp(48px, min(calc(2.4vw + 26px), calc((100svh - 400px) * 0.16)), 80px); }
    .gng-melds { --gng-meldcw: clamp(24px, calc((100svh - 400px) * 0.075), 30px); }
}
@media (max-width: 950px) {
    .gng-layout {
        grid-template-areas: "board table" "actions actions" "hand hand";
        grid-template-rows: auto auto auto;
    }
    .gng-board { --gng-ch: clamp(24px, calc((100svh - 400px) / 16), 40px); }
    .gng-melds { --gng-meldcw: 34px; }
}
@media (max-width: 560px) {
    .gng-board {
        --gng-ch: clamp(22px, min(calc((100svh - 470px) / 16), calc((100vw - 230px) / 5)), 30px);
        --gng-cw: calc(var(--gng-ch) * 1.12);
        padding: 4px;
    }
    .gng-melds { --gng-meldcw: 40px; }
    .gng-key { font-size: 0.8em; line-height: 1.25em; }
    .gng-explain { height: 3.9em; }
    .gng-ex-long { display: none; }
    .gng-ex-short { display: inline; }
    .gng-seat { display: flex; align-items: center; gap: 4px; }
    /*
     * The head on two lines (see "THE HEAD ON TWO LINES"), sized to its text and
     * set beside the cards: the strip takes what the head leaves and compresses
     * its backs into it, never wider. Two lines of 1.15 are no taller than the
     * cards, so the strip keeps its one-line height.
     */
    .gng-seathead, .gng-seats4 .gng-seathead {
        flex: 0 0 auto;
        flex-direction: column;
        align-items: flex-start;
        gap: 0;
        max-width: 70%;
        min-width: 0;
        overflow: hidden;
        line-height: 1.15;
    }
    .gng-seatid { gap: 4px; }
    .gng-seat > .gng-seatcards { flex: 1 1 0; min-width: 0; }
    /*
     * 4 seats: the three strips stack, and one head column (the widest head,
     * at most 70%) serves all three, so their cards line up. Each strip is a
     * subgrid of it; without subgrid a strip falls back to its head above its
     * cards.
     */
    .gng-seats4 { display: grid; grid-template-columns: fit-content(70%) minmax(0, 1fr); gap: 8px 4px; }
    .gng-seats4 .gng-seat { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; align-items: center; }
    .gng-seats4 .gng-seathead { max-width: none; }
    #status-line.gng-status { min-height: 3.9em; }
    /* A long online dealer name wraps the progress line here: two lines reserved (longStatus()). */
    #progress-line.gng-progress { white-space: normal; min-height: 2.6em; }
}
@media (pointer: coarse) {
    .gng-pilestrip .card + .card { margin-left: calc(28px - var(--gng-pilecw)); }
    .gng-actions-row .btn, .gng-drawpile { min-height: 44px; }
}
/* =========================================================================
   Foolish Faces (foolishfaces)
   A trick taker for 4 or 5 where every card goes face up or face down, the
   Ace, Jack, Queen, and King played face up have abilities instead of rank,
   and the two Joker holders are a secret team.
     the table as a table: the other seats' panels on their sides of the trick
       cross, the viewer's row South                (.ff-board, .ff-trick)
     a small panel per seat: total, this round's face-up cards, the Joker
       hats                                          (.ff-seat)
     each seat's team as this screen knows it: purple the Joker team, green
       the other team, gray not known               (.ff-team-*)
     the card winning the trick ringed, or the seat a King names outlined
       (.ff-winning, .ff-named)
     a covered card dimmed under a Queen chip       (.ff-covered, .ff-coverchip)
     your own face-down cards with a rank chip only you see  (.ff-private)
     ability chips on A, J, Q, K                     (.ff-fx, icons.ts)
     under the hand, the cards you have not seen and the cards you have
       taken, or every seat's pile laid out like the table  (.ff-pile)
   ANTI-JITTER (ui-conventions "Containers must not move"): every band above
   the hand keeps one height all round. The status reserves two lines, the
   progress line one band, every seat panel its rows (the turn marker and the
   King glyph are always rendered and only made visible), the trick cross is a
   fixed grid of fixed slots, the viewer's row keeps its
   line behind the gate (.ff-veiled hides content, never the box), and the
   action row keeps one button's height. No min-height on #hand-cards: the
   shell's handspace reserves the hand.
   Every rule naming a SHARED id is scoped through a class this game owns
   (.status.ff-status, .progress.ff-progress, .ff-handpanel #hand-cards,
   #result-panel.ff-summary, #log-entries .ff-logtrick). Nothing is rotated.
   ========================================================================= */
/* --- the status line ---------------------------------------------------- */
/* Two lines reserved: the action wanted, and a hint. Neither wraps, so the box
   never grows. */
.status.ff-status { font-size: 16px; line-height: 1.3; min-height: 2.6em; }
.status.ff-status .ff-statustext,
.status.ff-status .ff-hint {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.status.ff-status.ff-turn .ff-statustext { color: var(--accent); font-weight: 700; }
.status.ff-status .ff-hint { font-weight: 400; opacity: 0.85; }
/* --- the progress line: the round and the stock ------------------------- */
.progress.ff-progress {
    opacity: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    flex-wrap: nowrap;
    white-space: nowrap;
    /* 35px, not 34: the stock chip with its 22px cards is 34.6px tall, and the
       line must not grow by that fraction when the cards appear after the
       exchange. */
    min-height: 35px;
    margin-bottom: 6px;
}
.progress.ff-progress .ff-round {
    font-size: 13px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.6;
}
.progress.ff-progress .ff-stock {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    padding: 1px 8px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
}
.progress.ff-progress .ff-stocklabel { font-size: 12px; opacity: 0.8; margin-right: 3px; }
/* Its own size, not .card.mini (46px / 34px): the band is 35px tall. */
.progress.ff-progress .card { --cw: 22px; position: relative; }
/* Each team's face-up cards this round, once every team is known. Veiled, not
   removed, until then, so the line keeps its width. */
.progress.ff-progress .ff-teamscores { display: inline-flex; align-items: center; gap: 4px; }
.ff-teamchip {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    min-width: 26px;
    justify-content: center;
    padding: 1px 6px;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--ff-team) 80%, transparent);
    background: color-mix(in srgb, var(--ff-team) 14%, transparent);
    color: var(--ff-team);
    font-size: 13px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 18px;
}
.ff-teamchip .ff-hat { width: 15px; height: 15px; }
/* --- the board: the table as a table ------------------------------------ */
/* ONE GRID. #ff-table is display: contents, so the seat panels are cells of it.
   The viewer's row spans the bottom row at every width, so it never sizes a
   column. */
.ff-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas:
        ".    up    ."
        "left trick right"
        "down down  down";
    gap: 6px;
    justify-content: center;
    align-items: center;
    margin-bottom: 8px;
}
/* Five seats: the four other panels in one row above the cross. */
.ff-board.seats-5 {
    grid-template-columns: repeat(4, minmax(0, 176px));
    grid-template-areas:
        "left  upleft upright right"
        "trick trick  trick   trick"
        "down  down   down    down";
}
/* The top row follows the cross under it: the two middle seats at the top, West
   and East a little lower, as their cards sit lower in the cross. */
.ff-board.seats-5 .ff-seat { align-self: start; }
.ff-board.seats-5 .ff-seat.pos-left,
.ff-board.seats-5 .ff-seat.pos-right { margin-top: 26px; }
.ff-table { display: contents; }
.ff-board .ff-trick { grid-area: trick; justify-self: center; }
.ff-board .ff-seat.pos-left { grid-area: left; }
.ff-board .ff-seat.pos-up { grid-area: up; }
.ff-board .ff-seat.pos-right { grid-area: right; }
.ff-board .ff-seat.pos-upleft { grid-area: upleft; }
.ff-board .ff-seat.pos-upright { grid-area: upright; }
.ff-board .ff-mine { grid-area: down; justify-self: center; }
/* On a desktop the 4-seat side panels hug the cross. */
@media (min-width: 621px) {
    .ff-board:not(.seats-5) .ff-seat.pos-left { justify-self: end; }
    .ff-board:not(.seats-5) .ff-seat.pos-right { justify-self: start; }
    .ff-board:not(.seats-5) .ff-seat.pos-up { justify-self: center; }
}
/* --- team colors ------------------------------------------------------- */
/* One token per team, read by every rule that colors a team: the seat panels,
   the viewer's row, the team chips, the log, the summaries, and the pile view.
   Light tints, so text in them stays readable on the felt. */
.ff-team-joker { --ff-team: #cfaeff; }
.ff-team-other { --ff-team: #8fe6ad; }
.ff-team-unknown { --ff-team: #b7c0c8; }
/* --- a seat's panel ----------------------------------------------------- */
/* A FIXED structure for every seat: every part is always rendered, the turn
   marker and the King glyph only made visible, so nothing moves (plan B1).
   Head row: marker, name, glyph, total. Chips row: this round's count, the
   Joker stars. */
.ff-seat {
    box-sizing: border-box;
    width: 176px;
    max-width: 100%;
    min-width: 0;
    padding: 6px 8px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
    outline-offset: 1px;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    grid-template-areas:
        "turn  name  named total"
        "round round round jmark";
    column-gap: 3px;
    row-gap: 2px;
    align-items: center;
}
.ff-seat .ff-seat-turn { grid-area: turn; }
.ff-seat .ff-seat-name { grid-area: name; }
.ff-seat .ff-seat-namedmark { grid-area: named; }
.ff-seat .ff-seat-total { grid-area: total; }
.ff-seat .ff-seat-round { grid-area: round; }
.ff-seat .ff-jokermark { grid-area: jmark; justify-self: end; }
/* The team: the border and an inset line in its color, a faint tint inside.
   Gray (not known) is quieter than either team. Box-shadow, not a wider
   border, so the panel keeps its size. */
.ff-seat.ff-team-joker,
.ff-seat.ff-team-other {
    border-color: color-mix(in srgb, var(--ff-team) 85%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ff-team) 55%, transparent);
    background-image: linear-gradient(color-mix(in srgb, var(--ff-team) 9%, transparent),
        color-mix(in srgb, var(--ff-team) 9%, transparent));
}
.ff-seat.ff-team-unknown { border-color: color-mix(in srgb, var(--ff-team) 45%, transparent); }
/* Whose turn: the ring, and a shape too. */
.ff-seat.ff-active { outline-color: var(--accent); }
/* A King names this seat and it has not played: dashed, beside the glyph. */
.ff-seat.ff-named { outline-color: var(--accent); outline-style: dashed; }
.ff-seat-turn,
.ff-seat-namedmark { visibility: hidden; }
.ff-seat-turn.ff-on,
.ff-seat-namedmark.ff-on { visibility: visible; }
.ff-seat-turn { color: var(--accent); font-size: 10px; line-height: 1; }
.ff-seat-namedmark { color: var(--accent); line-height: 0; }
.ff-seat-namedmark .ff-icon { display: block; width: 11px; height: 11px; }
.ff-seat-name {
    min-width: 0;
    font-weight: 700;
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ff-seat-total {
    font-size: 16px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    text-align: right;
}
.ff-seat-round {
    appearance: none;
    background: none;
    border: 0;
    margin: 0;
    padding: 0;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
    min-width: 0;
    font-size: 12px;
    opacity: 0.85;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-decoration: underline dotted;
    text-underline-offset: 2px;
}
.ff-seat-round:hover,
.ff-seat-round.ff-open { opacity: 1; color: var(--accent); }
.ff-seat-round:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* One jester hat per distinct Joker publicly seen at this seat. Rendered empty
   when there is none, at the same height. */
.ff-jokermark {
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    gap: 1px;
    min-height: 16px;
    color: #cfaeff;
    white-space: nowrap;
    padding: 0 3px;
    border-radius: 999px;
    border: 1px solid transparent;
}
.ff-jokermark .ff-hat { display: block; width: 16px; height: 16px; }
/* The hat's bells, a lighter tone than the hat (icons.ts JESTER_HAT): near
   white on the felt, a pale purple where the hat is drawn on a white tile. */
.ff-hat .ff-bell { fill: #f6efff; }
.ff-jokertile .ff-hat .ff-bell,
.ff-private .ff-hat .ff-bell { fill: #c2a0f0; }
.ff-jokermark.ff-marked { background: rgba(207, 174, 255, 0.16); border-color: rgba(207, 174, 255, 0.65); }
/* A King's target: the panel is an armed control in the target stage. */
.ff-seat.ff-targetable { cursor: pointer; outline: 2px solid #ffd479; }
.ff-seat.ff-targetable:focus-visible { outline-width: 3px; }
/* --- the viewer's row --------------------------------------------------- */
/* Never hidden; behind the gate its content is veiled, so it keeps its line. */
.ff-mine {
    box-sizing: border-box;
    min-height: 24px;
    padding: 0 10px;
    border-radius: 999px;
    outline: 2px solid transparent;
    outline-offset: 1px;
}
.ff-mine.ff-team-joker,
.ff-mine.ff-team-other {
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ff-team) 80%, transparent);
    background: color-mix(in srgb, var(--ff-team) 9%, transparent);
}
.ff-mine.ff-team-unknown { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ff-team) 35%, transparent); }
.ff-mine.ff-active { outline-color: var(--accent); }
.ff-mine.ff-named { outline-color: var(--accent); outline-style: dashed; }
.ff-mine-line {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: 24px;
    line-height: 22px;
    white-space: nowrap;
}
.ff-mine-name { font-weight: 700; font-size: 13px; }
.ff-mine .ff-seat-total { font-size: 15px; }
.ff-veiled { visibility: hidden; }
/* --- the trick: a cross ------------------------------------------------- */
/* Half-card rows, so the side cards sit level with the gap between the top
   and bottom cards: two card heights tall, not three. Fixed slots, so the
   cross is the same size empty or full. */
.ff-trick {
    --ff-tcw: 56px;
    --ff-half: calc(var(--ff-tcw) * 89 / 128);
    display: grid;
    grid-template-columns: repeat(3, var(--ff-tcw));
    grid-template-rows: repeat(4, var(--ff-half));
    /* A little air between the cards: a row gap lifts the top card off the
       side ones and drops the bottom card below them. */
    gap: 3px 12px;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.ff-trick .ff-play {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 6px;
    outline: 2px solid transparent;
    outline-offset: 2px;
}
.ff-trick .ff-play.pos-up { grid-area: 1 / 2 / 3 / 3; }
.ff-trick .ff-play.pos-left { grid-area: 2 / 1 / 4 / 2; }
.ff-trick .ff-play.pos-right { grid-area: 2 / 3 / 4 / 4; }
.ff-trick .ff-play.pos-down { grid-area: 3 / 2 / 5 / 3; }
.ff-trick.seats-5 { grid-template-columns: repeat(4, var(--ff-tcw)); }
.ff-trick.seats-5 .ff-play.pos-upleft { grid-area: 1 / 2 / 3 / 3; }
.ff-trick.seats-5 .ff-play.pos-upright { grid-area: 1 / 3 / 3 / 4; }
.ff-trick.seats-5 .ff-play.pos-left { grid-area: 2 / 1 / 4 / 2; }
.ff-trick.seats-5 .ff-play.pos-right { grid-area: 2 / 4 / 4 / 5; }
.ff-trick.seats-5 .ff-play.pos-down { grid-area: 3 / 2 / 5 / 4; justify-self: center; }
#ff-trick .card { --cw: var(--ff-tcw); position: relative; }
/* A slot still owed a card, and the one being waited on. */
.ff-trick .ff-play .card.empty { border-style: dashed; opacity: 0.35; }
.ff-trick .ff-play.onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
/* A King names this seat and it has not played: its empty slot is outlined. */
.ff-trick .ff-play.ff-named { outline: 2px dashed var(--accent); }
/* A Queen's or a King's target in the target stage. */
.ff-trick .ff-play.ff-targetable { cursor: pointer; outline: 2px solid #ffd479; }
.ff-trick .ff-play.ff-targetable:focus-visible { outline-width: 3px; }
/* The card winning the trick, or that won the trick still on the table: a
   ring and a check, so it is not color alone. */
.ff-trick .card.ff-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
.ff-wincheck {
    position: absolute;
    right: -7px;
    bottom: -7px;
    z-index: 3;
    font-size: 13px;
    font-weight: 800;
    line-height: 1;
    color: var(--accent);
    pointer-events: none;
}
/* A covered card: dimmed, with the Queen's glyph over it, so coverage is a
   shape as well as a fade. */
.card.ff-covered { opacity: 0.45; }
.ff-coverchip {
    position: absolute;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    z-index: 2;
    line-height: 0;
    padding: 2px;
    border-radius: 5px;
    background: rgba(16, 22, 30, 0.86);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.35);
    pointer-events: none;
}
.ff-coverchip .ff-icon { display: block; width: 16px; height: 16px; }
/* Who a face-up King named, on the King, still there when a Queen covers it. */
.ff-kingtag {
    position: absolute;
    left: 50%;
    top: -9px;
    translate: -50% 0;
    z-index: 3;
    max-width: calc(var(--ff-tcw) + 16px);
    overflow: hidden;
    text-overflow: ellipsis;
    padding: 0 4px;
    border-radius: 999px;
    background: rgba(16, 22, 30, 0.9);
    border: 1px solid var(--accent);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    line-height: 14px;
    white-space: nowrap;
    pointer-events: none;
}
.ff-kingtag.ff-off { border-color: rgba(255, 255, 255, 0.35); color: rgba(255, 255, 255, 0.8); }
/* The last player's forced way up, in the slot the card will go to. */
.ff-trick .ff-play .card.ff-ghost { opacity: 0.6; }
.ff-trick .ff-play .card.ff-ghostup { border: 2px dashed var(--accent); opacity: 0.95; }
.ff-ghostlabel {
    position: absolute;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    padding: 1px 4px;
    border-radius: 4px;
    background: rgba(16, 22, 30, 0.88);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    line-height: 1.2;
    white-space: nowrap;
}
/* --- ability chips (icons.ts) ------------------------------------------- */
/* A dark chip in the bottom-right corner of an A, J, Q, or K. Positioned by the
   CONTAINER rules above and below (a flight ghost is a clone in <body>, outside
   all of them), never by a class on the card. */
.ff-fx {
    position: absolute;
    bottom: 3%;
    right: 4%;
    z-index: 1;
    line-height: 0;
    padding: 1px;
    border-radius: 5px;
    background: rgba(16, 22, 30, 0.86);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.35);
    pointer-events: none;
}
.ff-fx .ff-icon {
    display: block;
    width: clamp(9px, calc(var(--cw) * 0.27), 20px);
    height: clamp(9px, calc(var(--cw) * 0.27), 20px);
}
/* On the small public copies the chip tucks into the corner. */
.ff-stock .ff-fx,
.ff-tcross .ff-fx,
.ff-pile .ff-fx { bottom: 1px; right: 1px; padding: 0; border-radius: 3px; }
.ff-stock .ff-fx .ff-icon,
.ff-tcross .ff-fx .ff-icon { width: 8px; height: 8px; }
/* The docked coach panel (a tutorial on a phone) is fixed inside the sticky
   #log-panel, and a sticky box is a stacking context of its own, so the coach's
   z-index counts only inside the aside. A chip with a z-index anywhere outside
   it (an ability chip, the cover chip, the win check, the private rank) would
   paint over the coach's text. Isolating every container that holds a chip keeps
   each chip's z-index inside that container. `isolation` makes no containing
   block for fixed descendants, unlike `container-type` or a transform. */
.ff-board,
.ff-handpanel,
.ff-pile,
.progress.ff-progress { isolation: isolate; }
/* --- your own face-down cards ------------------------------------------- */
/* The rank only, no suit, on a light chip: only you ever see this. */
.ff-private {
    position: absolute;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    z-index: 2;
    background: #f2efe9;
    color: #1a1a1a;
    font-size: 13px;
    font-weight: 700;
    line-height: 1.2;
    padding: 1px 5px;
    border-radius: 6px;
    border: 1px solid rgba(0, 0, 0, 0.35);
    pointer-events: none;
    white-space: nowrap;
}
.ff-private.ff-privatejoker { color: #6b3fb0; line-height: 0; padding: 2px 3px; }
.ff-private.ff-privatejoker .ff-hat { display: block; width: 14px; height: 14px; }
.ff-pile .ff-private { font-size: 10px; padding: 0 3px; }
.ff-pile .ff-private.ff-privatejoker .ff-hat,
.ff-tcross .ff-private.ff-privatejoker .ff-hat { width: 9px; height: 9px; }
.ff-tcross .ff-private { font-size: 8px; padding: 0 2px; border-radius: 3px; }
/* --- the action row ----------------------------------------------------- */
/* Controls only, one button high whether or not there is one. */
.ff-actions {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 8px;
    flex-wrap: nowrap;
    /* Taller than a button (about 38.4px), so a button arriving never grows it. */
    min-height: 40px;
    margin: 0 0 4px;
}
/* --- the box under the hand: unseen cards and piles --------------------- */
/* The cards you have not seen: one tile per rank (the top half of its
   simplified card, which is the rank alone; the Joker's tile is the hat) over
   its count. Two groups, as in the hand; a narrow screen wraps between them. */
.ff-unseen { margin-bottom: 8px; }
.ff-unseen-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px 12px;
    min-height: 32px;
}
.ff-unseen-group { display: inline-flex; gap: 3px; }
.ff-unseen-rank { --ff-tilecw: 26px; display: inline-flex; flex-direction: column; align-items: center; gap: 2px; }
.ff-unseen-rank.ff-zero { opacity: 0.3; }
.ff-unseen-tile {
    position: relative;
    display: block;
    width: var(--ff-tilecw);
    height: calc(var(--ff-tilecw) * 89 / 64 * 0.52);
    overflow: hidden;
    border-radius: 3px;
    background: #fff;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
}
.ff-unseen-tile .card { --cw: var(--ff-tilecw); display: block; margin: 0; box-shadow: none; }
.ff-unseen-tile.ff-jokertile { display: flex; align-items: center; justify-content: center; color: #6b3fb0; }
.ff-unseen-tile.ff-jokertile .ff-hat { display: block; width: 72%; height: 86%; }
/* Under each tile, one box per card of the rank (4, or 2 for the Joker), as
   wide together as the tile: the unseen cards filled on the left, the seen
   ones faded on the right. All seen: the whole entry fades (.ff-zero). */
.ff-unseen-pips {
    display: flex;
    gap: 2px;
    width: var(--ff-tilecw, 26px);
    height: 7px;
}
.ff-pip { flex: 1 1 0; min-width: 0; border-radius: 2px; background: #f2efe9; }
.ff-pip.ff-seen { background: rgba(242, 239, 233, 0.22); }
.ff-unseen-rank .ff-unseen-tile + .ff-unseen-pips { --ff-tilecw: inherit; }
/* --- the hand ----------------------------------------------------------- */
/* The slight gap between 2..10 and the Joker, and J, Q, K, A: a margin on the
   first card of the second group, never a spacer element. */
.ff-handpanel #hand-cards .card { position: relative; }
.ff-handpanel #hand-cards .card.ff-groupstart { margin-left: calc(var(--cw) * 0.22); }
/* A card that cannot be played (or put back) now: dimmed, not removed. */
.ff-handpanel #hand-cards .card.ff-mute { opacity: 0.4; }
/* --- your pile ---------------------------------------------------------- */
.ff-pile .ff-pilerow {
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
    justify-content: center;
}
.ff-pile .card { --cw: 26px; position: relative; }
.ff-pile .ff-unseen-tile .card { --cw: var(--ff-tilecw); }
.ff-pile-empty { font-size: 13px; opacity: 0.7; }
.ff-pile .ff-piletitle { text-align: center; font-size: 13px; margin: 0 0 4px; }
/* Every seat's pile, laid out like the table: West left, North top, East
   right, you at the bottom (5 seats: the four others across the top). Each
   seat's box in its team's color; the seat whose chip opened it outlined. */
.ff-allhead { position: relative; min-height: 26px; }
.ff-allhead .ff-allback { position: absolute; right: 0; top: 0; }
.ff-allgrid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-areas: ". up ." "left . right" ". down .";
    gap: 6px;
}
.ff-allgrid.seats-5 {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    grid-template-areas: "upleft upleft upright upright" "left left right right" ". down down .";
}
.ff-allseat {
    min-width: 0;
    padding: 4px;
    border-radius: 8px;
    border: 1px solid color-mix(in srgb, var(--ff-team) 70%, transparent);
    background: color-mix(in srgb, var(--ff-team) 8%, transparent);
}
.ff-allseat.ff-team-unknown { border-color: color-mix(in srgb, var(--ff-team) 40%, transparent); background: none; }
.ff-allseat.ff-focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.ff-allseat.pos-up { grid-area: up; }
.ff-allseat.pos-left { grid-area: left; }
.ff-allseat.pos-right { grid-area: right; }
.ff-allseat.pos-down { grid-area: down; }
.ff-allseat.pos-upleft { grid-area: upleft; }
.ff-allseat.pos-upright { grid-area: upright; }
.ff-allname {
    display: flex;
    justify-content: space-between;
    gap: 4px;
    font-size: 12px;
    white-space: nowrap;
    margin-bottom: 3px;
}
.ff-allwho { font-weight: 700; color: var(--ff-team); overflow: hidden; text-overflow: ellipsis; }
.ff-allcount { opacity: 0.85; }
.ff-allseat .ff-pilerow { justify-content: flex-start; gap: 2px; }
.ff-allseat .card { --cw: 20px; }
.ff-allseat .ff-pile-empty { font-size: 11px; }
/* --- the log: a trick as a small cross ---------------------------------- */
#log-entries .ff-logtrick {
    display: flex;
    align-items: center;
    gap: 10px;
    /* Room on the left for the lead badge on a West card. */
    padding: 5px 0 3px 6px;
}
.ff-logtrick-text { min-width: 0; flex: 1 1 auto; font-size: 12px; }
.ff-logtrick-cap { opacity: 0.9; }
.ff-logtrick-why { opacity: 0.8; }
.ff-logtrick-why .ff-icon {
    display: inline-block;
    width: 12px;
    height: 12px;
    vertical-align: -2px;
    margin-right: 4px;
    color: var(--accent);
}
.ff-tcross {
    --tx-cw: 19px;
    flex: none;
    display: grid;
    grid-template-columns: repeat(3, var(--tx-cw));
    grid-template-rows: repeat(3, calc(var(--tx-cw) * 89 / 64));
    grid-template-areas: ". up ." "left . right" ". down .";
    align-items: center;
    justify-items: center;
    gap: 1px 4px;
}
.ff-tcross.seats-5 {
    grid-template-columns: repeat(4, var(--tx-cw));
    grid-template-areas: ". upleft upright ." "left . . right" ". down down .";
}
.ff-tcross .card { --cw: var(--tx-cw); position: relative; }
.ff-tcross-cell { position: relative; line-height: 0; }
.ff-tcross-cell.pos-up { grid-area: up; }
.ff-tcross-cell.pos-left { grid-area: left; }
.ff-tcross-cell.pos-right { grid-area: right; }
.ff-tcross-cell.pos-down { grid-area: down; }
.ff-tcross-cell.pos-upleft { grid-area: upleft; }
.ff-tcross-cell.pos-upright { grid-area: upright; }
.ff-tcross-led,
.ff-tcross-won {
    position: absolute;
    font-size: 10px;
    line-height: 1;
    pointer-events: none;
}
/* Who led: a badge on the card's top-left corner, inside the cross, so a lead
   from the left seat is not cut off at the panel's edge. */
.ff-tcross-led {
    top: -5px;
    left: -5px;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 13px;
    height: 13px;
    padding-left: 1px;
    box-sizing: border-box;
    border-radius: 50%;
    background: rgba(16, 22, 30, 0.92);
    border: 1px solid var(--accent);
    color: var(--accent);
    font-size: 8px;
}
.ff-tcross-won { bottom: -1px; right: -8px; color: var(--accent); }
/* The trick in play: a dashed edge, so it does not read as a finished trick. */
#log-entries .ff-logtrick.ff-livetrick {
    border-left: 2px dashed var(--accent);
    padding-left: 8px;
    background: rgba(255, 255, 255, 0.04);
    border-radius: 0 6px 6px 0;
}
.ff-logjoker { color: #cfaeff; }
/* The trick's winner, in its team's color once the team is known. */
#log-entries .ff-logtrick .ff-who.ff-team-joker,
#log-entries .ff-logtrick .ff-who.ff-team-other { color: var(--ff-team); font-weight: 700; }
.ff-logwin { color: var(--accent); font-weight: 700; }
/* --- the summaries ------------------------------------------------------ */
/* Real tables, so each column is sized once for every row and lines up
   (ui-conventions "Score tables line up by column"). */
#result-panel.ff-summary .ff-summaryround { text-align: center; opacity: 0.8; margin-bottom: 4px; }
.ff-result {
    border-collapse: collapse;
    margin: 6px auto 10px;
    font-size: 13px;
}
.ff-result th,
.ff-result td {
    padding: 3px 8px;
    border-bottom: 1px solid var(--panel-border);
    white-space: nowrap;
}
.ff-result th { font-size: 12px; opacity: 0.8; }
.ff-result .ff-l { text-align: left; }
.ff-result .ff-n { text-align: right; font-variant-numeric: tabular-nums; }
.ff-result tr.ff-team-joker .ff-l,
.ff-result tr.ff-team-other .ff-l { color: var(--ff-team); }
.ff-result .ff-hat { display: inline-block; width: 13px; height: 13px; vertical-align: -2px; margin-right: 3px; }
#log-entries .ff-result { margin: 4px 0 8px; font-size: 12px; }
/* On a phone the 5-seat players table and a solo row ("Solo: Bot Northeast",
   "3 x 3 = 9") ran past the lifted dialog at 360px: tighter cells. */
@media (max-width: 620px) {
    .ff-result { font-size: 12px; }
    .ff-result th { font-size: 11px; }
    .ff-result th,
    .ff-result td { padding: 3px 4px; }
}
/* --- the Card abilities list (board and rules page) --------------------- */
.ff-ref { list-style: none; padding-left: 0; }
.ff-ref li { margin-bottom: 6px; }
.ff-ref .ff-icon,
.rules.cheatsheet .ff-icon {
    display: inline-block;
    width: 1.15em;
    height: 1.15em;
    vertical-align: -0.2em;
    margin-right: 0.3em;
    color: var(--accent);
}
.ff-hatref { color: #cfaeff; }
.ff-hatref .ff-icon { color: #cfaeff; }
/* --- the trick examples (rules page and Rules summary) ------------------ */
/* A Why line spans the row under its trick. Its own width counts for nothing
   when the table sizes its columns (width 0, then the cell's full width), so
   the table stays as narrow as its cards and the line wraps inside it. */
.cheatsheet .ff-tricks .ff-why { display: block; width: 0; min-width: 100%; }
/* --- phones (620px and below) ------------------------------------------- */
/* Every reservation is re-derived here (design 6.13's budget): the three
   other seats in one row across the top at 4 seats, West to East, the four
   at 5 seats, the cross under them, the viewer's row under that. */
@media (max-width: 620px) {
    .status.ff-status { font-size: 14px; }
    .ff-board:not(.seats-5) {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        grid-template-areas:
            "left  up    right"
            "trick trick trick"
            "down  down  down";
        column-gap: 4px;
        align-items: start;
    }
    .ff-board .ff-seat { width: auto; justify-self: stretch; padding: 4px; }
    .ff-board .ff-seat-name,
    .ff-board .ff-seat-round { font-size: 11px; white-space: nowrap; }
    .ff-board .ff-seat-total { font-size: 12px; }
    .ff-board .ff-jokermark { font-size: 11px; padding: 0 2px; }
    /* Five seats: four panels share the row, so the name gets row 1 to
       itself and the marker, total, stars, and glyph share row 2 (plan B1). */
    .ff-board.seats-5 {
        grid-template-columns: repeat(4, minmax(0, 1fr));
        column-gap: 4px;
        align-items: start;
    }
    .ff-board.seats-5 .ff-seat.pos-left,
    .ff-board.seats-5 .ff-seat.pos-right { margin-top: 12px; }
    .ff-trick { gap: 2px 10px; padding: 7px 8px; }
    .ff-unseen-pips { height: 6px; }
    .ff-board.seats-5 .ff-seat {
        padding: 4px 3px;
        grid-template-columns: auto minmax(0, 1fr) auto auto;
        grid-template-areas:
            "name  name  name  name"
            "turn  total jmark named"
            "round round round round";
    }
    .ff-board.seats-5 .ff-seat .ff-seat-total { text-align: left; }
    .ff-board.seats-5 .ff-seat-name,
    .ff-board.seats-5 .ff-seat-round { font-size: 10px; }
    .ff-trick { --ff-tcw: 40px; }
    .ff-unseen-rank { --ff-tilecw: 24px; }
    .ff-unseen-group { gap: 2px; }
    .ff-allseat .card { --cw: 18px; }
    .ff-kingtag { font-size: 9px; line-height: 12px; top: -7px; }
    .ff-ghostlabel { font-size: 9px; padding: 1px 3px; }
    /* Thirteen cards in two rows of seven, the group gap included. */
    .ff-handpanel {
        --ff-handcw: min(46px, calc((100vw - 66px) / 7.22));
        padding: 6px 8px 8px;
    }
    .ff-handpanel #hand-cards { gap: 6px 4px; }
    .ff-handpanel #hand-cards .card { --cw: var(--ff-handcw); }
    .ff-handpanel .panel-head { min-height: 24px; }
    .ff-handpanel .panel-head h3 { margin-bottom: 4px; font-size: 14px; }
}
@supports (width: round(down, 1.5px, 1px)) {
    @media (max-width: 620px) {
        .ff-handpanel { --ff-handcw: min(46px, round(down, calc((100vw - 66px) / 7.22), 1px)); }
    }
}
@media (max-width: 389px) {
    .ff-trick { --ff-tcw: 38px; }
    .ff-unseen-rank { --ff-tilecw: 22px; }
}
/* =========================================================================
 * Vintry (vintry)
 *
 * Prefix policy: every game-specific class is `vt-`. Shared classes are reused
 * as they are: cards are `.card` (`.mini simple` in the other seats'
 * vineyards), hand cards select with `.card.sel` and arm with `.clickable`,
 * buttons are `.btn`, and the result panel reuses `.winner-banner`,
 * `.result-note`, and `.result-actions`.
 *
 * NOTHING MOVES. Every box that changes content keeps its shape: the action
 * slots are fixed, the offer box beside them has a fixed size, every vineyard
 * is 8 slots with room under each for a barrel or a worker's chip, each seat's
 * note line is one line, and the control row reserves one button row. Card
 * sizes are custom properties on the containers, so each reservation follows
 * the card it holds.
 *
 * COMPACT ON PURPOSE: at 1280 by 800 the whole hand is on screen with four
 * seats (tools/vintry-shot.mjs prints where the hand panel ends). The actions
 * and the offer box share one row, and panels inside the board drop the
 * shared panel's bottom margin, since the board's gap already spaces them.
 * ========================================================================= */

.vt-board {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-bottom: 10px;
    --vt-acw: 50px;    /* action cards */
    --vt-ocw: 52px;    /* draft cards */
    --vt-offerw: 330px;
    --vt-mcw: 48px;    /* the viewed seat's vineyard */
    --vt-scw: 32px;    /* every other seat's vineyard */
}
.vt-board .panel { margin-bottom: 0; }

/* Suit glyphs in the board's own text (prices, seat notes), as in the log. */
.vt-board .vt-suit.red { color: var(--red); }
.vt-board .vt-suit.red.diamond { color: var(--red-diamond); }
.vt-board .vt-suit.club { color: var(--blue-club); }

/* --- the top box: the action line and the offer box ------------------------ */

.vt-table {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 8px 24px;
    padding: 8px 12px;
}
.vt-actions {
    display: flex;
    justify-content: center;
    gap: 8px;
    flex-wrap: nowrap;
}
.vt-action {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    width: calc(var(--vt-acw) + 44px);
}
.vt-actioncard {
    position: relative;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    height: calc(var(--vt-acw) * 89 / 64 + 6px);
    width: 100%;
}
.vt-actioncard .card { --cw: var(--vt-acw); }
/* The King stack: three card edges peek out under the top King. */
.vt-kingstack > .card {
    box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.35), 4px 4px 0 rgba(242, 239, 233, 0.5),
        6px 6px 0 rgba(0, 0, 0, 0.3);
}
/* Market running: the four Kings fanned, top King on the left. */
.vt-kingfan .card.vt-king { --cw: calc(var(--vt-acw) * 0.62); margin-left: -12px; }
.vt-kingfan .card.vt-king:first-child { margin-left: 0; }
.vt-actionname {
    font-weight: 800;
    font-size: 13px;
    line-height: 16px;
    white-space: nowrap;
}
.vt-actionhint {
    font-size: 11px;
    line-height: 14px;
    opacity: 0.7;
    white-space: nowrap;
}
.vt-action.vt-down .vt-actionname,
.vt-action.vt-down .vt-actionhint { opacity: 0.4; }
.vt-action.vt-previewed .vt-actionname { color: var(--accent); }

/* The offer box: a label line over the draft cards, the barrel prices, or
   the two piles. Fixed in both directions, so the action line beside it
   never shifts when its content changes. */
.vt-offer {
    flex: 0 0 var(--vt-offerw);
    height: calc(var(--vt-ocw) * 89 / 64 + 26px);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
}
.vt-offerlabel { font-weight: 700; font-size: 13px; line-height: 16px; white-space: nowrap; }
.vt-offercards { display: flex; gap: 6px; justify-content: center; }
.vt-offercards .card { --cw: var(--vt-ocw); }
.vt-prices { display: flex; gap: 8px; }
.vt-price {
    font-weight: 800;
    font-size: 15px;
    padding: 3px 10px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    background: rgba(255, 255, 255, 0.06);
    white-space: nowrap;
}
.vt-piles { display: flex; gap: 18px; justify-content: center; }
.vt-pile { display: flex; align-items: center; gap: 7px; }
.vt-pile .card.vt-pilecard { --cw: 34px; }
.vt-pilecount { font-size: 12px; opacity: 0.8; white-space: nowrap; }
.vt-suit { font-weight: 800; }

/* --- the seats ------------------------------------------------------------ */

.vt-seats { display: flex; flex-direction: column; gap: 8px; }
.vt-others {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
    gap: 8px;
}
.vt-seat {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 5px 10px;
    border: 1px solid var(--panel-border);
}
.vt-seat.vt-active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.vt-seathead {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 10px;
    height: 18px;
    line-height: 18px;
}
.vt-seatname { font-weight: 800; white-space: nowrap; }
.vt-startmark {
    font-size: 10px;
    line-height: 14px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 0 6px;
    border-radius: 999px;
    border: 1px solid var(--accent);
    white-space: nowrap;
}
.vt-seatstat { font-size: 12px; opacity: 0.8; white-space: nowrap; }
.vt-seatscore { font-size: 13px; font-weight: 700; white-space: nowrap; }

/* The note line: this round's choices, then the seat's last move. The newest
   move at the table is in the accent color, and so is the forecast that
   replaces your note while you point at an action. */
.vt-seatnote {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    height: 18px;
    font-size: 12px;
    line-height: 18px;
    white-space: nowrap;
    overflow: hidden;
}
.vt-chosechip {
    flex: none;
    font-size: 11px;
    line-height: 15px;
    font-weight: 700;
    padding: 0 7px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.13);
}
.vt-chosechip.vt-newchip { background: var(--accent); color: #111; }
.vt-notetext { overflow: hidden; text-overflow: ellipsis; opacity: 0.85; }
.vt-seatnote.vt-newest .vt-notetext,
.vt-seatnote.vt-forecast .vt-notetext { color: var(--accent); opacity: 1; font-weight: 700; }

/* A vineyard: 8 slots, each a card with room under it for a barrel. */
.vt-vineyard {
    --vt-pcw: var(--vt-scw);
    display: flex;
    justify-content: center;
    gap: 3px;
    flex-wrap: nowrap;
}
.vt-seat.vt-mine .vt-vineyard { --vt-pcw: var(--vt-mcw); gap: 6px; }
.vt-plot {
    position: relative;
    width: var(--vt-pcw);
    height: calc(var(--vt-pcw) * 89 / 64 + var(--vt-pcw) * 0.42);
    flex: none;
}
.vt-plot > .card { --cw: var(--vt-pcw); position: relative; z-index: 1; }
.vt-emptyplot::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    width: var(--vt-pcw);
    height: calc(var(--vt-pcw) * 89 / 64);
    border: 1px dashed rgba(255, 255, 255, 0.22);
    border-radius: 6%;
    box-sizing: border-box;
}
/* The barrel: a back tucked under the bottom of its field. */
.vt-plot .card.vt-barrel {
    --cw: calc(var(--vt-pcw) * 0.8);
    position: absolute;
    left: calc(var(--vt-pcw) * 0.1);
    top: calc(var(--vt-pcw) * 0.75);
    z-index: 0;
}
/* A barrel this field would get, while you point at Barrel. */
.vt-plot .card.vt-barrel.vt-ghost {
    opacity: 0.5;
    outline: 2px dashed var(--accent);
    outline-offset: 1px;
}
.vt-sellchip {
    position: absolute;
    right: -4px;
    bottom: 0;
    z-index: 2;
    font-size: 11px;
    font-weight: 800;
    padding: 1px 5px;
    border-radius: 999px;
    background: var(--accent);
    color: #111;
    white-space: nowrap;
    pointer-events: none;
}
.vt-plot .card.vt-target { outline: 2px solid var(--accent); outline-offset: 1px; }

/* A worker's job, in the space under it where a field keeps its barrel. */
.vt-workerchip {
    position: absolute;
    left: 50%;
    top: calc(var(--vt-pcw) * 89 / 64 + 2px);
    translate: -50% 0;
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 0 4px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.14);
    font-size: clamp(10px, calc(var(--vt-pcw) * 0.25), 12px);
    line-height: 1.3;
    font-weight: 800;
    white-space: nowrap;
}
.vt-icon { display: block; width: 1.1em; height: 1.1em; flex: none; }

/* --- the controls and the hand ------------------------------------------- */

.vt-controls { min-height: 40px; justify-content: center; margin-bottom: 8px; }
.vt-handpanel { padding-top: 10px; padding-bottom: 10px; }
.vt-handpanel #hand-cards .vt-handcard { position: relative; flex: none; }
.vt-handcard .card.vt-paying { opacity: 0.55; transform: translateY(4px); }
.vt-handcard .card.vt-unaffordable { opacity: 0.4; }
.vt-costchip {
    position: absolute;
    top: 4px;
    right: 4px;
    z-index: 2;
    min-width: 20px;
    padding: 1px 5px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 800;
    text-align: center;
    background: #f2efe9;
    color: #111;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
    pointer-events: none;
    white-space: nowrap;
}
.vt-costchip.vt-toodear { background: rgba(242, 239, 233, 0.45); }
.vt-costchip.vt-paychip { background: #e0a43c; }
/* A worker in hand: its job's glyph in the bottom corner. */
.vt-workerbadge {
    position: absolute;
    left: 4px;
    bottom: 4px;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    border-radius: 999px;
    background: rgba(23, 73, 45, 0.9);
    color: #f2efe9;
    font-size: 12px;
    pointer-events: none;
}

/* --- the result ------------------------------------------------------------ */

.vt-result {
    border-collapse: collapse;
    margin: 0 auto 10px;
    font-variant-numeric: tabular-nums;
}
.vt-result th, .vt-result td { padding: 4px 10px; text-align: right; }
.vt-result th.vt-resultname, .vt-result tr:first-child th:first-child { text-align: left; }
.vt-result tr:first-child th { font-size: 12px; opacity: 0.8; }
.vt-result td.vt-total { font-weight: 800; }
.vt-result tr.vt-winner th, .vt-result tr.vt-winner td { color: var(--accent); }

/* --- phones ---------------------------------------------------------------- */

@media (max-width: 560px) {
    .vt-board {
        --vt-acw: 40px;
        --vt-ocw: 44px;
        --vt-offerw: 100%;
        --vt-mcw: 38px;
        --vt-scw: 30px;
    }
    .vt-table { padding: 8px 6px; }
    .vt-actions { gap: 4px; }
    .vt-action { width: calc(var(--vt-acw) + 24px); }
    .vt-actionhint {
        font-size: 10px;
        line-height: 12px;
        min-height: 24px;
        white-space: normal;
        text-align: center;
    }
    .vt-others { grid-template-columns: 1fr; }
    /* A turn's instruction runs to two lines here: hold both, so the board
       does not step down when it does. */
    .status.vt-status { min-height: 50px; }
    .vt-seathead { gap: 7px; }
    .vt-seat.vt-mine .vt-vineyard { gap: 4px; }
}
/* =========================================================================
   Fight the Landlord (doudizhu)
   A three-seat climbing game: one Landlord against two Farmers. Beat the play
   on the table with a higher play of the same kind and size, a bomb, or the
   rocket, or pass, and be first to shed every card. The board is built
   around what a player reads every turn:
     the table as a table: the viewed seat South, the next seat to play West
       (play goes clockwise), the other East, each a small fixed panel with
       its total, its role, its cards left as a count and a bar of backs, and
       its bid or Passed                                  (.ddz-seat, .pos-*)
     the play to beat at full size, the trick's earlier plays small
                                                          (.ddz-center)
     a finished trick held, ringed and starred, until the next lead
                                                          (.ddz-winning)
     beside the viewer's panel, the kitty with the deal and the stake
       (.ddz-kitty), and the cards still out (.ddz-unseen), a tile per rank,
       lowest on the left, as Foolish Faces shows its unseen cards
   Under the controls, the quick plays, one tap each (.ddz-quick), which also
   carry the hint's sentence. On the viewer's own turn, a card no play can use
   is faded (.ddz-dim).
   FITTING ONE SCREEN. At 1280 x 800 the board, the controls, and a two-row
   hand fit without scrolling: the deal number lives in the kitty box (the
   shared progress line folds away), the status line and the Play button
   reserve one line on a desktop, and the hand box follows the hand
   (reserveHandSpace: false), since nothing a player aims at sits below it.
   EVERY CARD SIZE is a custom property on .ddz-board, never a rule on .card
   or a shared id, so each reservation follows it.
   ANTI-JITTER (ui-conventions.md, "Containers must not move"). Every box above
   the hand is reserved at its tallest ordinary shape, and re-derived at the
   narrower breakpoints: the seat panels at one height in every state (the ▶,
   the role, and the bid or Passed are switched by .ddz-off, which keeps their
   space), the label at two lines, the play to beat at one row (a long play
   fans to fit), the earlier plays at one scrolling row, the controls at one
   row, the quick plays at one scrolling row. Nothing on the board is ever
   `hidden` in a game.
   SHARED IDS are reached only through a class this game owns: the hand panel
   through .ddz-handpanel, the status and progress lines through .ddz-status
   and .ddz-progress.
   ========================================================================= */

/* --- sizes ------------------------------------------------------------- */
.ddz-board {
    --ddz-playcw: 52px;
    --ddz-earliercw: 24px;
    --ddz-kittycw: 30px;
    --ddz-backcw: 12px;
    --ddz-backstep: 4px;
    --ddz-centerw: 360px;
    --ddz-seatw: 232px;
    --ddz-tilecw: 22px;
}

/* Switched on and off without moving anything: the space is kept. */
.ddz-off { visibility: hidden; }

/* --- the board: the table as a table ------------------------------------ */
.ddz-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas:
        "left  center right"
        "kitty down   unseen";
    align-items: center;
    gap: 10px 16px;
    margin-bottom: 6px;
}
.ddz-table { display: contents; }
.ddz-seat.pos-left { grid-area: left; justify-self: end; }
.ddz-seat.pos-right { grid-area: right; justify-self: start; }
.ddz-seat.pos-down { grid-area: down; justify-self: center; }
.ddz-center { grid-area: center; }
.ddz-kitty { grid-area: kitty; justify-self: end; }
.ddz-unseen { grid-area: unseen; justify-self: start; }

/* --- a seat's panel ------------------------------------------------------ */
.ddz-seat {
    box-sizing: border-box;
    width: var(--ddz-seatw);
    max-width: 100%;
    min-width: 0;
    padding: 6px 9px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.ddz-seat.ddz-active { outline-color: var(--accent); }
.ddz-seat-head {
    display: flex;
    align-items: baseline;
    gap: 5px;
    margin-bottom: 4px;
    white-space: nowrap;
}
.ddz-toact { flex: none; color: var(--accent); font-size: 11px; }
.ddz-seat-name {
    flex: 1 1 0;
    min-width: 0;
    font-weight: 700;
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ddz-seat-total {
    flex: none;
    font-size: 16px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.ddz-seat-line {
    display: flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
    min-height: calc(var(--ddz-backcw) * 89 / 64);
}
/* The role: the landlord's badge in the accent, filled, so it is not color alone. */
.ddz-role {
    flex: none;
    padding: 0 5px;
    border-radius: 6px;
    border: 1px solid var(--panel-border);
    font-size: 11px;
    font-weight: 700;
    line-height: 16px;
    white-space: nowrap;
}
.ddz-role.ddz-role-lord {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-ink);
}
.ddz-seat.ddz-lord { border-color: var(--accent); }
/* The hand as a bar of backs in a FIXED box sized for 20. */
.ddz-backs {
    flex: none;
    display: flex;
    align-items: center;
    width: calc(var(--ddz-backcw) + 19 * var(--ddz-backstep));
    height: calc(var(--ddz-backcw) * 89 / 64);
    overflow: hidden;
}
.ddz-backs .card.ddz-back {
    --cw: var(--ddz-backcw);
    margin-left: calc(var(--ddz-backstep) - var(--ddz-backcw));
    border-radius: 2px;
    box-shadow: -1px 0 0 rgba(0, 0, 0, 0.35);
}
.ddz-backs .card.ddz-back:first-child { margin-left: 0; }
.ddz-count {
    flex: none;
    min-width: 2ch;
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.ddz-said {
    flex: none;
    font-size: 11px;
    opacity: 0.85;
    font-style: italic;
    white-space: nowrap;
}

/* --- the play area ------------------------------------------------------- */
.ddz-center {
    box-sizing: border-box;
    width: var(--ddz-centerw);
    max-width: 100%;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
/* The play to beat in words, with who played it: TWO lines reserved. */
.ddz-label {
    height: 2.8em;
    line-height: 1.4;
    font-size: 14px;
    white-space: normal;
    text-align: center;
    overflow: hidden;
}
.ddz-label-who {
    display: inline-block;
    max-width: 10em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: bottom;
    font-weight: 700;
}
/* The play to beat, at full size: one reserved row; a long play fans. */
.ddz-top {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    min-height: calc(var(--ddz-playcw) * 89 / 64);
}
.ddz-top .ddz-group {
    position: relative;
    display: flex;
    flex-wrap: nowrap;
    gap: 6px;
    border-radius: 8px;
}
.ddz-top .card.ddz-topcard { --cw: var(--ddz-playcw); flex: none; }
.ddz-group.ddz-fan { gap: 0; }
.ddz-group.ddz-fan > .card + .card { margin-left: calc(-1 * var(--ddz-ovl, 0.4) * var(--cw)); }
.ddz-top .ddz-group.ddz-winning {
    outline: 3px solid var(--accent);
    outline-offset: 3px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
.ddz-star {
    position: absolute;
    top: -11px;
    right: -11px;
    width: 18px;
    height: 18px;
    line-height: 18px;
    border-radius: 50%;
    text-align: center;
    font-size: 12px;
    color: var(--accent-ink);
    background: var(--accent);
}
/* The trick's earlier plays, oldest left: ONE row that scrolls rather than
   wraps, NOT centered (a centered scrolling strip clips its left end). */
.ddz-earlier {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: center;
    gap: 8px;
    height: calc(var(--ddz-earliercw) * 89 / 64 + 8px);
    margin-top: 6px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}
.ddz-earlier .ddz-group { flex: none; display: flex; gap: 2px; }
.ddz-earlier .card.ddz-earliercard { --cw: var(--ddz-earliercw); flex: none; margin: 0; }

/* --- beside the viewer's panel: the kitty, the deal, the stake ---------- */
.ddz-kitty {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 8px;
    min-height: calc(var(--ddz-kittycw) * 89 / 64);
    white-space: nowrap;
}
.ddz-kittycards { display: flex; gap: 3px; flex: none; }
.ddz-kitty .card.ddz-kittycard { --cw: var(--ddz-kittycw); }
.ddz-kittyside { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.ddz-dealno { font-size: 12px; opacity: 0.8; }
.ddz-stake {
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

/* --- beside the viewer's panel: the cards still out --------------------- */
/* One tile per rank (the top half of its simplified card, the rank alone; a
   Joker's tile is a star in its color) over one box per card of the rank,
   the ones still out filled on the left. Two runs, 3 to 10 and J to the Red
   Joker, the 2 set apart: a wide box lays them on one line, a narrow one on
   two. All gone: the whole entry fades (.ddz-zero). */
.ddz-unseen-title {
    margin-bottom: 3px;
    font-size: 11px;
    opacity: 0.8;
    text-align: center;
    white-space: nowrap;
}
.ddz-unseen-rows {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px 6px;
}
.ddz-unseen-run { display: inline-flex; gap: 3px; }
.ddz-unseen-rank { display: inline-flex; flex-direction: column; align-items: center; gap: 2px; }
.ddz-unseen-rank.ddz-groupstart { margin-left: 6px; }
.ddz-unseen-rank.ddz-zero { opacity: 0.3; }
.ddz-tile {
    position: relative;
    display: block;
    width: var(--ddz-tilecw);
    height: calc(var(--ddz-tilecw) * 89 / 64 * 0.52);
    overflow: hidden;
    border-radius: 3px;
    background: #fff;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
}
.ddz-tile .card { --cw: var(--ddz-tilecw); display: block; margin: 0; box-shadow: none; }
.ddz-tile.ddz-jokertile {
    display: flex;
    align-items: center;
    justify-content: center;
    color: #1a1a1a;
    font-size: 12px;
    line-height: 1;
}
.ddz-tile.ddz-jokertile.ddz-red { color: #c62828; }
.ddz-pips { display: flex; gap: 2px; width: var(--ddz-tilecw); height: 6px; }
.ddz-pip { flex: 1 1 0; min-width: 0; border-radius: 2px; background: #f2efe9; }
.ddz-pip.ddz-seen,
.ddz-veiled .ddz-pip { background: rgba(242, 239, 233, 0.22); }

/* --- the action box ---------------------------------------------------- */
.ddz-actions { margin-bottom: 6px; }
/* One row, the Play button ONE line on a desktop: a long label ellipsizes,
   and its tooltip carries it in full. */
.ddz-controls {
    --ddz-playw: 22em;
    flex-wrap: nowrap;
    align-items: stretch;
    margin: 2px 0;
    min-height: 44px;
}
.ddz-controls .btn.ddz-play {
    flex: none;
    box-sizing: border-box;
    width: var(--ddz-playw);
    min-height: 44px;
    line-height: 1.25;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ddz-controls .btn.ddz-pass {
    flex: none;
    box-sizing: border-box;
    width: 6em;
    white-space: nowrap;
}
.ddz-controls .btn.ddz-bid {
    flex: none;
    box-sizing: border-box;
    width: 5.5em;
    white-space: nowrap;
}
.ddz-controls .btn.hinted,
.ddz-quick .btn.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
/* The quick plays: ONE row of one-tap buttons that scrolls rather than wraps,
   so the hand under it never moves. Not centered, because it scrolls. */
.ddz-quick {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: center;
    gap: 6px;
    width: max-content;
    max-width: 100%;
    /* The tallest chip is the coarse-pointer button minimum, 36px, plus the
       padding, so a chip arriving never grows the row (a fine pointer's chip
       is 25px; see the media query below). */
    min-height: 44px;
    margin: 0 auto 2px;
    padding: 2px 2px 4px;
    box-sizing: border-box;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}
.ddz-quick .btn.ddz-chip { flex: none; white-space: nowrap; }
.ddz-quick .btn.ddz-chip.ddz-chip-bomb { border-color: var(--accent); }
@media (pointer: fine) { .ddz-quick { min-height: 33px; } }
.ddz-quickhint {
    font-size: 13px;
    opacity: 0.75;
    white-space: nowrap;
}
/* The hint's sentence, ahead of the plays it may name. */
.ddz-hinttext {
    flex: none;
    font-size: 13px;
    font-weight: 700;
    color: var(--accent);
    white-space: nowrap;
}
/* "bomb" and "rocket" in prose: the named plays. */
.ddz-em { font-style: italic; }

/* --- the hand ---------------------------------------------------------- */
/* On the viewer's own turn: a card no allowed play uses (not a button), or,
   with a selection, one that cannot join it (still tappable). */
.ddz-handpanel .card.ddz-dim { opacity: 0.45; }
/* The cards a hovered quick play would use: a dashed ring, so not color alone. */
.ddz-handpanel .card.ddz-preview {
    outline: 2px dashed var(--accent);
    outline-offset: 2px;
}

/* --- status and progress ----------------------------------------------- */
/* One line on a desktop; every status the game writes fits one there. */
.status.ddz-status { line-height: 1.35; min-height: 1.35em; }
.status.ddz-status.ddz-turn { color: var(--accent); font-weight: 700; }
/* The deal number is in the kitty box. */
.progress.ddz-progress { display: none; }

/* --- the summary ------------------------------------------------------- */
.ddz-stakeline { margin: 0 0 8px; text-align: center; font-size: 14px; }
.ddz-result {
    border-collapse: collapse;
    margin: 0 auto 10px;
    font-size: 14px;
}
.ddz-result th,
.ddz-result td {
    padding: 4px 10px;
    border-bottom: 1px solid var(--panel-border);
    white-space: nowrap;
}
.ddz-result th { font-size: 12px; font-weight: 600; opacity: 0.85; }
.ddz-result .ddz-name { text-align: left; }
.ddz-result .ddz-num { text-align: right; font-variant-numeric: tabular-nums; }
.ddz-result tr.ddz-best td { color: var(--accent); font-weight: 700; }

/* --- narrower screens ------------------------------------------------- */
/* Below 1180px the side cells cannot hold a seat panel beside the play area,
   so the two opponents go in ONE ROW across the top, still West then East,
   the play area under them, and the kitty, the viewer's panel, and the cards
   still out (on two lines) in one row under that: six equal tracks, two to a
   box, so nothing adds a row of its own. */
@media (max-width: 1179px) {
    .ddz-board {
        grid-template-columns: repeat(6, minmax(0, 1fr));
        grid-template-areas:
            "left   left   left   right  right  right"
            "center center center center center center"
            "kitty  kitty  down   down   unseen unseen";
        gap: 8px 10px;
    }
    .ddz-seat.pos-left { justify-self: end; }
    .ddz-seat.pos-right { justify-self: start; }
    .ddz-seat.pos-down { justify-self: center; }
    .ddz-center { justify-self: center; }
    .ddz-kitty { justify-self: end; }
    .ddz-unseen { justify-self: start; }
    .ddz-unseen-rows { flex-direction: column; align-items: flex-start; }
}
/* Phones: the panels stretch to their cells, every reservation re-derived. */
@media (max-width: 720px) {
    .ddz-board {
        --ddz-centerw: 100%;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        grid-template-areas:
            "left   right"
            "center center"
            "kitty  down"
            "unseen unseen";
        gap: 8px 6px;
        align-items: start;
    }
    .ddz-unseen { justify-self: center; }
    .ddz-unseen-rows { flex-direction: row; align-items: center; }
    .ddz-seat.pos-left,
    .ddz-seat.pos-right,
    .ddz-seat.pos-down { width: auto; justify-self: stretch; padding: 5px 6px 6px; }
    .ddz-center { justify-self: stretch; }
    .ddz-kitty { align-self: center; }
    .ddz-seat-total { font-size: 15px; }
    /* The bid or Passed never shares the backs' row here: it sits on a second,
       reserved row of the line, so a panel is one height whatever it says. */
    .ddz-seat-line { flex-wrap: wrap; row-gap: 2px; }
    .ddz-seat-line .ddz-said { flex-basis: 100%; }
    /* Two lines for Play on a phone, which the longest label needs. */
    .ddz-controls { --ddz-playw: min(19em, 100% - 6em - 8px); min-height: calc(2 * 1.25em + 16px); }
    .ddz-controls .btn.ddz-play {
        min-height: calc(2 * 1.25em + 16px);
        white-space: normal;
        overflow-wrap: anywhere;
    }
    .ddz-controls .btn.ddz-bid { width: auto; flex: 1 1 0; }
    .status.ddz-status { min-height: 2.7em; }
}
@media (max-width: 400px) {
    .ddz-board {
        --ddz-playcw: 46px;
        --ddz-earliercw: 22px;
        --ddz-kittycw: 26px;
        --ddz-backcw: 8px;
        --ddz-backstep: 3px;
        --ddz-tilecw: 20px;
    }
}
/* =========================================================================
   Three Hags (threehags)
   A climbing game for two or three. Beat the combo on the pile with a higher
   one of the same kind (onesie, twosie, threesie) or pass, and be first to
   empty your hand. The board is built around what a player reads every turn:
     the table as a table: the viewed seat South; the opponent North at two
       players, the next seat West and the other East at three; each a small
       fixed panel with its total, its cards left, and Passed (.hag-seat, .pos-*)
     the stock at two players, the play to beat at full size, the pile's
       earlier plays small                                  (.hag-center)
     a pile that just ended held, ringed and starred, until the next lead
                                                            (.hag-winning)
   Under the controls, the melds: every combo of two or more cards the hand
   makes, one tap each on the player's turn (.hag-meldbox). On the viewer's own
   turn, a card no play can use is faded (.hag-dim).
   EVERY CARD SIZE is a custom property on .hag-board, never a rule on .card or
   a shared id, so each reservation follows it.
   ANTI-JITTER (ui-conventions.md, "Containers must not move"). Every box above
   the hand is reserved at its tallest ordinary shape, and re-derived at the
   phone breakpoint: the seat panels at one height in every state (the ▶ and
   Passed are switched by .hag-off, which keeps their space), the stock row at
   one card, the label at two lines, the play to beat at one row, the earlier
   plays at one scrolling row, the controls at one row, the note at two lines,
   the melds at two chip rows that scroll, the status line likewise. Nothing on the
   board is ever `hidden` in a game.
   SHARED IDS are reached only through a class this game owns: the hand panel
   through .hag-handpanel, the status and progress lines through .hag-status
   and .hag-progress.
   ========================================================================= */

/* --- sizes ------------------------------------------------------------- */
.hag-board {
    --hag-playcw: 56px;
    --hag-earliercw: 28px;
    --hag-stockcw: 28px;
    --hag-backcw: 12px;
    --hag-backstep: 4px;
    --hag-centerw: 340px;
    --hag-seatw: 232px;
}

/* Switched on and off without moving anything: the space is kept. */
.hag-off { visibility: hidden; }

/* --- the board: the table as a table ------------------------------------ */
.hag-board {
    display: grid;
    align-items: center;
    gap: 10px 16px;
    margin-bottom: 8px;
}
.hag-board.hag-n2 {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
        "up"
        "center"
        "down";
    justify-items: center;
}
.hag-board.hag-n3 {
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas:
        "left center right"
        ".    down   .";
}
.hag-table { display: contents; }
.hag-seat.pos-up { grid-area: up; justify-self: center; }
.hag-seat.pos-left { grid-area: left; justify-self: end; }
.hag-seat.pos-right { grid-area: right; justify-self: start; }
.hag-seat.pos-down { grid-area: down; justify-self: center; }
.hag-center { grid-area: center; justify-self: center; }

/* --- a seat's panel ------------------------------------------------------ */
.hag-seat {
    box-sizing: border-box;
    width: var(--hag-seatw);
    max-width: 100%;
    min-width: 0;
    padding: 6px 9px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.hag-seat.hag-active { outline-color: var(--accent); }
.hag-seat-head {
    display: flex;
    align-items: baseline;
    gap: 5px;
    margin-bottom: 4px;
    white-space: nowrap;
}
.hag-toact { flex: none; color: var(--accent); font-size: 11px; }
.hag-seat-name {
    flex: 1 1 0;
    min-width: 0;
    font-weight: 700;
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.hag-seat-total {
    flex: none;
    font-size: 16px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.hag-seat-line {
    display: flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
    min-height: calc(var(--hag-backcw) * 89 / 64);
}
/* The hand as a bar of backs in a FIXED box sized for 20; the count says the rest. */
.hag-backs {
    flex: none;
    display: flex;
    align-items: center;
    width: calc(var(--hag-backcw) + 19 * var(--hag-backstep));
    height: calc(var(--hag-backcw) * 89 / 64);
    overflow: hidden;
}
.hag-backs .card.hag-back {
    --cw: var(--hag-backcw);
    margin-left: calc(var(--hag-backstep) - var(--hag-backcw));
    border-radius: 2px;
    box-shadow: -1px 0 0 rgba(0, 0, 0, 0.35);
}
.hag-backs .card.hag-back:first-child { margin-left: 0; }
.hag-count {
    flex: none;
    min-width: 5.5ch;
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.hag-said {
    flex: none;
    font-size: 11px;
    opacity: 0.85;
    font-style: italic;
    white-space: nowrap;
}

/* --- the play area ------------------------------------------------------- */
.hag-center {
    box-sizing: border-box;
    width: var(--hag-centerw);
    max-width: 100%;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
/* The stock: one row, one card high (kept, invisible, at three players). */
.hag-stock {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 8px;
    min-height: calc(var(--hag-stockcw) * 89 / 64);
    white-space: nowrap;
}
.hag-stock .card.hag-stockcard { --cw: var(--hag-stockcw); flex: none; }
.hag-stock .card.hag-stockcard.hag-empty { opacity: 0.25; }
.hag-stockcount {
    flex: none;
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
/* The play to beat in words, with who played it: TWO lines reserved. */
.hag-label {
    height: 2.8em;
    margin-top: 4px;
    line-height: 1.4;
    font-size: 14px;
    white-space: normal;
    text-align: center;
    overflow: hidden;
}
.hag-label-who {
    display: inline-block;
    max-width: 10em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: bottom;
    font-weight: 700;
}
/* The play to beat, at full size: one reserved row (three cards at most). */
.hag-top {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    min-height: calc(var(--hag-playcw) * 89 / 64);
}
.hag-top .hag-group {
    position: relative;
    display: flex;
    flex-wrap: nowrap;
    gap: 6px;
    border-radius: 8px;
}
.hag-top .card.hag-topcard { --cw: var(--hag-playcw); flex: none; }
.hag-top .hag-group.hag-winning {
    outline: 3px solid var(--accent);
    outline-offset: 3px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
.hag-star {
    position: absolute;
    top: -11px;
    right: -11px;
    width: 18px;
    height: 18px;
    line-height: 18px;
    border-radius: 50%;
    text-align: center;
    font-size: 12px;
    color: var(--accent-ink);
    background: var(--accent);
}
/* The pile's earlier plays, oldest left: ONE row that scrolls rather than
   wraps, NOT centered (a centered scrolling strip clips its left end). */
.hag-earlier {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: center;
    gap: 8px;
    height: calc(var(--hag-earliercw) * 89 / 64 + 8px);
    margin-top: 6px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}
.hag-earlier .hag-group { flex: none; display: flex; }
.hag-earlier .hag-group.hag-small > .card + .card { margin-left: calc(-0.45 * var(--hag-earliercw)); }
.hag-earlier .card.hag-earliercard { --cw: var(--hag-earliercw); flex: none; }

/* --- the action box ---------------------------------------------------- */
.hag-actions { margin-bottom: 6px; }
.hag-controls {
    --hag-playw: 17em;
    flex-wrap: nowrap;
    align-items: stretch;
    margin: 4px 0;
    min-height: calc(1.25em + 16px);
}
.hag-controls .btn.hag-play {
    flex: none;
    box-sizing: border-box;
    width: var(--hag-playw);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.hag-controls .btn.hag-pass {
    flex: none;
    box-sizing: border-box;
    width: 6em;
    white-space: nowrap;
}
.hag-controls .btn.hinted,
.hag-melds .btn.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
.hag-note {
    min-height: 2.6em;
    line-height: 1.3;
    font-size: 14px;
    text-align: center;
}

/* --- the melds --------------------------------------------------------- */
/* A compact box: a small title and two rows of chips, the rest scrolling, so a
   hand that grows a meld never moves the hand below. A row is one chip high:
   --hag-chiph, the desktop .btn.small, and the coarse-pointer button minimum
   (36px) on a touch screen. */
.hag-meldbox {
    --hag-chiph: 25px;
    padding: 6px 10px 6px;
    margin-bottom: 8px;
}
.hag-meldbox h3.hag-meldtitle {
    margin: 0 0 4px;
    font-size: 13px;
    line-height: 16px;
}
.hag-melds {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-content: flex-start;
    gap: 4px 6px;
    height: calc(2 * var(--hag-chiph) + 4px);
    box-sizing: content-box;
    overflow-y: auto;
    scrollbar-width: thin;
}
@media (pointer: coarse) {
    .hag-meldbox { --hag-chiph: 36px; }
}
.hag-melds .btn.hag-chip {
    flex: none;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.hag-melds .btn.hag-chip.hag-chip-sweep { border-color: var(--accent); }
.hag-melds .btn.hag-chip.hag-chip-idle { opacity: 0.6; cursor: default; }
.hag-meldfam {
    flex: none;
    align-self: center;
    font-size: 12px;
    font-weight: 700;
    opacity: 0.8;
    white-space: nowrap;
}
.hag-meldhint {
    align-self: center;
    font-size: 13px;
    opacity: 0.75;
    white-space: nowrap;
}

/* Suit glyphs spliced into the game's own text (def.ts appendSuitColored),
   the colors the game log and the card labels use. */
.hag-melds .red,
.hag-label .red,
.hag-note .red,
.hag-controls .red,
.status.hag-status .red { color: var(--red); }
.hag-melds .club,
.hag-label .club,
.hag-note .club,
.hag-controls .club,
.status.hag-status .club { color: var(--blue-club); }
.hag-melds .red.diamond,
.hag-label .red.diamond,
.hag-note .red.diamond,
.hag-controls .red.diamond,
.status.hag-status .red.diamond { color: var(--red-diamond); }
/* On the primary button's light fill: a dark red, and clubs in the button's own ink. */
.hag-controls .btn.primary .red { color: #b3261e; }
.hag-controls .btn.primary .club { color: inherit; }
.hag-controls .btn.primary .red.diamond { color: #b4400c; }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .hag-controls .btn.primary .red.diamond { color: var(--fc-diamond-ink); }
.deck-fourcolor .hag-controls .btn.primary .club { color: var(--fc-club-ink); }

/* --- the hand ---------------------------------------------------------- */
/* On the viewer's own turn: a card no allowed play uses (not a button), or,
   with a selection, one that cannot join it (still tappable). */
.hag-handpanel .card.hag-dim { opacity: 0.4; }
/* A card drawn since the viewer's last play: a soft gold edge and a dot at its
   top, so not color alone (the tooltip says "Just drawn"). Neither touches the
   outline, which the selection and the hint use. */
.hag-handpanel .card.hag-new {
    position: relative;
    box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.75), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.hag-handpanel .card.hag-new::after {
    content: '';
    position: absolute;
    top: -5px;
    left: 50%;
    width: 9px;
    height: 9px;
    margin-left: -4.5px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.55);
    pointer-events: none;
}
/* The cards a hovered meld would use: a dashed ring, so not color alone. */
.hag-handpanel .card.hag-preview {
    outline: 2px dashed var(--accent);
    outline-offset: 2px;
}

/* --- status and progress ----------------------------------------------- */
.status.hag-status { line-height: 1.35; min-height: 2.7em; }
.status.hag-status.hag-turn { color: var(--accent); font-weight: 700; }
.progress.hag-progress { min-height: 1.4em; white-space: nowrap; }

/* --- the summary ------------------------------------------------------- */
.hag-pointsline { margin: 0 0 8px; text-align: center; font-size: 14px; }
.hag-result {
    border-collapse: collapse;
    margin: 0 auto 10px;
    font-size: 14px;
}
.hag-result th,
.hag-result td {
    padding: 4px 10px;
    border-bottom: 1px solid var(--panel-border);
    white-space: nowrap;
}
.hag-result th { font-size: 12px; font-weight: 600; opacity: 0.85; }
.hag-result .hag-name { text-align: left; }
.hag-result .hag-num { text-align: right; font-variant-numeric: tabular-nums; }
.hag-result tr.hag-best td { color: var(--accent); font-weight: 700; }

/* --- phones ------------------------------------------------------------ */
/* At three players the two opponents go in ONE ROW across the top, West then
   East, the center under them, the viewer's panel under that. Every
   reservation above is re-derived here. */
@media (max-width: 720px) {
    .hag-board { --hag-centerw: 100%; gap: 8px 6px; }
    .hag-board.hag-n3 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        grid-template-areas:
            "left   right"
            "center center"
            "down   down";
        align-items: start;
    }
    .hag-board.hag-n3 .hag-seat.pos-left,
    .hag-board.hag-n3 .hag-seat.pos-right { width: auto; justify-self: stretch; padding: 5px 6px 6px; }
    .hag-center { justify-self: stretch; }
    .hag-seat-total { font-size: 15px; }
    /* Passed never shares the backs' row here: it sits on a second, reserved
       row of the line, so a panel is one height whatever it says. */
    .hag-board.hag-n3 .hag-seat-line { flex-wrap: wrap; row-gap: 2px; }
    .hag-board.hag-n3 .hag-seat-line .hag-said { flex-basis: 100%; }
    .hag-controls { --hag-playw: min(17em, 100% - 6em - 8px); }
    .hag-note { min-height: 3.9em; }
    .status.hag-status { min-height: 4.05em; }
}
@media (max-width: 400px) {
    .hag-board {
        --hag-playcw: 50px;
        --hag-earliercw: 24px;
        --hag-stockcw: 24px;
        --hag-backcw: 8px;
        --hag-backstep: 3px;
    }
}
/* =========================================================================
   Feast Regale (src/feastregale/, pages/feastregale.mjs)
   The rules' own layout: five table cards in the middle row with a head seat
   at each end, five seats above, five below, as one 7 x 3 grid. Shared classes
   (.card, .card.simple, .panel, .btn, .logline, .winner-banner) are reused;
   everything game-owned is prefixed `frg-`.
   Every board card is on the simplified atlas (ui-conventions.md, "Dense
   grids of cards"). A seated guest's seat card shows as a tab on the seat's
   outer side, so nothing a Queen counts is hidden under the guest. Solo food
   is a plate of its suit, since no solo rule reads a food's rank.
   ========================================================================= */
.frg-board {
    --frg-cw: 64px;
    --frg-tab: 20px;
    display: grid;
    grid-template-columns: repeat(7, calc(var(--frg-cw) + 8px));
    grid-template-rows: auto auto auto;
    justify-content: center;
    align-items: center;
    column-gap: 4px;
    row-gap: 10px;
    padding: 12px 8px;
    margin: 0 auto 8px;
    width: fit-content;
    max-width: 100%;
    box-sizing: border-box;
}
.frg-seat, .frg-slot {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 3px;
    border: 2px solid transparent;
    border-radius: 10px;
    background: transparent;
    color: inherit;
    font: inherit;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
}
/* The table: a cloth behind the middle row's five cards. */
.frg-cloth {
    grid-column: 2 / 7;
    grid-row: 2;
    align-self: stretch;
    margin: -4px 0;
    border-radius: 12px;
    background: linear-gradient(180deg, #7a4a2a, #5e3720);
    box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.25), 0 2px 6px rgba(0, 0, 0, 0.35);
}
.frg-slot { z-index: 1; }
.frg-seat:hover, .frg-slot:hover { background: rgba(255, 255, 255, 0.06); }
.frg-cardslot { position: relative; }
.frg-cardslot > .card { --cw: var(--frg-cw); }
/* The seat card's tab: rank and suit, on the seat's outer side. Always in the
   cell, empty or not, so a guest arriving never moves the board. */
.frg-tab {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    height: var(--frg-tab);
    min-width: var(--frg-cw);
    box-sizing: border-box;
    white-space: nowrap;
}
.frg-tab.frg-tabon {
    border-radius: 6px;
    background: #f7f4ee;
    color: #1a1a1a;
    font-weight: 700;
    font-size: 13px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.frg-tab .frg-tabtext.red { color: #c0182a; }
.frg-tab .frg-tabtext.red.diamond { color: #c8541a; }
.frg-tab .frg-tabtext.club { color: #1f4f8f; }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .frg-tab .frg-tabtext.red.diamond { color: var(--fc-diamond-ink); }
.deck-fourcolor .frg-tab .frg-tabtext.club { color: var(--fc-club-ink); }
.frg-tab .frg-depth { position: static; }
/* How many cards deep a seat or a table card is, once it is more than one. */
.frg-depth {
    position: absolute;
    top: -6px;
    right: -6px;
    min-width: 16px;
    padding: 0 4px;
    border-radius: 8px;
    font-size: 11px;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
    color: #17324a;
    background: rgba(238, 243, 255, 0.95);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
/* A guest's badge strip, on the inner edge of its card (toward the table), so
   it never covers the seat tab: below the card in the top row and at the
   heads, above it in the bottom row. Half of it sits in the row gap. */
.frg-marks {
    position: absolute;
    left: -6px;
    right: -6px;
    bottom: -9px;
    display: flex;
    justify-content: center;
    gap: 2px;
    pointer-events: none;
}
.frg-bottom .frg-marks { bottom: auto; top: -9px; }
.frg-mood, .frg-pts, .frg-chip {
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    box-sizing: border-box;
    border-radius: 9px;
    font-size: 11.5px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
}
.frg-mood.frg-ok { background: #2e7d46; color: #fff; }
/* A progress chip: green when that demand is met, red when not. */
.frg-chip.frg-met { background: #2e7d46; color: #fff; }
.frg-chip.frg-unmet { background: #b3261e; color: #fff; }
/* Picky Eaters' chip is a little plate of the Jack's suit, like the table's. */
.frg-chip.frg-platechip { border-radius: 50%; padding: 0; width: 18px; box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.55), 0 1px 2px rgba(0, 0, 0, 0.45); }
.frg-pts { background: #eef3ff; color: #17324a; }
.frg-pts.frg-plus { background: #2e7d46; color: #fff; }
.frg-pts.frg-minus { background: #b3261e; color: #fff; }
/* A seat that is wrong for its guest: a King below a neighboring seat, a Jack
   above 5 (and High Kings' depth, Matriarchy's height). Only a move fixes the
   guest's own seat, so the whole seat box says so. */
.frg-seat.frg-wrongseat {
    border-color: #e0554a;
    background: rgba(179, 38, 30, 0.38);
}
.frg-seat.frg-wrongseat .frg-tab.frg-tabon { background: #ffd9d4; box-shadow: 0 0 0 2px #e0554a; }
/* While a guest is about to take a seat, each lit seat previews what it would
   give them: alone, a check on the seats that would satisfy them, which also
   take a green edge; with players, the points they would make there. A
   preview chip has a white dashed ring, so it never reads as a seated guest's. */
.frg-marks.frg-preview .frg-mood, .frg-marks.frg-preview .frg-pts {
    box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.9), 0 1px 2px rgba(0, 0, 0, 0.45);
    outline: 1.5px dashed rgba(255, 255, 255, 0.9);
    outline-offset: 1px;
}
.frg-seat.lit.frg-fits { border-color: #4cc06a; background: rgba(46, 125, 70, 0.32); }
/* Solo food: a plate of its suit, card-wide, centered in the card's height so
   the table row keeps the height the cards give it. */
.frg-plate {
    width: var(--frg-cw);
    height: calc(var(--frg-cw) * 89 / 64);
    display: flex;
    align-items: center;
    justify-content: center;
}
.frg-platesuit {
    width: calc(var(--frg-cw) * 0.92);
    height: calc(var(--frg-cw) * 0.92);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: radial-gradient(circle, #fbf9f4 0 54%, #e9e3d6 55% 60%, #fbf9f4 61%);
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.45);
    font-size: calc(var(--frg-cw) * 0.5);
    line-height: 1;
    color: #1a1a1a;
}
.frg-plate.red .frg-platesuit { color: #c0182a; }
.frg-plate.red.diamond .frg-platesuit { color: #c8541a; }
.frg-plate.club .frg-platesuit { color: #1f4f8f; }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .frg-plate.red.diamond .frg-platesuit { color: var(--fc-diamond-ink); }
.deck-fourcolor .frg-plate.club .frg-platesuit { color: var(--fc-club-ink); }
.frg-foodback { --cw: var(--frg-cw); opacity: 0.75; }
/* Targets and picks. Green: where the card or guest can go. Gold: the guest
   picked to move. Dashed blue: a guest the placed card lets move. */
.frg-seat.lit, .frg-slot.lit { border-color: var(--accent); background: rgba(255, 209, 102, 0.12); }
.frg-seat.frg-movable .frg-guest { outline: 2px dashed #7fb0ff; outline-offset: 2px; }
.frg-seat.sel .frg-guest { outline: 3px solid #d9b24a; outline-offset: 2px; }
.frg-seat.hinted, .frg-slot.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
/* With players, the viewer's own royals carry a gold edge. */
.frg-seat.frg-mine .frg-guest { box-shadow: 0 0 0 2px #d9b24a, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }

/* --- above the board: the queues, or the players ------------------------- */
.frg-topbox {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px 18px;
    margin: 0 auto 8px;
    min-height: 40px;
}
.frg-queues { --frg-qcw: 60px; --frg-lcw: 34px; }
.frg-queue { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.frg-qtitle { font-size: 13px; font-weight: 700; text-align: center; white-space: nowrap; }
.frg-qcards {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 4px;
    min-height: calc(var(--frg-qcw) * 89 / 64 + 6px);
    min-width: calc(var(--frg-qcw) + 6 * (var(--frg-lcw) + 4px));
}
.frg-qcards > .frg-front { --cw: var(--frg-qcw); }
.frg-qcards > .frg-later { --cw: var(--frg-lcw); opacity: 0.85; }
.frg-qempty { font-size: 13px; opacity: 0.7; margin: 0 auto; }
/* --- with players: the stage, the suit scores, and the player boxes ------
   The board sits in the middle column, the suit scores and the viewer's own
   box left of it, the other players right of it, so with players nothing is
   added above the board. The side columns grow down beside the board and are
   sized to stay within its height, so a box gaining or losing a row of cards
   moves nothing. A box's cards shrink with its width so that at least three
   fit a row: three boxes of six cards then stay within the board at the 84px
   the side columns narrow to, before the layout gives up on them. Below that
   (phones, and the 951px to 1090px band where the log column takes 300px) the
   boxes go above the board in ONE row, each hand a single line of cards that
   scrolls sideways, so the board and the hand move down as little as they can.
   The stage's container is named, because each box is a container too. */
.frg-stagewrap { container: frgstage / inline-size; }
.frg-stage {
    display: grid;
    grid-template-columns: minmax(84px, 1fr) auto minmax(84px, 1fr);
    column-gap: 10px;
    align-items: start;
}
.frg-stage.frg-solostage { grid-template-columns: auto; justify-content: center; }
.frg-side { min-width: 0; display: flex; flex-direction: column; gap: 8px; position: relative; }
.frg-west { align-items: flex-end; }
.frg-east { align-items: flex-start; }
.frg-side > * { width: 100%; max-width: 200px; box-sizing: border-box; }
.frg-meblock { display: flex; flex-direction: column; gap: 8px; }
.frg-pbox {
    container-type: inline-size;
    padding: 5px 7px 7px;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.2);
    border: 2px solid transparent;
}
.frg-pbox.frg-active { border-color: var(--accent); }
.frg-phead { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.frg-toact { color: var(--accent); font-size: 11px; }
.frg-toact.frg-off { visibility: hidden; }
.frg-pname { font-weight: 700; overflow: hidden; text-overflow: ellipsis; }
.frg-psuit { font-size: 17px; min-width: 14px; text-align: center; }
.frg-psuit.frg-hidden { opacity: 0.6; font-size: 14px; }
.frg-psuit.red { color: #ff7a7a; }
.frg-psuit.red.diamond { color: #ffa66b; }
.frg-psuit.club { color: #9cc4ff; }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .frg-psuit.red.diamond { color: var(--fc-diamond); }
.deck-fourcolor .frg-psuit.club { color: var(--fc-club); }
/* A narrow box (the side columns near their 84px) keeps "Bot 1" whole: the
   accent border already marks the player to act, so the arrow gives up its room. */
@container (max-width: 96px) {
    .frg-phead { gap: 3px; }
    .frg-pname { font-size: 13.5px; }
    .frg-toact.frg-off { display: none; }
}
/* A box's card width: 25px, or less when fewer than three would fit a row. */
.frg-phands {
    --frg-ocw: clamp(18px, calc((100cqw - 4px) / 3), 25px);
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-top: 4px;
}
/* A hand: one row of small cards, wrapping. The second hand, when a player
   holds two, sits below the first with a rule between them. */
.frg-phand {
    display: flex;
    flex-wrap: wrap;
    gap: 2px;
    min-height: calc(var(--frg-ocw) * 89 / 64);
}
.frg-phand > .card { --cw: var(--frg-ocw); }
.frg-phand.frg-pextra { padding-top: 4px; border-top: 1px dashed rgba(255, 255, 255, 0.3); }
.frg-phand.frg-pnone { align-items: center; font-size: 12.5px; opacity: 0.7; }
/* Two players: the opponent holds two hands every other turn, so its box
   keeps room for both and does not change height turn to turn. */
.frg-n2 .frg-east .frg-phands { min-height: calc(2 * var(--frg-ocw) * 89 / 64 * 2 + 14px); }
/* The suit scores: four chips, any of which opens its breakdown. */
.frg-scores { padding: 5px 7px 7px; border-radius: 10px; background: rgba(0, 0, 0, 0.2); }
.frg-scorestitle { font-size: 12.5px; font-weight: 700; text-align: center; margin-bottom: 4px; white-space: nowrap; }
.frg-suitchips { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; }
.frg-suitchip { font-variant-numeric: tabular-nums; font-weight: 700; white-space: nowrap; }
.frg-suitchip.red { color: #ff8f8f; }
.frg-suitchip.red.diamond { color: #ffb07a; }
.frg-suitchip.club { color: #a8ccff; }
/* Card colors: Four colors. Not on a selected chip, which keeps the accent ink. */
.deck-fourcolor .frg-suitchip.red.diamond:not(.sel) { color: var(--fc-diamond); }
.deck-fourcolor .frg-suitchip.club:not(.sel) { color: var(--fc-club); }
.frg-suitchip.frg-mysuit { box-shadow: inset 0 0 0 2px #d9b24a; }
.frg-suitchip.sel { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
/* A suit's breakdown floats over the board beside it, so opening it moves nothing. */
.frg-breakdown {
    position: absolute;
    z-index: 6;
    top: 100%;
    left: 0;
    margin-top: 4px;
    width: 230px;
    padding: 8px 10px 10px;
    border-radius: 10px;
    background: #17392a;
    border: 1px solid rgba(255, 255, 255, 0.22);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
    text-align: left;
}
.frg-scores { position: relative; }
.frg-bdhead { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.frg-bdtitle { font-weight: 700; white-space: nowrap; }
.frg-bdowner { font-size: 12.5px; opacity: 0.8; margin-top: 2px; }
.frg-bdroyals { display: flex; flex-wrap: wrap; gap: 4px 10px; margin: 6px 0; font-size: 13px; font-variant-numeric: tabular-nums; }
.frg-bdroyal { white-space: nowrap; }
.frg-bdsub { font-size: 12.5px; font-weight: 700; margin-top: 4px; }
.frg-bdtable { border-collapse: collapse; margin-top: 2px; font-variant-numeric: tabular-nums; }
.frg-bdtable td { padding: 1px 0; }
.frg-bdtable td.frg-name { text-align: left; padding-right: 14px; white-space: nowrap; }
.frg-bdtable td.frg-num { text-align: right; }
/* The viewer's second hand, below the hand they play from. */
.frg-nexthand { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 8px; }
.frg-nexttitle { font-size: 13px; font-weight: 700; white-space: nowrap; }
.frg-nextcards { display: flex; flex-wrap: wrap; gap: 3px; justify-content: center; }
@container frgstage (max-width: 740px) {
    .frg-stage.frg-multistage {
        display: flex;
        flex-wrap: wrap;
        align-items: stretch;
        gap: 6px;
    }
    /* The scores over your box, then the other players' boxes, join the row
       as items of their own, the previous player's last. */
    .frg-multistage .frg-east, .frg-multistage .frg-west { display: contents; }
    .frg-multistage .frg-meblock, .frg-multistage .frg-side > .frg-pbox { flex: 1 1 0; min-width: 104px; max-width: none; }
    .frg-multistage .frg-meblock { gap: 4px; }
    .frg-multistage .frg-west > .frg-pbox { order: 1; }
    .frg-multistage .frg-board { flex: 0 0 100%; order: 10; }
    .frg-multistage .frg-scorestitle { display: none; }
    .frg-multistage .frg-scores { padding: 4px 5px; }
    .frg-multistage .frg-pbox { padding: 4px 5px 5px; }
    /* One line of cards per hand: a scrolling strip starts at its left edge. */
    .frg-multistage .frg-phands { --frg-ocw: 25px; }
    .frg-multistage .frg-phand { flex-wrap: nowrap; overflow-x: auto; }
    .frg-n2 .frg-east .frg-phands { min-height: calc(2 * var(--frg-ocw) * 89 / 64 + 6px); }
    .frg-breakdown { width: min(230px, 92vw); }
}
/* Two players are dealt 12 cards, which wrap to a second row at laptop widths
   and push that row below the fold: the hand is sized to fit 12 on one row. */
.panel.frg-handpanel { container-type: inline-size; --frg-basecw: var(--cw); }
.panel.frg-handpanel.frg-hand12 #hand-cards { gap: 4px; }
.panel.frg-handpanel.frg-hand12 #hand-cards .card {
    --cw: max(36px, min(var(--frg-basecw), calc((100cqw - 11 * 4px - 24px) / 12)));
}

/* --- the action row ------------------------------------------------------ */
.frg-actions {
    display: flex;
    justify-content: center;
    gap: 8px;
    min-height: 40px;
    margin: 0 auto 6px;
}
.frg-actions .btn.frg-off { visibility: hidden; }
.frg-status.frg-turn { color: var(--accent); }

/* --- the setup screen ---------------------------------------------------- */
.segmented.frg-na { opacity: 0.55; }

/* --- the result ---------------------------------------------------------- */
.frg-unhappy {
    margin: 6px auto 10px;
    padding-left: 20px;
    max-width: 560px;
    text-align: left;
    font-size: 14px;
}
.frg-unhappy li { margin: 3px 0; }
.frg-result { border-collapse: collapse; margin: 6px auto 10px; font-variant-numeric: tabular-nums; }
.frg-result th, .frg-result td { padding: 4px 10px; }
.frg-result th.frg-name, .frg-result td.frg-name { text-align: left; }
.frg-result th.frg-num, .frg-result td.frg-num { text-align: right; }
.frg-result tr.frg-best td { font-weight: 700; color: var(--accent); }
.frg-result thead th { border-bottom: 1px solid rgba(255, 255, 255, 0.25); }

@media (max-width: 720px) {
    .frg-board { --frg-cw: 52px; --frg-tab: 18px; column-gap: 2px; row-gap: 8px; padding: 10px 4px; }
    .frg-tab.frg-tabon { font-size: 12px; }
}
@media (max-width: 480px) {
    .frg-board {
        --frg-cw: clamp(34px, calc((100vw - 84px) / 7), 46px);
        --frg-tab: 16px;
        column-gap: 1px;
        row-gap: 6px;
        padding: 8px 2px;
    }
    .frg-seat, .frg-slot { padding: 2px; border-width: 1px; }
    .frg-tab.frg-tabon { font-size: 11px; }
    .frg-mood, .frg-pts, .frg-chip { min-width: 15px; height: 15px; line-height: 15px; font-size: 9.5px; padding: 0 3px; }
    .frg-chip.frg-platechip { width: 15px; }
    .frg-marks { bottom: -8px; left: -4px; right: -4px; gap: 1px; }
    .frg-bottom .frg-marks { top: -8px; }
    .frg-queues { --frg-qcw: 46px; --frg-lcw: 26px; }
    /* Two lines, whoever is to play: "Your turn" wraps on a phone, and a
       one-line status would move the board and the hand by a line each turn. */
    .status.frg-status { line-height: 1.35; min-height: 2.7em; }
}
/* =========================================================================
   deePoker (deepoker)
   Play 2 hand cards into a lane of the shared 3x3 Arena to make five-card
   poker hands. The board is built around what a player reads every turn:
     a panel per seat with its points and its hand face up  (.dp-seats)
     the Arena, its lane letters around it as the rules draw it, the center
       card starred                                         (.dp-arena)
     beside it: the pool (the deck, three face-up cards, the discard pile),
       the last poker hand played, and the controls          (.dp-side)
   Every lane letter shows what two cards would score there (.dp-lane-pts):
   the two picked, or the whole hand when it holds two. The previewed lane's
   cells are ringed (.dp-inlane), and the keep choices are rows of card
   pictures (.dp-keeprow). A poker hand flashes its lane and the last-hand box
   (.dp-flash). In the last round the progress line and the seat panels say so
   (.dp-lastround, .dp-seat-tag).
   EVERY CARD SIZE is a custom property on .dp-board, never a rule on .card or
   a shared id, so each reservation follows it.
   ANTI-JITTER (ui-conventions.md, "Containers must not move"): every seat
   panel is one fixed size whatever its hand holds; the side column is one
   fixed width; the last hand is a head line and one row of cards; the
   controls are one row, the keep choices two rows, the note two lines, each
   a fixed height. Nothing on the board is ever `hidden` in a game.
   SHARED IDS are reached only through a class this game owns: the hand panel
   through .dp-handpanel, the status and progress lines through .dp-status and
   .dp-progress.
   ========================================================================= */

/* --- sizes ------------------------------------------------------------- */
.dp-board {
    --dp-cw: 80px;
    --dp-lw: 36px;
    --dp-poolcw: 68px;
    --dp-pilecw: 54px;
    --dp-lastcw: 40px;
    --dp-seatcw: 26px;
    --dp-keepcw: 34px;
    --dp-sidew: 360px;
    --dp-seatw: 220px;
}

/* Switched on and off without moving anything: the space is kept. */
.dp-off { visibility: hidden; }

/* --- the seats: a panel each, its hand face up --------------------------- */
.dp-board { margin-bottom: 8px; }
.dp-seats {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px 8px;
    margin-bottom: 10px;
}
.dp-seat {
    box-sizing: border-box;
    width: var(--dp-seatw);
    max-width: 100%;
    padding: 5px 8px 6px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.dp-seat.dp-active { outline-color: var(--accent); }
.dp-seat.dp-won { border-color: var(--accent); }
.dp-seat.dp-done { opacity: 0.7; }
.dp-seat-head {
    display: flex;
    align-items: baseline;
    gap: 5px;
    white-space: nowrap;
}
.dp-toact { flex: none; color: var(--accent); font-size: 11px; }
.dp-seat-name {
    flex: 1 1 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: 700;
    font-size: 13px;
}
.dp-seat-tag {
    flex: none;
    padding: 0 6px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    background: var(--accent);
    color: var(--accent-ink);
}
.dp-seat.dp-done .dp-seat-tag { background: transparent; color: var(--ink); border: 1px solid var(--panel-border); }
/* The score changes all game, so it has a fixed width. */
.dp-seat-score {
    flex: none;
    display: inline-block;
    width: 2.2ch;
    text-align: right;
    font-size: 16px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
/* A row held at one card's height whatever the hand holds (up to 7). */
.dp-seat-cards {
    display: flex;
    gap: 3px;
    margin-top: 4px;
    height: calc(var(--dp-seatcw) * 89 / 64);
}
.dp-seats .card.dp-seatcard { --cw: var(--dp-seatcw); flex: none; }
/* Solo: the one panel is the player's, whose hand is the hand panel. */
.dp-seats.dp-n1 .dp-seat-cards { display: none; }

/* --- the table: the Arena, and beside it the side column ----------------- */
.dp-table {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    gap: 12px 22px;
}

/* --- the Arena ----------------------------------------------------------- */
.dp-arena {
    display: grid;
    grid-template-columns: var(--dp-lw) repeat(3, var(--dp-cw)) var(--dp-lw);
    grid-template-rows: var(--dp-lw) repeat(3, calc(var(--dp-cw) * 89 / 64)) var(--dp-lw);
    gap: 6px;
    padding: 8px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.12);
}
.dp-spot {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 0;
}
.dp-cellspot { position: relative; }
.dp-arena .card.dp-cell { --cw: var(--dp-cw); }
.dp-arena .card.dp-center { box-shadow: 0 0 0 2px #e8b93d; }
.dp-arena .card.dp-inlane { box-shadow: 0 0 0 3px var(--accent); }
.dp-arena .card.dp-center.dp-inlane { box-shadow: 0 0 0 3px var(--accent), 0 0 0 5px #e8b93d; }
/* The cell a keep choice under the pointer would replace. */
.dp-arena .card.dp-cell.dp-keeptarget { box-shadow: 0 0 0 4px #ff8a65; }
.dp-star {
    position: absolute;
    top: -8px;
    right: -8px;
    font-size: 16px;
    line-height: 1;
    color: #e8b93d;
    text-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
    pointer-events: none;
}
.dp-lane {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: var(--dp-lw);
    height: var(--dp-lw);
    padding: 0;
    border-radius: 50%;
    border: 1px solid var(--panel-border);
    background: var(--panel);
    color: var(--ink);
    font: inherit;
    line-height: 1;
    cursor: pointer;
}
.dp-lane-letter { font-size: 13px; font-weight: 800; }
.dp-lane-pts {
    font-size: 12px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    min-height: 12px;
}
/* What two cards would score: 5 and up stands out, a pair or less recedes. */
.dp-lane.dp-pts-big .dp-lane-pts { color: var(--accent); }
.dp-lane.dp-pts-mid .dp-lane-pts { color: var(--ink); }
.dp-lane.dp-pts-low .dp-lane-pts { color: var(--ink); opacity: 0.55; }
.dp-lane.dp-pts-big { border-color: var(--accent); }
.dp-lane.dp-lane-idle { cursor: default; }
.dp-lane.dp-lane-idle:not(.dp-pts-big):not(.dp-pts-mid):not(.dp-pts-low) { opacity: 0.7; }
.dp-lane.dp-lane-on { background: var(--accent); color: var(--accent-ink); }
.dp-lane.dp-lane-on .dp-lane-pts { color: var(--accent-ink); opacity: 1; }
.dp-lanemirror {
    font-size: 12px;
    font-weight: 800;
    opacity: 0.45;
    border-radius: 4px;
}
.dp-lanemirror.dp-lane-on { opacity: 1; color: var(--accent); }

/* --- the side column: the pool, the last hand, the controls -------------- */
.dp-side {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    width: var(--dp-sidew);
    max-width: 100%;
}
.dp-pool {
    display: flex;
    align-items: flex-end;
    justify-content: center;
    gap: 10px;
}
.dp-pile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.dp-pool .card.dp-deckcard,
.dp-pool .card.dp-discardcard { --cw: var(--dp-pilecw); }
.dp-pilecount {
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    opacity: 0.85;
}
.dp-faceup { display: flex; gap: 6px; }
.dp-pool .card.dp-poolcard { --cw: var(--dp-poolcw); }
.dp-pool .card.dp-empty { opacity: 0.3; }
.dp-last {
    box-sizing: border-box;
    width: calc(5 * var(--dp-lastcw) + 4 * 4px + 8px + 22px);
    max-width: 100%;
    padding: 6px 10px 8px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.dp-last-head {
    font-size: 12px;
    font-weight: 700;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-bottom: 5px;
}
.dp-last-pts { color: var(--accent); }
.dp-last-cards {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 4px;
    height: calc(var(--dp-lastcw) * 89 / 64);
}
.dp-last-cards .card { --cw: var(--dp-lastcw); flex: none; }
.dp-last-gap { flex: none; width: 4px; }
.dp-last-cards .card.dp-last-played { box-shadow: 0 0 0 2px var(--accent); }
.dp-last-cards .card.dp-last-kept { box-shadow: 0 0 0 2px #e8b93d; }

/* --- the controls, the keep choices, the note ---------------------------- */
.dp-actions { width: 100%; }
/* Heights are fixed rather than minimums, so no button or wrapped line can
   grow a row: the controls scroll sideways instead. The row scrolls when it
   overflows, so it centers with auto margins on its end children rather than
   justify-content: centered content that overflows is clipped behind an edge
   nobody can scroll to (ui-conventions.md). */
.dp-controls {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: center;
    gap: 8px;
    height: 42px;
    overflow-x: auto;
    overflow-y: hidden;
}
.dp-controls .btn { white-space: nowrap; flex: none; }
.dp-controls > :first-child { margin-left: auto; }
.dp-controls > :last-child { margin-right: auto; }
/* Two rows, one per played card: "Keep [card] in place of [3 lane cards]". */
.dp-keeps {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    height: calc(2 * (var(--dp-keepcw) * 89 / 64 + 6px) + 4px);
    margin-top: 4px;
}
.dp-keeprow {
    display: flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
}
.dp-keepword { font-size: 12px; opacity: 0.85; }
.dp-keeps .card { --cw: var(--dp-keepcw); flex: none; }
.dp-keep {
    flex: none;
    display: block;
    padding: 2px;
    line-height: 0;
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    background: var(--panel);
    cursor: pointer;
}
.dp-keep:hover,
.dp-keep:focus-visible { border-color: #ff8a65; outline: 2px solid #ff8a65; outline-offset: 0; }
.dp-note {
    height: calc(2 * 1.35em);
    line-height: 1.35;
    overflow: hidden;
    margin-top: 4px;
    text-align: center;
    font-size: 13px;
}
.dp-note .red,
.dp-last-head .red { color: var(--red); }
.dp-note .red.diamond { color: var(--red-diamond); }
.dp-note .club { color: var(--blue-club); }

/* --- the hand ------------------------------------------------------------ */
.dp-handpanel #hand-cards .card.dp-match { box-shadow: 0 0 0 2px #e8b93d; }

/* --- status and progress ------------------------------------------------- */
.status.dp-status { line-height: 1.25; min-height: 1.25em; }
.status.dp-status.dp-turn { font-weight: 700; }
.status.dp-status .red { color: var(--red); }
.status.dp-status .red.diamond { color: var(--red-diamond); }
.status.dp-status .club { color: var(--blue-club); }
.progress.dp-progress { line-height: 1.3; min-height: 1.3em; }
/* The last round has to be noticed: in the accent color, bold, on a band. */
.progress.dp-progress.dp-lastround {
    color: var(--accent);
    font-weight: 800;
    background: rgba(255, 209, 102, 0.14);
    border-radius: 8px;
}

/* --- a poker hand, seen: its lane and the last-hand box flash ------------- */
@keyframes dp-flash-ring {
    0% { box-shadow: 0 0 0 0 rgba(255, 209, 102, 0); }
    12% { box-shadow: 0 0 0 6px rgba(255, 209, 102, 0.95); }
    75% { box-shadow: 0 0 0 6px rgba(255, 209, 102, 0.75); }
    100% { box-shadow: 0 0 0 0 rgba(255, 209, 102, 0); }
}
@keyframes dp-flash-fill {
    0% { background-color: transparent; }
    12% { background-color: rgba(255, 209, 102, 0.55); }
    75% { background-color: rgba(255, 209, 102, 0.35); }
    100% { background-color: transparent; }
}
.dp-arena .card.dp-cell.dp-flash,
.dp-last.dp-flash { animation: dp-flash-ring 1.9s ease-out; }
.dp-arena .dp-lane.dp-flash,
.dp-arena .dp-lanemirror.dp-flash,
.progress.dp-progress.dp-flash { animation: dp-flash-fill 1.9s ease-out; }
body.motion-off .dp-flash { animation: none; }

/* --- the result ---------------------------------------------------------- */
.dp-result {
    border-collapse: collapse;
    margin: 0 auto 10px;
    font-size: 14px;
}
.dp-result th,
.dp-result td {
    padding: 4px 10px;
    border-bottom: 1px solid var(--panel-border);
    white-space: nowrap;
}
.dp-result th { font-size: 12px; font-weight: 600; opacity: 0.85; }
.dp-result .dp-name { text-align: left; }
.dp-result .dp-num { text-align: right; font-variant-numeric: tabular-nums; }
.dp-result tr.dp-best td { color: var(--accent); font-weight: 700; }

/* --- phones ------------------------------------------------------------ */
/* One column: the side column goes under the Arena. Two seat panels a row. */
@media (max-width: 720px) {
    .dp-board {
        --dp-cw: 60px;
        --dp-lw: 30px;
        --dp-poolcw: 56px;
        --dp-pilecw: 42px;
        --dp-lastcw: 34px;
        --dp-seatcw: 21px;
        --dp-keepcw: 30px;
        --dp-sidew: 100%;
        --dp-seatw: 172px;
    }
    .dp-arena { gap: 4px; padding: 5px; }
    .dp-table { gap: 8px; }
    .dp-seats { gap: 4px 6px; margin-bottom: 8px; }
    .dp-seat { padding: 4px 6px 5px; }
    .dp-seat-cards { gap: 2px; }
    .dp-seat-name { font-size: 12px; }
    .dp-seat-score { font-size: 14px; }
    .dp-side { gap: 8px; }
    /* The longest status and progress lines wrap to two lines here. */
    .status.dp-status { min-height: 2.5em; }
    .progress.dp-progress { min-height: 2.6em; }
}
@media (max-width: 380px) {
    .dp-board { --dp-cw: 56px; --dp-lw: 28px; --dp-poolcw: 52px; --dp-seatw: 164px; --dp-seatcw: 20px; }
}
/* =========================================================================
   Mexican Standoff (mexicanstandoff)
   A trick game for 3 or 4 where every trick you take holds bullets aimed at
   you, and the shoot-out after the last trick decides who is left alive.
     the table as a table: the other seats round the trick in play order,
       this player South, and the trick a cross with each card on its seat's
       side                                     (.mxs-board, .mxs-trick, .pos-*)
     a panel per seat: name, dealer chip, and life; its open cards and its
       hidden hand (other seats only); the tricks it has taken
       (.mxs-seat, .mxs-seat-open, .mxs-seat-hand, .mxs-piles)
     one center cell holding two layers, the trick and the start-up decision,
       so the cell is the larger of the two all game   (.mxs-center, .mxs-off)
     this player's open cards, a row of their own above the hand
       (.mxs-openpanel, .mxs-opencards)
     the shoot-out summary, one column per seat        (.mxs-result)
   CONTAINERS MUST NOT MOVE. Every panel line has a reserved height derived
   from the card size that fills it, the pile strip and the open-card rows are
   one row that never wraps, and the center is a fixed size whichever layer it
   shows. #hand-cards gets no min-height: src/shared/ui/handspace.ts reserves
   it at runtime.
   THE BOARD IS A CONTAINER (mxsboard), so the layout follows the room the
   board has, not the viewport: below 820px the other seats stack above the
   trick. Nothing inside the board is position: fixed.
   SHARED IDS only through a class this game owns: .mxs-handpanel on the hand
   panel, .mxs-status and .mxs-progress added by the renderers, .mxs-summary
   on the result panel.
   ========================================================================= */

/* --- status and progress ------------------------------------------------ */
/* Two lines reserved: the action wanted, and a hint when one is asked for. */
.status.mxs-status { min-height: 2.6em; }
.status.mxs-status.mxs-turn { color: var(--accent); }
.mxs-statusmain { display: block; }
.mxs-hintnote { display: block; font-size: 14px; font-weight: 400; opacity: 0.85; }
.progress.mxs-progress { min-height: 20px; white-space: nowrap; }
.mxs-suited .red { color: var(--red); }
.mxs-suited .red.diamond { color: var(--red-diamond); }
.mxs-suited .club { color: var(--blue-club); }

/* --- the board ----------------------------------------------------------- */
.mxs-board {
    container: mxsboard / inline-size;
    --mxs-playcw: 56px;
    --mxs-oppcw: 34px;
    --mxs-backcw: 22px;
    --mxs-pilecw: 26px;
    --mxs-gap: 3px;
    --mxs-centerw: 280px;
    --mxs-seatw: 272px;
    --mxs-btnh: 38px;
    --mxs-stagecw: 24px;
    margin-bottom: 10px;
}
/* ONE GRID on a wide board. #mxs-table is display: contents there, so the
   panels are cells of the board and take the areas their seats sit in. The
   center track is a fixed width, never auto, so neither flank moves when the
   center changes layer. */
.mxs-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--mxs-centerw) minmax(0, 1fr);
    column-gap: 12px;
    row-gap: 8px;
    align-items: center;
}
.mxs-board.mxs-seats-3 .mxs-grid { grid-template-areas: "w center e" ". mine ."; }
.mxs-board.mxs-seats-4 .mxs-grid { grid-template-areas: ". n ." "w center e" ". mine ."; }
.mxs-table { display: contents; }
.mxs-seat.pos-w { grid-area: w; justify-self: end; }
.mxs-seat.pos-e { grid-area: e; justify-self: start; }
.mxs-seat.pos-n { grid-area: n; justify-self: center; }
.mxs-seat.mxs-mine { grid-area: mine; justify-self: center; }

/* --- a seat's panel ------------------------------------------------------ */
.mxs-seat {
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 4px;
    width: var(--mxs-seatw);
    max-width: 100%;
    min-width: 0;
    padding: 6px 8px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default, so the accent ring never shifts the layout. */
    outline: 2px solid transparent;
}
.mxs-seat.mxs-active { outline-color: var(--accent); }
.mxs-seat.mxs-gone { opacity: 0.6; }
.mxs-seat-head {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 5px;
    min-height: calc(13px * 1.4);
    font-size: 13px;
    line-height: 1.4;
    white-space: nowrap;
}
.mxs-seat-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-weight: 700; }
/* Whose turn, as a shape as well as the ring. Always rendered, so the name
   never shifts sideways when the turn moves. */
.mxs-toact { visibility: hidden; color: var(--accent); }
.mxs-seat.mxs-active .mxs-toact { visibility: visible; }
/* Always rendered, so the name sits in one place whoever deals. */
.mxs-dealer {
    visibility: hidden;
    font-size: 10px;
    font-weight: 700;
    line-height: 1.3;
    padding: 0 3px;
    border: 1px solid var(--accent);
    border-radius: 4px;
    color: var(--accent);
}
.mxs-seat.mxs-isdealer .mxs-dealer { visibility: visible; }
.mxs-life { font-variant-numeric: tabular-nums; opacity: 0.85; }
.mxs-life.mxs-life-full { opacity: 0.55; }
.mxs-life.mxs-alive { color: #8fd6a4; font-weight: 700; opacity: 1; }
.mxs-life.mxs-dead { color: #ff9c8f; font-weight: 700; text-decoration: line-through; opacity: 1; }
/* The open cards: one row of up to 7, reserved at one card's height. */
.mxs-seat-open {
    display: flex;
    justify-content: center;
    gap: var(--mxs-gap);
    min-height: calc(var(--mxs-oppcw) * 1.39);
}
.mxs-seat .mxs-seat-open .card { --cw: var(--mxs-oppcw); }
/* The last line: how many hand cards the seat holds, beside the tricks taken,
   one pile-card high. */
.mxs-seat-line {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: calc(var(--mxs-pilecw) * 1.39 + 19px);
}
/* One card back with the count on it, a fixed size whatever the count. */
.mxs-handchip {
    position: relative;
    flex: none;
    display: inline-flex;
}
.mxs-seat .mxs-handchip .card { --cw: var(--mxs-backcw); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); }
.mxs-handcount {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 12px;
    font-weight: 800;
    color: #fff;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
    font-variant-numeric: tabular-nums;
}
.mxs-handchip.mxs-handchip-empty { opacity: 0.35; }
/* The tricks taken: one strip that NEVER scrolls. Each pile is a box a little
   wider than its card that may shrink to a sliver, so a long strip overlaps
   its cards like a fanned hand instead of overflowing; the damage badge sits
   under each card, its number at the left, which stays in view. The last pile
   keeps its full width. Centered by auto margins on its ends. */
.mxs-piles {
    display: flex;
    flex: 1 1 0;
    min-width: 0;
    flex-wrap: nowrap;
    gap: 3px;
    overflow: hidden;
    min-height: calc(var(--mxs-pilecw) * 1.39 + 19px);
    align-items: center;
    padding: 3px 0;
    box-sizing: border-box;
}
.mxs-piles > :first-child { margin-left: auto; }
.mxs-piles > :last-child { margin-right: auto; }
.mxs-nopile { font-size: 11px; opacity: 0.55; white-space: nowrap; }
.mxs-pile {
    position: relative;
    flex: 0 1 calc(var(--mxs-pilecw) + 4px);
    min-width: 19px;
    height: calc(var(--mxs-pilecw) * 1.39 + 13px);
}
.mxs-pile:last-child { flex-shrink: 0; }
.mxs-pile .card { position: absolute; left: 0; top: 0; --cw: var(--mxs-pilecw); }
/* The damage the trick's bullets will do, under its trigger. */
.mxs-pilebadge {
    position: absolute;
    left: 0;
    top: calc(var(--mxs-pilecw) * 1.39 + 1px);
    box-sizing: border-box;
    width: var(--mxs-pilecw);
    font-size: 10px;
    font-weight: 800;
    line-height: 12px;
    text-align: left;
    padding-left: 3px;
    font-variant-numeric: tabular-nums;
    border-radius: 4px;
    background: rgba(20, 20, 20, 0.82);
    color: #ffd7d2;
    white-space: nowrap;
}
.mxs-pile.mxs-fired .mxs-pilebadge { background: rgba(200, 50, 40, 0.92); color: #fff; }
.mxs-pile.mxs-firing .card { outline: 3px solid #ff7b6b; outline-offset: 1px; }
.mxs-pile.mxs-unfired { opacity: 0.45; }

/* A card just gained, by the exchange or the extra cards: lit, then fading.
   def.ts sets a negative animation-delay, so a repaint resumes the fade
   instead of restarting it. */
.card.mxs-gained { animation: mxs-gain 4s ease-out both; }
@keyframes mxs-gain {
    0%, 65% {
        outline: 3px solid #7fd6ff;
        outline-offset: 2px;
        box-shadow: 0 0 16px 3px rgba(127, 214, 255, 0.85);
    }
    100% {
        outline: 3px solid transparent;
        outline-offset: 2px;
        box-shadow: 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
    }
}
body.motion-off .card.mxs-gained { animation: none; outline: 3px solid #7fd6ff; outline-offset: 2px; }

/* A seat hit by the shooting being played back: a red flash, resumed where a
   repaint left it like the gained card's. */
.mxs-seat.mxs-hitnow { animation: mxs-hit 1.3s ease-out both; }
@keyframes mxs-hit {
    0% { box-shadow: 0 0 0 0 rgba(255, 90, 70, 0); }
    15% { box-shadow: 0 0 0 3px rgba(255, 90, 70, 0.95), inset 0 0 22px rgba(255, 70, 50, 0.45); }
    100% { box-shadow: 0 0 0 2px rgba(255, 90, 70, 0.6), inset 0 0 0 rgba(255, 70, 50, 0); }
}
body.motion-off .mxs-seat.mxs-hitnow { animation: none; box-shadow: 0 0 0 2px rgba(255, 90, 70, 0.8); }

/* --- the center: three layers in one cell -------------------------------- */
/* The trick and the start-up decision share the cell's grid area, so the cell
   is the larger of the two all game. The shoot-out's stage lies over the cell
   (position: absolute), so whatever a shooting holds never sizes it. */
.mxs-center {
    grid-area: center;
    position: relative;
    display: grid;
    box-sizing: border-box;
    width: var(--mxs-centerw);
    justify-self: center;
    justify-items: center;
    align-items: center;
}
.mxs-center > :not(.mxs-stage) { grid-area: 1 / 1; }
.mxs-center > .mxs-off { visibility: hidden; }

/* --- the trick: a cross --------------------------------------------------- */
.mxs-trick {
    display: grid;
    grid-template-columns: repeat(3, var(--mxs-playcw));
    gap: 8px;
    justify-content: center;
    justify-items: center;
    align-items: center;
    padding: 8px 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.mxs-board.mxs-seats-3 .mxs-trick { grid-template-areas: "w . e" ". s ."; }
.mxs-board.mxs-seats-4 .mxs-trick { grid-template-areas: ". n ." "w . e" ". s ."; }
.mxs-play.pos-s { grid-area: s; }
.mxs-play.pos-w { grid-area: w; }
.mxs-play.pos-n { grid-area: n; }
.mxs-play.pos-e { grid-area: e; }
.mxs-play .card { --cw: var(--mxs-playcw); }
.mxs-play .card.empty { opacity: 0.35; }
.mxs-play.onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
/* The viewer's own face-down card: its face, marked as the others see it. */
.mxs-play .card.mxs-down { outline: 2px dashed rgba(255, 255, 255, 0.6); outline-offset: 2px; opacity: 0.8; }
/* The card that took the finished trick: ringed and glowing. */
.mxs-play .card.mxs-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
/* The ranking note: always two lines reserved, holding words only after a
   J, Q, K, or A lead, so the cross never moves. */
.mxs-trickrule {
    grid-column: 1 / -1;
    align-self: stretch;
    min-height: calc(11px * 1.3 * 2);
    font-size: 11px;
    line-height: 1.3;
    text-align: center;
    color: #ffd7a0;
    overflow: hidden;
}
/* A card of the other color, or every card of a trick nobody followed: discarded. */
.mxs-play .card.mxs-discarded { opacity: 0.4; filter: grayscale(0.6); }

/* --- the shoot-out's stage -------------------------------------------------- */
/* The shooting being shown: its name, then a row per seat it hit, with the
   bullets that fired at that seat and the life they cost. */
.mxs-stage {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
    box-sizing: border-box;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(60, 10, 5, 0.32);
    overflow: hidden;
}
.mxs-center > .mxs-stage.mxs-off { visibility: hidden; }
.mxs-stagehead { display: flex; justify-content: center; align-items: center; min-height: 22px; }
.mxs-stagetitle { font-size: 14px; font-weight: 800; white-space: nowrap; color: #ffd7d2; }
.mxs-stagerows {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 5px;
    flex: 1 1 auto;
    min-height: 0;
}
.mxs-stagerow {
    display: grid;
    grid-template-columns: 64px minmax(0, 1fr) auto 3.6em;
    align-items: center;
    column-gap: 6px;
}
.mxs-stagename {
    font-size: 12px;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-align: left;
}
.mxs-stagebullets { display: flex; align-items: center; min-width: 0; overflow: hidden; }
.mxs-stagebullets .card { --cw: var(--mxs-stagecw); flex: none; }
.mxs-stagebullets .card + .card { margin-left: 2px; }
.mxs-stagebullets.mxs-stagetight .card + .card { margin-left: calc(var(--mxs-stagecw) * -0.45); }
.mxs-stagebullets.mxs-stagetighter .card + .card { margin-left: calc(var(--mxs-stagecw) * -0.62); }
.mxs-stagegap { flex: none; width: calc(var(--mxs-stagecw) * 0.5); }
.mxs-stagedmg { font-size: 15px; font-weight: 800; color: #ff9c8f; font-variant-numeric: tabular-nums; }
.mxs-stageleft { font-size: 12px; white-space: nowrap; text-align: right; opacity: 0.85; font-variant-numeric: tabular-nums; }
.mxs-stageleft.mxs-dead { color: #ff9c8f; font-weight: 800; opacity: 1; text-decoration: none; }
/* The status line's Skip, during the playback. */
.mxs-status .mxs-skip { margin-left: 10px; vertical-align: middle; }

/* --- the start-up decision ------------------------------------------------ */
/* Three fixed rows whatever they hold, so the cell is one size all game. */
.mxs-action {
    display: grid;
    grid-template-rows: 22px calc(2 * var(--mxs-btnh) + 6px) calc(12px * 1.35 * 3);
    row-gap: 8px;
    align-items: center;
    justify-items: center;
    box-sizing: border-box;
    width: 100%;
    padding: 6px 4px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.mxs-actiontitle { font-size: 14px; font-weight: 700; white-space: nowrap; }
/* The two choices one above the other, so the longest label ("Exchange the
   10♣") fits the center at every width. */
.mxs-actionrow { display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 6px; }
.mxs-actionrow .btn {
    box-sizing: border-box;
    min-width: 180px;
    height: var(--mxs-btnh);
    padding-block: 0;
    white-space: nowrap;
}
.mxs-actionrow .btn.hinted { outline: 3px solid var(--accent); }
.mxs-actionnote {
    align-self: start;
    font-size: 12px;
    line-height: 1.35;
    text-align: center;
    opacity: 0.85;
    overflow: hidden;
}
.mxs-actionrow .red { color: var(--red); }
.mxs-actionrow .red.diamond { color: var(--red-diamond); }
.mxs-actionrow .club { color: var(--blue-club); }
.mxs-actionrow .btn.primary .red,
.mxs-actionrow .btn.primary .club { color: inherit; }

/* --- this player's open cards ---------------------------------------------- */
/* One row that never wraps, reserved at one card's height plus the lift a
   picked card takes. Centered, as a hand is. */
.mxs-openpanel { --mxs-owncw: var(--cw); }
.mxs-opencards {
    justify-content: center;
    flex-wrap: nowrap;
    min-height: calc(var(--mxs-owncw) * 1.39 + 6px);
    padding-top: 6px;
}
.mxs-openpanel .mxs-opencards .card { --cw: var(--mxs-owncw); }
/* A card that cannot be played now: dimmed rather than removed. */
.mxs-mute { opacity: 0.4; }

/* --- the summary ------------------------------------------------------ */
/* A real table, one column per seat, centered as a block. Every cell carries
   its own alignment, because the lifted dialog centers text and cells inherit it. */
.mxs-result {
    border-collapse: collapse;
    margin: 6px auto 0;
    font-size: 14px;
}
.mxs-result th,
.mxs-result td {
    padding: 3px 10px;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    border-bottom: 1px solid var(--panel-border);
}
.mxs-result th { font-size: 12px; font-weight: 600; opacity: 0.85; }
.mxs-result .mxs-stepcol { text-align: left; }
.mxs-result .mxs-num { text-align: right; }
.mxs-result .mxs-startrow td { opacity: 0.7; }
.mxs-hit { color: #ff9c8f; }
.mxs-left { font-weight: 700; }
.mxs-left.mxs-dead { color: #ff9c8f; text-decoration: line-through; }
.mxs-log-shot { color: #ffc2b8; }
/* A finished trick in the log: its caption, then the trick as a small cross,
   each card on the side its player sits (the Hearts log's shape). */
#log-entries .mxs-logtrick {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 3px 0;
}
.mxs-logtrick-cap { font-size: 12px; flex: 1 1 auto; min-width: 0; opacity: 0.9; }
.mxs-logcross {
    --mxs-lcw: 19px;
    flex: none;
    display: grid;
    grid-template-columns: repeat(3, var(--mxs-lcw));
    grid-template-rows: repeat(3, calc(var(--mxs-lcw) * 1.39));
    grid-template-areas: ". n ." "w . e" ". s .";
    align-items: center;
    justify-items: center;
    gap: 1px 4px;
    padding: 0 9px;
}
.mxs-logcross.mxs-logcross-3 {
    grid-template-rows: repeat(2, calc(var(--mxs-lcw) * 1.39));
    grid-template-areas: "w . e" ". s .";
}
.mxs-logcross .card { --cw: var(--mxs-lcw); }
.mxs-logcross .card.mxs-logdim { opacity: 0.38; }
.mxs-logcell { position: relative; line-height: 0; }
.mxs-logcell.pos-s { grid-area: s; }
.mxs-logcell.pos-w { grid-area: w; }
.mxs-logcell.pos-n { grid-area: n; }
.mxs-logcell.pos-e { grid-area: e; }
/* The lead's caret and the taker's check: symbols as well as the ring. */
.mxs-logled,
.mxs-logwon {
    position: absolute;
    font-size: 10px;
    line-height: 1;
    font-weight: 800;
    pointer-events: none;
}
.mxs-logled { top: -1px; left: -8px; opacity: 0.85; }
.mxs-logwon { bottom: -1px; right: -8px; color: var(--accent); }

/* --- the ranking in the Cheat_Sheet ----------------------------------------- */
/* Thirteen example cards a row, so the example goes under its words, at a size
   that fits a phone. */
.cheatsheet ul.reflist.mxs-ranklist > li {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: 'name' 'what' 'ex';
    row-gap: 3px;
}
.cheatsheet .reflist.mxs-ranklist > li > .reflist-name { grid-row: auto; }
.cheatsheet .reflist.mxs-ranklist > li > .cardex { --cw: 24px; gap: 1px; justify-self: start; margin-top: 2px; }
.mxs-log-end { color: var(--accent); font-weight: 700; }

/* --- touch screens: the decision buttons are tapped ------------------------ */
@media (pointer: coarse) {
    .mxs-board { --mxs-btnh: 44px; }
}

/* --- a narrow board: the other seats above the trick ------------------------ */
/* Every reservation is re-derived here. The panels sit in a wrapping row of
   their own (one a line on a phone), then the center at its fixed size, then
   this player's panel. */
@container mxsboard (max-width: 819px) {
    .mxs-grid { display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
    .mxs-table { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; }
    .mxs-table .mxs-seat { flex: 1 1 260px; width: auto; max-width: 420px; }
    .mxs-center { margin-inline: auto; }
    .mxs-seat.mxs-mine { align-self: center; }
}
@media (max-width: 560px) {
    /* A status sentence can wrap on a phone, so two lines and the hint's. */
    .status.mxs-status { min-height: calc(2.8em + 20px); }
    .mxs-board {
        --mxs-playcw: 44px;
        --mxs-oppcw: 30px;
        --mxs-backcw: 20px;
        --mxs-pilecw: 24px;
        --mxs-centerw: 260px;
        --mxs-seatw: 260px;
        --mxs-stagecw: 21px;
    }
    .mxs-trick { gap: 6px; padding: 6px 10px; }
    /* Seven cards in one row at 40px and a 4px gap: 304px, inside a 326px panel. */
    .mxs-openpanel { --mxs-owncw: 40px; }
    .mxs-opencards { gap: 4px; }
    .mxs-handpanel #hand-cards { gap: 4px; }
    .mxs-handpanel #hand-cards .card { --cw: 40px; }
    .cheatsheet .reflist.mxs-ranklist > li > .cardex { --cw: 20px; }
    .mxs-stagerow { grid-template-columns: 56px minmax(0, 1fr) auto 3.4em; column-gap: 5px; }
}
/* =========================================================================
   Mortal Swords (mortalswords)
   A sword fight for two to five knights. Each attack walks clockwise, and
   every defender must stop the same card. The board is built around what a
   player reads every turn:
     the other knights across the top, clockwise from the viewer's left, each
       panel centered: the name, the hand as backs (a status word once it is
       empty), the table cards face-up or face-down             (.msw-seat)
     the deck with the A♠ sideways under it, the fight with a result pill on
       every card and the outcome line under it, and the discards
                                                            (.msw-center)
   The log paints each attack as one line of small cards and pills (.msw-lg).
     the viewer's own table cards, tappable, above the hand  (.msw-mine)
   EVERY CARD SIZE is a custom property on .msw-board (and the hand panel),
   never a rule on .card or a shared id, so each reservation follows it.
   ANTI-JITTER (ui-conventions.md, "Containers must not move"). Every box
   above the hand is reserved at its tallest ordinary shape, and re-derived at
   the phone breakpoint: each seat panel at five cards in each row, the fight
   at one row of slots with three caption lines and the outcome at two lines
   (three on a phone), the viewer's table at one row of five, the controls at
   one row, the note at two lines. The ▶ and the status tag sit out of the flow,
   so the name and the title center; .msw-off hides them in place.
   SHARED IDS are reached only through a class this game owns: the hand panel
   through .msw-handpanel, the status line through .msw-status.
   ========================================================================= */

/* --- sizes ------------------------------------------------------------- */
.msw-board {
    --msw-backcw: 18px;
    --msw-oppcw: 34px;
    --msw-deckcw: 46px;
    --msw-strikecw: 64px;
    --msw-defcw: 52px;
    --msw-slotw: 76px;
    --msw-minecw: 60px;
    --msw-seatw: 210px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}
/* The result colors, on the board, the hand panel, and the log lines, which all use them. */
.msw-board, .panel.msw-handpanel, .logline.msw-logattack {
    --msw-parry: #5cc46a;
    --msw-deflect: #7fb6e8;
    --msw-hit: #ef6b62;
}
.msw-off { visibility: hidden; }

/* --- the other knights ---------------------------------------------------- */
.msw-seats {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
    width: 100%;
}
.msw-seat {
    position: relative;
    box-sizing: border-box;
    width: var(--msw-seatw);
    max-width: 100%;
    min-width: 0;
    padding: 6px 9px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.msw-seat.msw-active { outline-color: var(--accent); }
.msw-seat.msw-gone { opacity: 0.5; }
.msw-seat-head, .msw-mine-head {
    position: relative;
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 5px;
    margin-bottom: 4px;
    white-space: nowrap;
}
/* Out of the flow, so the name centers in the panel. */
.msw-seat-head .msw-toact { position: absolute; left: 0; top: 2px; color: var(--accent); font-size: 11px; }
.msw-mine-head .msw-tag { position: absolute; right: 0; top: 0; }
.msw-seat-name {
    flex: 0 1 auto;
    max-width: calc(100% - 28px);
    min-width: 0;
    font-weight: 700;
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.msw-tag {
    flex: none;
    font-size: 11px;
    font-weight: 700;
    padding: 1px 6px;
    border-radius: 8px;
    background: rgba(239, 107, 98, 0.22);
    color: #ffd2cd;
    white-space: nowrap;
}
.msw-seat-hand {
    display: flex;
    justify-content: center;
    gap: 3px;
    align-items: center;
    min-height: calc(var(--msw-backcw) * 89 / 64);
    margin-bottom: 4px;
}
.msw-seat-hand .card.msw-oppback { --cw: var(--msw-backcw); border-radius: 2px; }
.msw-seat-hand .card.msw-oppcard { --cw: var(--msw-oppcw); }
.msw-seat-table {
    display: flex;
    justify-content: center;
    gap: 4px;
    align-items: center;
    min-height: calc(var(--msw-oppcw) * 89 / 64);
}
.msw-seat-table .card.msw-oppcard { --cw: var(--msw-oppcw); }
/* A review shows the hand as faces at the table cards' size, so the row grows
   to theirs; live play never paints faces there. */
.msw-seat-hand:has(.msw-oppcard) { min-height: calc(var(--msw-oppcw) * 89 / 64); }
.card.msw-down { opacity: 0.55; }
/* The card the last attack's damage took, until the next attack starts. */
.card.msw-wound { outline: 3px solid var(--msw-hit); outline-offset: 1px; }
.card.msw-down.msw-wound { opacity: 0.8; }

/* --- the center: deck, fight, discards ----------------------------------- */
.msw-center {
    display: grid;
    grid-template-columns: auto minmax(0, auto) auto;
    align-items: center;
    justify-content: center;
    gap: 14px;
    width: 100%;
}
.msw-deck, .msw-discard {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.msw-deckstack {
    position: relative;
    width: calc(var(--msw-deckcw) * 1.55);
    height: calc(var(--msw-deckcw) * 89 / 64);
}
.msw-deckstack .card.msw-deckcard {
    --cw: var(--msw-deckcw);
    position: absolute;
    right: 0;
    top: 0;
}
/* The A♠ lies sideways under the deck: turned with `rotate`, centered with
   `translate`, never `transform` (ui-conventions, "A sideways card turns with
   rotate"), and scoped under its container so a flight ghost of it is not. */
.msw-deckstack .card.msw-ace {
    --cw: var(--msw-deckcw);
    position: absolute;
    left: calc(var(--msw-deckcw) * 0.2);
    top: 50%;
    translate: 0 -50%;
    rotate: -90deg;
}
.msw-discard .card.msw-discardcard { --cw: var(--msw-deckcw); }
.card.msw-empty { opacity: 0.25; }
.msw-count {
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.msw-quiet { font-weight: 400; opacity: 0.75; }

.msw-fight {
    min-width: calc(var(--msw-slots, 2) * (var(--msw-slotw) + 6px) + 6px);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.msw-fight-head {
    font-size: 12px;
    font-weight: 700;
    opacity: 0.8;
    white-space: nowrap;
    text-align: center;
}
.msw-fight-row {
    display: flex;
    justify-content: center;
    align-items: flex-end;
    gap: 6px;
    min-height: calc(var(--msw-strikecw) * 89 / 64 + 52px);
}
.msw-slot {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: var(--msw-slotw);
    min-width: 0;
}
.msw-slot.msw-strike { margin-right: 6px; }
.msw-slot .card.msw-strikecard { --cw: var(--msw-strikecw); }
.msw-slot .card.msw-defcard { --cw: var(--msw-defcw); }
.msw-slot.msw-hit .card.msw-defcard { outline: 3px solid var(--msw-hit); outline-offset: 1px; }
.msw-slot.msw-parry .card.msw-defcard { outline: 3px solid var(--msw-parry); outline-offset: 1px; }
.msw-slot.msw-deflect .card.msw-defcard { outline: 3px solid var(--msw-deflect); outline-offset: 1px; }
.msw-slot.msw-next .card.msw-waiting { border-color: var(--accent); }
.msw-slotname, .msw-slotresult, .msw-slotfrom {
    width: 100%;
    font-size: 11px;
    line-height: 14px;
    min-height: 14px;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.msw-slotname { font-weight: 700; margin-top: 3px; }
.msw-slotresult { min-height: 18px; line-height: 18px; margin-top: 2px; }
.msw-slotfrom { font-style: italic; opacity: 0.8; }

/* What a card did: a colored pill, on the board and in the log. */
.msw-pill {
    display: inline-block;
    padding: 0 7px;
    border-radius: 9px;
    font-size: 11px;
    font-weight: 800;
    line-height: 16px;
    white-space: nowrap;
    color: #10210f;
}
.msw-pill.msw-pill-parry { background: var(--msw-parry); }
.msw-pill.msw-pill-deflect { background: var(--msw-deflect); }
.msw-pill.msw-pill-hit { background: var(--msw-hit); color: #fff; }
.msw-pill.msw-pill-attack { background: rgba(255, 255, 255, 0.16); color: inherit; }
.msw-pill.msw-pill-next { box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent); background: transparent; }

/* How the attack ended, under the fight: always rendered, two lines reserved. */
.msw-outcome {
    box-sizing: border-box;
    width: 100%;
    max-width: 460px;
    min-height: 36px;
    margin-top: 4px;
    font-size: 13px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
}
.msw-outcome.msw-out-hit { color: #ffc4bf; }
.msw-outcome.msw-out-safe { color: #c9f0cf; }

/* --- the viewer's table -------------------------------------------------- */
.msw-mine {
    box-sizing: border-box;
    width: min(100%, 420px);
    display: flex;
    flex-direction: column;
    align-items: center;
}
.msw-mine-head { justify-content: center; }
.msw-mine-title { font-weight: 700; font-size: 13px; }
.msw-mine-cards {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 6px;
    min-height: calc(var(--msw-minecw) * 89 / 64 + 6px);
}
.msw-mine-cards .card.msw-minecard { --cw: var(--msw-minecw); }
.msw-none { font-size: 12px; opacity: 0.6; white-space: nowrap; }

/* What a card would do against the attack, on the defender's own turn. */
.card.msw-parries { box-shadow: 0 0 0 3px var(--msw-parry); }
.card.msw-deflects { box-shadow: 0 0 0 3px var(--msw-deflect); }
.card.msw-dim { opacity: 0.45; filter: saturate(0.6); }

/* --- controls and the note ----------------------------------------------- */
.msw-actions {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    margin-bottom: 6px;
}
.msw-controls {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: 8px;
    min-height: 36px;
}
.msw-controls .btn { white-space: nowrap; }
.msw-controls .btn.sel { outline: 2px solid var(--accent); }
.msw-controls .btn.hinted { animation: hintpulse 0.9s ease-in-out infinite alternate; outline: 3px solid var(--accent); }
.msw-note {
    min-height: 36px;
    max-width: 560px;
    font-size: 13px;
    line-height: 18px;
    text-align: center;
}
.msw-hint { color: var(--accent); }
.status.msw-status { min-height: 2.6em; }

/* --- the log --------------------------------------------------------------- */
#log-entries .msw-logquiet { opacity: 0.7; }
#log-entries .msw-logout { font-weight: 700; }
/* One attack, one line: small simplified cards (logpic's picCard, no ids),
   each with its knight, where it came from, and its result pill. */
.msw-lg-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 3px 8px;
}
.msw-lg-chip { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
.msw-lg-name { font-weight: 700; }
.msw-lg .card.msw-lgcard { --cw: 22px; flex: none; }
.msw-lg-from { font-size: 11px; font-style: italic; opacity: 0.8; }
.msw-lg-arrow { opacity: 0.6; }
.msw-lg .msw-pill { font-size: 10px; line-height: 14px; padding: 0 5px; }
.msw-lg-after { margin-top: 2px; }
/* The line's prose, for a screen reader only. */
.msw-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* --- phones ----------------------------------------------------------------- */
@media (max-width: 560px) {
    .msw-board {
        --msw-backcw: 14px;
        --msw-oppcw: 26px;
        --msw-deckcw: 30px;
        --msw-strikecw: 44px;
        --msw-defcw: 38px;
        --msw-slotw: 46px;
        --msw-minecw: 50px;
        --msw-seatw: calc(50% - 5px);
        gap: 8px;
    }
    .msw-seats { gap: 8px; }
    .msw-seats.msw-n2 .msw-seat { width: min(100%, 240px); }
    .msw-center { gap: 6px; }
    .msw-fight-row { gap: 3px; }
    .msw-slotname, .msw-slotfrom { font-size: 10px; }
    .msw-slot .msw-pill { font-size: 9.5px; padding: 0 4px; }
    .msw-outcome { min-height: 54px; font-size: 12px; }
    .msw-slot.msw-strike { margin-right: 2px; }
    .msw-controls { gap: 6px; }
    .msw-controls .btn { padding-left: 10px; padding-right: 10px; }
    .msw-note { min-height: 54px; }
    .status.msw-status { min-height: 3.9em; }
}
/* =========================================================================
   Puzzle Poker (src/puzzlepoker/, pages/puzzlepoker.mjs)
   The duel's 5 x 3 grid, drawn from the viewer's side: the viewer's two rows
   at the bottom (blue), the shared row in the middle (purple), the opponent's
   two rows on top (red), as in the rules' own diagram. The solo grid is 3 x 3.
   Every cell is a fixed size whether or not it holds a card, so nothing on
   the board moves as cards come and go (ui-conventions.md, "Containers must
   not move"). Grid cards use the simplified atlas ("Dense grids of cards").
   Shared classes (.card, .card.simple, .panel, .btn, .logline,
   .winner-banner) are reused; everything game-owned is prefixed `pzp-`.
   ========================================================================= */
/* The board's geometry is variables, because the meld lines (view.ts) are
   placed from the same numbers that lay the cells out. */
.pzp-board {
    --pzp-cw: 56px;
    --pzp-gap: 6px;
    --pzp-padx: 8px;
    --pzp-pady: 8px;
    --pzp-rowgap: 4px;
    --pzp-rowpadx: 8px;
    --pzp-rowpady: 3px;
    --pzp-cellw: calc(var(--pzp-cw) + 8px);
    --pzp-cellh: calc(var(--pzp-cw) * 89 / 64 + 8px);
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--pzp-rowgap);
    padding: var(--pzp-pady) var(--pzp-padx);
    margin: 0 auto 8px;
    width: fit-content;
    max-width: 100%;
    box-sizing: border-box;
}
.pzp-board.pzp-solo { --pzp-cw: 72px; }
.pzp-row {
    display: flex;
    justify-content: center;
    gap: var(--pzp-gap);
    padding: var(--pzp-rowpady) var(--pzp-rowpadx);
    border-radius: 10px;
}
/* Each side's rows are outlined in its color, as in the rules' own diagram:
   a fill alone turns red into brown on the green felt. */
.pzp-row.pzp-mine { background: rgba(90, 140, 255, 0.2); box-shadow: inset 0 0 0 2px rgba(120, 165, 255, 0.7); }
.pzp-row.pzp-theirs { background: rgba(255, 110, 110, 0.12); box-shadow: inset 0 0 0 2px rgba(255, 115, 115, 0.7); }
.pzp-row.pzp-shared { background: rgba(165, 90, 235, 0.3); box-shadow: inset 0 0 0 2px rgba(205, 150, 255, 0.8); }
.pzp-row.pzp-rowsolo { background: rgba(255, 255, 255, 0.05); }
.pzp-cell {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--pzp-cellw);
    height: var(--pzp-cellh);
    padding: 0;
    border: 2px solid transparent;
    border-radius: 9px;
    background: transparent;
    color: inherit;
    font: inherit;
    cursor: default;
    -webkit-appearance: none;
    appearance: none;
    box-sizing: border-box;
}
.pzp-cell > .card { --cw: var(--pzp-cw); }
.pzp-slot {
    width: var(--pzp-cw);
    height: calc(var(--pzp-cw) * 89 / 64);
    border: 2px dashed rgba(255, 255, 255, 0.22);
    border-radius: 6px;
    box-sizing: border-box;
}
/* Targets. Gold: where the picked card can go. Red: a card the full solo grid
   can lose for -2. Dashed blue: a card the solo turn can shift. */
.pzp-cell.lit { border-color: var(--accent); background: rgba(255, 209, 102, 0.14); cursor: pointer; }
.pzp-cell.lit .pzp-slot { border-color: rgba(255, 209, 102, 0.75); }
.pzp-cell.pzp-remove { border-color: #e0554a; background: rgba(179, 38, 30, 0.28); }
.pzp-cell.pzp-movable { cursor: pointer; }
.pzp-cell.pzp-movable > .card { outline: 2px dashed #7fb0ff; outline-offset: 2px; }
.pzp-cell.sel > .card { outline: 3px solid #d9b24a; outline-offset: 2px; }
.pzp-cell.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
/* A meld made this turn, still to score: a green ring round each of its cards. */
.pzp-cell.pzp-melded > .card { box-shadow: 0 0 0 3px #3fbf6a, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
/* A card the last turn scored, held in its cell until the next card is played
   (heldScore in def.ts): gold-ringed over the empty slot, and faded once a card
   is picked, so a lit cell under it still reads as the place to play. */
.pzp-cell > .card.pzp-heldcard {
    position: absolute;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    box-shadow: 0 0 0 3px #e8b730, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.pzp-board.pzp-picking .pzp-cell > .card.pzp-heldcard { opacity: 0.3; }
/* --- meld lines and chips -------------------------------------------------
   One color per meld type, shared by a line and the chip that names it. */
.pzp-mk-kind { --pzp-mk: #e8b730; }
.pzp-mk-flush { --pzp-mk: #4a90ff; }
.pzp-mk-straight { --pzp-mk: #3fcf72; }
.pzp-mk-unordered { --pzp-mk: #c3c3c3; }
.pzp-lines { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
.pzp-line {
    position: absolute;
    left: var(--x0);
    top: var(--y0);
    width: hypot(calc(var(--x1) - var(--x0)), calc(var(--y1) - var(--y0)));
    height: 0;
    transform-origin: 0 0;
    rotate: atan2(calc(var(--y1) - var(--y0)), calc(var(--x1) - var(--x0)));
}
.pzp-linebar {
    position: absolute;
    left: -8px;
    right: -8px;
    top: calc(var(--off) - 2.5px);
    height: 5px;
    border-radius: 3px;
    background: var(--pzp-mk);
    opacity: 0.92;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);
}
.pzp-mk-unordered > .pzp-linebar {
    background: repeating-linear-gradient(90deg, var(--pzp-mk) 0 8px, transparent 8px 12px);
    box-shadow: none;
}
/* A cell's center, from its display row and column. */
.pzp-boardlines > .pzp-line {
    --x0: calc(var(--pzp-padx) + var(--pzp-rowpadx) + var(--c0) * (var(--pzp-cellw) + var(--pzp-gap)) + var(--pzp-cellw) / 2);
    --x1: calc(var(--pzp-padx) + var(--pzp-rowpadx) + var(--c1) * (var(--pzp-cellw) + var(--pzp-gap)) + var(--pzp-cellw) / 2);
    --y0: calc(var(--pzp-pady) + var(--d0) * (var(--pzp-cellh) + 2 * var(--pzp-rowpady) + var(--pzp-rowgap))
        + var(--pzp-rowpady) + var(--pzp-cellh) / 2);
    --y1: calc(var(--pzp-pady) + var(--d1) * (var(--pzp-cellh) + 2 * var(--pzp-rowpady) + var(--pzp-rowgap))
        + var(--pzp-rowpady) + var(--pzp-cellh) / 2);
}
.pzp-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    height: 20px;
    padding: 0 7px 0 5px;
    border-left: 4px solid var(--pzp-mk, rgba(255, 255, 255, 0.35));
    border-radius: 4px 10px 10px 4px;
    background: rgba(0, 0, 0, 0.3);
    font-size: 12.5px;
    font-weight: 600;
    line-height: 20px;
    white-space: nowrap;
}
.pzp-chippts { font-weight: 700; font-variant-numeric: tabular-nums; }
.pzp-chipnote { font-size: 10.5px; font-style: italic; font-weight: 400; opacity: 0.75; }
.progress.pzp-progress {
    opacity: 1;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 4px 6px;
    min-height: 24px;
}
.pzp-proglead { opacity: 0.85; white-space: nowrap; }
.pzp-progtotal { font-weight: 700; white-space: nowrap; }
/* The card a Joker picker is placing, shown where it is going. */
.pzp-cell.pzp-pending { border-color: var(--accent); }
.pzp-ghostcard { opacity: 0.8; }
/* A Joker on the grid wears its name, or "wild". */
.pzp-jokername {
    position: absolute;
    left: 50%;
    bottom: 3px;
    translate: -50% 0;
    padding: 0 5px;
    border-radius: 7px;
    background: #f7f4ee;
    color: #1a1a1a;
    font-size: 12px;
    font-weight: 700;
    line-height: 16px;
    white-space: nowrap;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
    pointer-events: none;
}
.pzp-jokername.red { color: #c0182a; }
.pzp-jokername.red.diamond { color: #c8541a; }
.pzp-jokername.club { color: #1f4f8f; }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .pzp-jokername.red.diamond { color: var(--fc-diamond-ink); }
.deck-fourcolor .pzp-jokername.club { color: var(--fc-club-ink); }
.pzp-jokername.pzp-wild { font-style: italic; color: #5a3b8a; }
/* --- above the board: the players and the deck ---------------------------- */
.pzp-top {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 8px 14px;
    margin: 0 auto 8px;
    min-height: 40px;
}
.pzp-deck {
    color: inherit;
    font: inherit;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
    border-color: rgba(255, 255, 255, 0.22);
}
.pzp-deck:hover:not(:disabled), .pzp-deck[aria-expanded="true"] { border-color: var(--accent); }
.pzp-deck:disabled { cursor: default; }
.pzp-player, .pzp-deck {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 5px 10px;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.2);
    border: 2px solid transparent;
    white-space: nowrap;
}
.pzp-player.pzp-active { border-color: var(--accent); }
.pzp-toact { color: var(--accent); font-size: 11px; }
.pzp-toact.pzp-off { visibility: hidden; }
.pzp-pname { font-weight: 700; }
.pzp-score {
    min-width: 22px;
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    font-size: 17px;
}
.pzp-jokers, .pzp-handcount, .pzp-decklabel { font-size: 13px; opacity: 0.85; }
.pzp-jokers.pzp-spent { opacity: 0.5; text-decoration: line-through; }
.pzp-deckcount { font-weight: 700; font-variant-numeric: tabular-nums; min-width: 18px; text-align: right; }
/* The deck running out (deckWarning in def.ts): white, then yellow, then red. */
.pzp-deck.pzp-decklow { color: #ffd34d; border-color: rgba(255, 211, 77, 0.6); }
.pzp-deck.pzp-deckout { color: #ff7070; border-color: rgba(255, 112, 112, 0.75); background: rgba(255, 80, 80, 0.14); }
.pzp-deck.pzp-decklow .pzp-decklabel, .pzp-deck.pzp-deckout .pzp-decklabel { opacity: 1; }
.pzp-solotop .pzp-toact { display: none; }
/* --- the action row ------------------------------------------------------- */
.pzp-actions {
    display: flex;
    justify-content: center;
    gap: 8px;
    min-height: 40px;
    margin: 0 auto 6px;
}
.pzp-actions .btn.pzp-off { visibility: hidden; }
.pzp-actions[hidden] { display: none; }
.pzp-status.pzp-turn { color: var(--accent); }
/* --- the hand and the Joker picker ----------------------------------------
   The picker sits over the top bar and grows UP over the status and progress
   lines, never down: the grid under it and the hand below the grid stay in
   view while a Joker is named, and nothing moves when it opens. */
.pzp-topwrap { position: relative; }
.pzp-handpanel #hand-cards .card.pzp-firstjoker { margin-left: 10px; }
.pzp-handpanel #hand-cards .card.pzp-drawn { box-shadow: 0 0 0 3px #7fb0ff, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
.pzp-handpanel #hand-cards .card.pzp-placing { opacity: 0.35; }
.pzp-picker {
    position: absolute;
    left: 50%;
    bottom: 0;
    translate: -50% 0;
    width: max-content;
    max-width: 100%;
    min-height: 100%;
    margin: 0;
    z-index: 6;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
    padding: 8px 12px;
    box-sizing: border-box;
    background: var(--panel-bg, #1d2a24);
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);
}
.pzp-picker[hidden] { display: none; }
.pzp-pickertitle { font-weight: 700; text-align: center; }
.pzp-pickrow { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px; }
.pzp-pickrow .btn.small { min-width: 34px; }
.pzp-rank.sel { outline: 2px solid var(--accent); }
.pzp-picklast { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 4px 14px; }
.pzp-pickrow .btn.small.pzp-suit { font-size: 20px; min-width: 44px; line-height: 1.1; }
.pzp-suit.red { color: #ff7a7a; }
.pzp-suit.red.diamond { color: #ffa66b; }
.pzp-suit.club { color: #9cc4ff; }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .pzp-suit.red.diamond { color: var(--fc-diamond); }
.deck-fourcolor .pzp-suit.club { color: var(--fc-club); }
/* --- the deck viewer -------------------------------------------------------
   A dialog over the page, so opening it moves nothing. */
.pzp-deckview {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 12px;
    background: rgba(0, 0, 0, 0.55);
}
.pzp-deckview[hidden] { display: none; }
.pzp-dvpanel {
    max-width: 100%;
    max-height: calc(100vh - 24px);
    overflow: auto;
    padding: 12px 14px 14px;
    box-sizing: border-box;
    /* Opaque: the shared .panel fill is translucent, and the board would show through. */
    background: var(--panel-bg, #1d2a24);
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.55);
}
.pzp-dvhead { position: relative; display: flex; justify-content: center; align-items: center; min-height: 31px; }
.pzp-dvhead .pzp-dvtitle { margin: 0; text-align: center; }
.pzp-dvclose { position: absolute; right: 0; }
.pzp-dvwhere { margin: 4px 0 10px; font-size: 14px; text-align: center; }
.pzp-dvgrid {
    --pzp-dvcw: 30px;
    display: grid;
    grid-template-columns: 18px repeat(var(--cols), var(--pzp-dvcw));
    gap: 3px;
    justify-content: center;
    align-items: center;
}
.pzp-dvgrid > .card.logpic-card { --cw: var(--pzp-dvcw); }
.pzp-dvrank { font-size: 12px; font-weight: 700; text-align: center; opacity: 0.8; }
.pzp-dvsuit { font-size: 16px; text-align: center; }
.pzp-dvsuit.red { color: #ff7a7a; }
.pzp-dvsuit.red.diamond { color: #ffa66b; }
.pzp-dvsuit.club { color: #9cc4ff; }
/* Card colors: Four colors (src/shared/ui/deckcolors.ts). */
.deck-fourcolor .pzp-dvsuit.red.diamond { color: var(--fc-diamond); }
.deck-fourcolor .pzp-dvsuit.club { color: var(--fc-club); }
.pzp-dvseen {
    width: var(--pzp-dvcw);
    height: calc(var(--pzp-dvcw) * 89 / 64);
    border: 1px dashed rgba(255, 255, 255, 0.18);
    border-radius: 4px;
    box-sizing: border-box;
}
/* --- the log: numbered plays and a scored turn's picture ---------------------
   The picture sits in the turn's bold <summary>, so it resets the weight. Its
   cards are 20px, so they are on the simplified atlas (picCard). */
.pzp-logorder {
    display: inline-block;
    min-width: 14px;
    height: 14px;
    margin-right: 5px;
    border-radius: 7px;
    background: #f7f4ee;
    color: #1a1a1a;
    font-size: 10px;
    font-weight: 700;
    line-height: 14px;
    text-align: center;
}
.pzp-pic {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin: 5px 8px 2px 1em;
    font-size: 12px;
    font-weight: 400;
}
.pzp-picgrid {
    --pzp-pcw: 20px;
    --pzp-pch: calc(20px * 89 / 64);
    --pzp-pgap: 2px;
    position: relative;
    display: inline-grid;
    flex: none;
    grid-template-columns: repeat(3, var(--pzp-pcw));
    grid-auto-rows: var(--pzp-pch);
    gap: var(--pzp-pgap);
}
.pzp-piccell { position: relative; border-radius: 3px; }
.pzp-piccell.pzp-picmine { background: rgba(90, 140, 255, 0.32); }
.pzp-piccell.pzp-pictheirs { background: rgba(255, 110, 110, 0.26); }
.pzp-piccell.pzp-picshared { background: rgba(165, 90, 235, 0.4); }
.pzp-piccell.pzp-picsolo { background: rgba(255, 255, 255, 0.08); }
.pzp-piccell > .card.logpic-card { --cw: var(--pzp-pcw); }
.pzp-piccell > .card.logpic-lost { opacity: 0.4; }
.pzp-piccell > .card.pzp-picjoker { outline: 1px dashed #c9a7ff; outline-offset: 0; }
.pzp-picorder {
    position: absolute;
    top: -4px;
    left: -4px;
    z-index: 4;
    min-width: 11px;
    height: 12px;
    padding: 0 1px;
    border-radius: 6px;
    background: #f7f4ee;
    color: #1a1a1a;
    font-size: 9px;
    font-weight: 700;
    line-height: 12px;
    text-align: center;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);
}
.pzp-piclines > .pzp-line {
    --x0: calc(var(--c0) * (var(--pzp-pcw) + var(--pzp-pgap)) + var(--pzp-pcw) / 2);
    --x1: calc(var(--c1) * (var(--pzp-pcw) + var(--pzp-pgap)) + var(--pzp-pcw) / 2);
    --y0: calc(var(--d0) * (var(--pzp-pch) + var(--pzp-pgap)) + var(--pzp-pch) / 2);
    --y1: calc(var(--d1) * (var(--pzp-pch) + var(--pzp-pgap)) + var(--pzp-pch) / 2);
}
.pzp-piclines .pzp-linebar { left: -3px; right: -3px; top: calc(var(--off) / 2 - 1.5px); height: 3px; }
.pzp-piclegend { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; }
.pzp-piclegend .pzp-chip { height: 18px; font-size: 11.5px; line-height: 18px; }
/* The picture is a button that opens the turn viewer. */
.pzp-picbtn {
    display: block;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 6px;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: zoom-in;
}
.pzp-picbtn:hover { background: rgba(255, 255, 255, 0.07); }
.pzp-picbtn:focus-visible { outline: 2px solid var(--accent, #f0c040); outline-offset: 1px; }
/* --- the turn viewer -------------------------------------------------------
   The deck viewer's dialog (.pzp-deckview, .pzp-dvpanel), holding the whole
   grid at a readable size beside the plays and what scored. */
.pzp-tvpanel { width: 640px; }
.pzp-tvbody { display: flex; flex-wrap: wrap; justify-content: center; align-items: flex-start; gap: 14px 18px; margin-top: 8px; }
.pzp-tvleft { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.pzp-tvright { flex: 1 1 300px; min-width: 0; }
.pzp-pic.pzp-picbig { margin: 0; }
.pzp-picbig .pzp-picgrid { --pzp-pcw: 44px; --pzp-pch: calc(44px * 89 / 64); --pzp-pgap: 4px; }
.pzp-picbig .pzp-piccell { border-radius: 5px; }
.pzp-picbig .pzp-picorder { top: -6px; left: -6px; min-width: 16px; height: 16px; border-radius: 8px; font-size: 11px; line-height: 16px; }
.pzp-picbig .pzp-piclines .pzp-linebar { left: -6px; right: -6px; top: calc(var(--off) - 2px); height: 4px; }
.pzp-tvsides { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; font-size: 12.5px; }
.pzp-tvside { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.pzp-tvswatch { width: 14px; height: 14px; border-radius: 3px; }
.pzp-tvswatch.pzp-picmine { background: rgba(90, 140, 255, 0.55); }
.pzp-tvswatch.pzp-pictheirs { background: rgba(255, 110, 110, 0.5); }
.pzp-tvswatch.pzp-picshared { background: rgba(165, 90, 235, 0.65); }
.pzp-tvplays { font-size: 13.5px; }
.pzp-tvplays > .logline { padding: 2px 0; }
.pzp-tvscore { border-collapse: collapse; margin: 10px auto 0; font-variant-numeric: tabular-nums; }
.pzp-tvscore td { padding: 3px 6px; vertical-align: middle; }
.pzp-tvscore td.pzp-tvname { text-align: left; }
.pzp-tvscore td.pzp-tvmade { text-align: left; }
.pzp-tvscore td.pzp-tvpts { text-align: right; font-weight: 700; }
.pzp-tvcards { display: inline-flex; gap: 2px; vertical-align: middle; }
.pzp-tvcards > .card.logpic-card { --cw: 24px; }
.pzp-tvcards > .card.pzp-picjoker { outline: 1px dashed #c9a7ff; outline-offset: 0; }
.pzp-tvnote { margin: 8px 0 0; font-size: 13.5px; text-align: center; }
/* --- the setup screen ----------------------------------------------------- */
.segmented.pzp-na { opacity: 0.55; }
/* --- the result ------------------------------------------------------------ */
.pzp-result { border-collapse: collapse; margin: 6px auto 10px; font-variant-numeric: tabular-nums; }
.pzp-result th, .pzp-result td { padding: 4px 10px; }
.pzp-result th.pzp-name, .pzp-result td.pzp-name { text-align: left; }
.pzp-result th.pzp-num, .pzp-result td.pzp-num { text-align: right; }
.pzp-result tr.pzp-best td { font-weight: 700; color: var(--accent); }
.pzp-result thead th { border-bottom: 1px solid rgba(255, 255, 255, 0.25); }
@media (max-width: 720px) {
    .pzp-board { --pzp-cw: 52px; --pzp-gap: 5px; }
    .pzp-board.pzp-solo { --pzp-cw: 62px; }
}
@media (max-width: 480px) {
    /* The longest status and progress lines wrap to two here: reserve both. */
    .status.pzp-status { min-height: 48px; }
    .progress.pzp-progress { min-height: 48px; align-content: center; }
    .pzp-board { --pzp-cw: 46px; --pzp-gap: 4px; --pzp-padx: 4px; --pzp-pady: 6px; --pzp-rowgap: 3px; --pzp-rowpadx: 6px; }
    .pzp-board.pzp-solo { --pzp-cw: 62px; }
    .pzp-dvgrid { --pzp-dvcw: 22px; gap: 2px; }
    .pzp-picbig .pzp-picgrid { --pzp-pcw: 40px; --pzp-pch: calc(40px * 89 / 64); }
    .pzp-tvbody { margin-top: 4px; gap: 10px; }
    .pzp-cell { border-width: 1px; }
    .pzp-jokername { font-size: 11px; line-height: 14px; }
    .pzp-pickrow { gap: 3px; }
    .pzp-pickrow .btn.small.pzp-rank { min-width: 30px; padding-left: 4px; padding-right: 4px; }
    .pzp-player, .pzp-deck { padding: 4px 8px; gap: 5px; }
}
/* =========================================================================
 * Powercreep! (powercreep)
 *
 * Prefix policy: every game-specific class is `pcr-`. Shared classes are
 * reused as they are: cards are `.card`, hand cards select with `.card.sel`
 * and arm with `.clickable`, buttons are `.btn`, and the result panel reuses
 * `.winner-banner`, `.result-note`, and `.result-actions`.
 *
 * NOTHING MOVES. Every fighter sits in a square slot, so turning it to
 * defense moves nothing; every field reserves one fighter's height; the
 * discard column is as tall as the two seats beside it and scrolls inside
 * itself, whatever it holds (a cancel chain, or the whole pile fanned down);
 * and the status line, the control row, and the hint line each reserve their
 * tallest. Card sizes are custom properties on the board, so each reservation
 * follows the card it holds.
 * ========================================================================= */
.pcr-board {
    position: relative;
    display: grid;
    /* The seats need about 690px for six fighters in a row (a slot is about
       95px), so the discard column takes whatever the board has past that,
       up to 200px (four columns of the fanned pile), and never less than its
       narrow width. */
    grid-template-columns: clamp(var(--pcr-colw), calc(100% - 690px), 200px) minmax(0, 1fr);
    grid-template-rows: auto auto auto auto;
    gap: 10px;
    --pcr-fcw: 58px;   /* a fighter on the field */
    --pcr-scw: 50px;   /* a card in the discard column */
    --pcr-colw: calc(var(--pcr-scw) + 22px);
}
.pcr-side { grid-column: 2; min-width: 0; }
.pcr-board > .pcr-controls { grid-column: 2; margin: 0; }
.pcr-board > .pcr-note { grid-column: 2; }

/* --- the discard column ----------------------------------------------------- */
/* It spans both seats, and its contents sit in an absolutely placed scroller,
   so a long pile scrolls inside the column instead of stretching the board. */
.pcr-pilecol {
    grid-column: 1;
    grid-row: 1 / 5;
    position: relative;
    margin: 0;
    padding: 0;
    min-height: 0;
    --cw: var(--pcr-scw);
    /* The column's width is the grid's, never its content's, so its contents
       can lay themselves out for it (the wide layout at the end of the file). */
    container: pcrpile / inline-size;
}
.pcr-pileinner {
    position: absolute;
    inset: 0;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 10px 0 12px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.pcr-pilehead, .pcr-pilemeta { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.pcr-discardlabel { font-size: 12px; color: var(--muted); margin-top: 8px; }
.pcr-discardcount { font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; }
.pcr-browse { margin-top: 4px; }
.pcr-block {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    margin-top: 8px;
    padding: 4px 2px 5px;
    width: calc(100% - 8px);
    box-sizing: border-box;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.18);
    --cw: 34px;
}
.pcr-blocktitle { font-size: 11px; font-weight: 700; line-height: 1.2; text-align: center; }
.pcr-reveal .card.pcr-colcard + .card.pcr-colcard { margin-top: calc(var(--cw) * 89 / 64 * -0.45); }
.pcr-reveal .card.pcr-summoned { outline: 2px solid var(--accent); outline-offset: 0; }
.pcr-vs { font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums; }
.pcr-reportline { font-size: 10px; line-height: 1.2; text-align: center; color: var(--muted); }
.pcr-chain.pcr-chaindone .pcr-link .card { opacity: 0.8; }
.pcr-link {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    margin-bottom: 3px;
}
.pcr-linklabel { font-size: 11px; line-height: 1.2; color: var(--muted); text-align: center; }
.pcr-verdict {
    margin-top: 2px;
    padding: 2px 5px;
    border-radius: 7px;
    font-size: 11px;
    font-weight: 700;
    line-height: 1.2;
    text-align: center;
    background: #1d7a3a;
    color: #fff;
}
.pcr-verdict.pcr-verdict-canceled, .pcr-verdict.pcr-verdict-off { background: #b3261e; }
/* The whole pile, sorted, as two columns of small whole cards in the simplified
   art (rank over suit), each row tucked a little under the one above. At this
   size a strip of overlapping cards showed only the ranks. */
.pcr-fan {
    display: grid;
    grid-template-columns: repeat(2, var(--cw));
    column-gap: 2px;
    justify-content: center;
    margin-top: 10px;
    --cw: 32px;
}
.pcr-fan .card.pcr-fancard:nth-child(n+3) { margin-top: -7px; }
.pcr-fan .card.pcr-fancard.clickable:hover { z-index: 1; }

.card.pcr-dim { opacity: 0.45; }

/* --- the discard dialog ------------------------------------------------------ */
/* Inside the board and over all of it, so opening it moves nothing. */
.pcr-board .pcr-pilepop {
    position: absolute;
    inset: -1px;
    z-index: 20;
    box-sizing: border-box;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    padding: 10px;
    border-radius: 10px;
    background: rgba(10, 30, 20, 0.92);
}
.pcr-board .pcr-pilepop[hidden] { display: none; }
.pcr-board > .pcr-covered { visibility: hidden; }
.pcr-poppanel {
    margin: 0;
    width: 100%;
    max-height: 100%;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 8px;
    overflow: hidden;
}
.pcr-pophead { display: flex; align-items: center; justify-content: center; position: relative; min-height: 31px; }
.pcr-poptitle { margin: 0; text-align: center; }
.pcr-popclose { position: absolute; right: 0; }
.pcr-poplist {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px;
    overflow-y: auto;
    --cw: 50px;
}

/* --- the battle on screen ------------------------------------------------ */
.pcr-fighter.pcr-attacking .pcr-slot { border-radius: 10px; box-shadow: 0 0 0 3px #ff6b4a; }
.pcr-fighter.pcr-defending .pcr-slot { border-radius: 10px; box-shadow: 0 0 0 3px #ffd166; }
.pcr-life.pcr-lifetarget { border-radius: 8px; box-shadow: 0 0 0 3px #ffd166; }
.pcr-role {
    position: absolute;
    left: 50%;
    bottom: -2px;
    translate: -50% 0;
    z-index: 3;
    padding: 1px 6px;
    border-radius: 8px;
    font-size: 11px;
    font-weight: 800;
    white-space: nowrap;
    pointer-events: none;
    color: #1f2933;
    background: #ffd166;
}
.pcr-attacking .pcr-role { background: #ff6b4a; color: #fff; }
.pcr-arrow {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
    pointer-events: none;
    z-index: 15;
}
.pcr-arrowshaft {
    fill: none;
    stroke: #ff6b4a;
    stroke-width: 5;
    stroke-linecap: round;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
}
.pcr-arrowtip { fill: #ff6b4a; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6)); }

/* --- a modifier on its way ------------------------------------------------ */
/* Over the board beside its target, so showing it moves nothing. Invisible
   until drawArrow has placed it, a frame after the repaint. */
.pcr-board > .pcr-incoming {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 16;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 4px;
    border-radius: 10px;
    background: rgba(20, 16, 40, 0.88);
    box-shadow: 0 0 0 3px #b18cff, 0 4px 14px rgba(0, 0, 0, 0.55);
    pointer-events: none;
    visibility: hidden;
    --cw: 44px;
}
.pcr-board > .pcr-incoming[hidden] { display: none; }
.pcr-board > .pcr-incoming.pcr-placed { visibility: visible; animation: pcr-incoming 0.6s ease-in-out infinite alternate; }
@keyframes pcr-incoming {
    from { box-shadow: 0 0 0 3px #b18cff, 0 4px 14px rgba(0, 0, 0, 0.55); }
    to { box-shadow: 0 0 0 5px #d9c4ff, 0 4px 14px rgba(0, 0, 0, 0.55); }
}
.pcr-incap {
    padding: 1px 6px;
    border-radius: 7px;
    font-size: 12px;
    font-weight: 800;
    line-height: 1.2;
    white-space: nowrap;
    color: #fff;
    background: #5b4a8a;
}
.pcr-incap.pcr-up { background: #1d7a3a; }
.pcr-incap.pcr-down { background: #b3261e; }
.pcr-modarrowshaft {
    fill: none;
    stroke: #b18cff;
    stroke-width: 4;
    stroke-linecap: round;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
}
.pcr-modarrowtip { fill: #b18cff; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6)); }
.pcr-fighter.pcr-modtarget .pcr-slot { border-radius: 10px; box-shadow: 0 0 0 3px #b18cff; }
.pcr-modchip.pcr-modtarget { box-shadow: 0 0 0 2px #b18cff; }
/* The other player's modifier that just took effect. */
.pcr-modchip.pcr-modnew { box-shadow: 0 0 0 2px #fff; animation: pcr-modnew 0.5s ease-in-out 3 alternate; }
@keyframes pcr-modnew {
    from { transform: scale(1); }
    to { transform: scale(1.25); }
}
@media (prefers-reduced-motion: reduce) {
    .pcr-board > .pcr-incoming.pcr-placed, .pcr-modchip.pcr-modnew { animation: none; }
}

/* --- a seat --------------------------------------------------------------- */
.pcr-seat {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 0;
}
.pcr-seat.pcr-active { box-shadow: 0 0 0 2px var(--accent); }
.pcr-seathead {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 6px 14px;
    min-height: 34px;
}
.pcr-seatname { font-weight: 700; white-space: nowrap; }
.pcr-life {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
}
.pcr-life .card.pcr-lifecard { --cw: 22px; }
.pcr-lifecount { font-weight: 700; font-variant-numeric: tabular-nums; }
.pcr-handcount { color: var(--muted); white-space: nowrap; }

/* --- a field and its fighters ---------------------------------------------- */
.pcr-field {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    gap: 6px 10px;
    /* One fighter: the slot plus its modifier row. */
    min-height: calc(var(--pcr-fcw) * 89 / 64 + 14px + 22px);
}
.pcr-nofield {
    align-self: center;
    color: var(--muted);
    font-style: italic;
}
.pcr-fighter {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    --cw: var(--pcr-fcw);
}
/* Square, as tall as an upright card plus room for a stack's fan, so a
   fighter turned sideways takes exactly the space it took standing. */
.pcr-slot {
    position: relative;
    width: calc(var(--pcr-fcw) * 89 / 64 + 14px);
    height: calc(var(--pcr-fcw) * 89 / 64 + 14px);
    display: flex;
    align-items: center;
    justify-content: center;
}
.pcr-pile {
    position: relative;
    width: var(--pcr-fcw);
    height: calc(var(--pcr-fcw) * 89 / 64);
}
.pcr-pile .card.pcr-fcard {
    position: absolute;
    left: calc((var(--pcr-i) - (var(--pcr-n, 1) - 1) / 2) * 7px);
    top: calc((var(--pcr-i) - (var(--pcr-n, 1) - 1) / 2) * 7px);
}
.pcr-pile-2 { --pcr-n: 2; }
.pcr-pile-3 { --pcr-n: 3; }
.pcr-pile-4 { --pcr-n: 4; }
/* Defense position: the whole pile lies sideways. `rotate`, never
   `transform`, so a pop-in or a hover lift cannot stand it back up
   (ui-conventions.md, "A sideways card turns with rotate"). */
.pcr-fighter.pcr-def .pcr-pile { rotate: 90deg; }
.pcr-fighter.pcr-spent .pcr-pile { opacity: 0.6; }
.pcr-fighter.pcr-picked .pcr-slot {
    border-radius: 10px;
    box-shadow: 0 0 0 2px var(--accent);
}
/* A pay summon's fighters: dotted while one could still be picked to pay,
   solid once picked. An outline, so it takes no space. */
.pcr-fighter.pcr-payable .pcr-slot { border-radius: 10px; outline: 3px dotted #ffd166; outline-offset: 1px; }
.pcr-fighter.pcr-paying .pcr-slot { border-radius: 10px; outline: 3px solid #ffd166; outline-offset: 1px; }
.pcr-value {
    position: absolute;
    top: 0;
    right: 0;
    z-index: 3;
    min-width: 26px;
    height: 26px;
    padding: 0 4px;
    border-radius: 13px;
    background: #1f2933;
    color: #fff;
    border: 2px solid #fff;
    font-weight: 800;
    font-size: 14px;
    line-height: 22px;
    text-align: center;
    font-variant-numeric: tabular-nums;
    box-sizing: border-box;
    pointer-events: none;
}
.pcr-value.pcr-up { background: #1d7a3a; }
.pcr-value.pcr-down { background: #b3261e; }
.pcr-tag {
    align-self: center;
    padding: 1px 6px;
    border-radius: 8px;
    background: #3b3b3b;
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    white-space: nowrap;
}
.pcr-tag.pcr-bomb { color: #ffd166; }
/* The change a picked J, Q, or K would make, on each fighter it can go on. */
.pcr-preview {
    position: absolute;
    bottom: 0;
    right: 0;
    z-index: 3;
    padding: 1px 6px;
    border-radius: 9px;
    color: #fff;
    font-weight: 800;
    font-size: 13px;
    pointer-events: none;
}
.pcr-preview.pcr-up { background: #1d7a3a; }
.pcr-preview.pcr-down { background: #b3261e; }
.pcr-mods {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: 3px;
    min-height: 18px;
    /* A tag and two chips on one line, the most a fighter shows (three or more
       modifiers are one chip). */
    max-width: 130px;
}
/* A J, Q, or K on a fighter: only the change it made. */
.pcr-modchip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    height: 18px;
    padding: 0 5px;
    border: 0;
    border-radius: 9px;
    color: #fff;
    font: inherit;
    font-size: 12px;
    font-weight: 800;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    box-sizing: border-box;
}
.pcr-modchip.pcr-up { background: #1d7a3a; }
.pcr-modchip.pcr-down { background: #b3261e; }
button.pcr-modchip { cursor: pointer; }
button.pcr-modchip.pcr-target { box-shadow: 0 0 0 2px var(--accent); }
.card.pcr-target { box-shadow: 0 0 0 2px var(--accent), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
/* A battle you can make that gains you nothing: offered, but ringed red. */
.card.pcr-risky { box-shadow: 0 0 0 3px #e5484d, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
.btn.pcr-risky { box-shadow: 0 0 0 2px #e5484d; }
/* A fighter a trip or a run can summon for free: a teal ring and a tag
   across its top edge, placed over the card so nothing moves. */
.card.pcr-free { box-shadow: 0 0 0 3px #2bb5a0, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
.pcr-freetag {
    position: absolute;
    left: 50%;
    top: calc(var(--cw) * -0.11);
    translate: -50% 0;
    z-index: 2;
    padding: 0 6px;
    border-radius: 999px;
    border: 2px solid #fff;
    background: #1b8a7a;
    color: #fff;
    font-size: clamp(10px, calc(var(--cw) * 0.17), 13px);
    font-weight: 800;
    line-height: 1.35;
    white-space: nowrap;
    pointer-events: none;
}
.card-fly .pcr-freetag { display: none; }
/* A hand card that can't be played on this move, lightly faded. */
.card.pcr-unplayable { opacity: 0.55; filter: saturate(0.7); }

/* --- the marks on cards ------------------------------------------------------ */
/* Two classes, so it out-specifies `.card-fly`'s position on a flight ghost;
   `pcr-badged` is in the def's ghostStripClasses for the same reason. */
.card.pcr-badged { position: relative; overflow: visible; }
.pcr-mark {
    position: absolute;
    left: 50%;
    bottom: calc(var(--cw) * -0.12);
    translate: -50% 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    min-width: clamp(18px, calc(var(--cw) * 0.36), 30px);
    height: clamp(18px, calc(var(--cw) * 0.36), 30px);
    padding: 0 4px;
    border-radius: 999px;
    border: 2px solid #fff;
    background: #1f2933;
    color: #fff;
    font-weight: 800;
    font-size: clamp(10px, calc(var(--cw) * 0.19), 15px);
    line-height: 1;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    pointer-events: none;
}
.pcr-mark .pcr-icon {
    width: clamp(13px, calc(var(--cw) * 0.29), 22px);
    height: clamp(13px, calc(var(--cw) * 0.29), 22px);
}
.pcr-mark.pcr-mark-icon { min-width: clamp(22px, calc(var(--cw) * 0.44), 36px); }
.pcr-mark-bullet { background: #8a5a12; }
.pcr-mark-shovel { background: #4b5b2a; }
.pcr-mark-buff { background: #4a3b8f; }
/* A fighter's pay cost: a plain circle. */
.pcr-mark-cost { background: #fff; color: #1f2933; border-color: #1f2933; min-width: 0;
    width: clamp(18px, calc(var(--cw) * 0.3), 26px); height: clamp(18px, calc(var(--cw) * 0.3), 26px); padding: 0; }
/* Room under the hand for the marks that hang below the cards. #hand-cards is
   a shared id, so the rule is scoped through the game's panel class. */
.pcr-handpanel #hand-cards { padding-bottom: 12px; row-gap: 16px; }

/* --- status, controls, note, result ---------------------------------------- */
/* The status line runs to 2 lines at its longest (a cancel prompt), so it
   holds 2 and the board below never steps down. */
.status.pcr-status { line-height: 1.35; min-height: calc(2 * 1.35em); }
.pcr-controls { min-height: 38px; justify-content: center; flex-wrap: wrap; }
.pcr-note { min-height: 1.4em; text-align: center; color: var(--muted); }
.pcr-result {
    border-collapse: collapse;
    margin: 0 auto;
    font-variant-numeric: tabular-nums;
}
.pcr-result th, .pcr-result td { padding: 4px 10px; text-align: right; }
.pcr-result th.pcr-resultname, .pcr-result tr:first-child th:first-child { text-align: left; }
.pcr-result tr.pcr-winner { font-weight: 700; }

/* --- the Card abilities list on the page ----------------------------------- */
.pcr-ref { list-style: none; padding-left: 0; }
.pcr-ref li { margin: 6px 0; }
.pcr-refmark { background: #1f2933; color: #fff; border-color: #1f2933; }
.pcr-refmark .pcr-icon { width: 1.1em; height: 1.1em; }

/* --- the discard column, wide (desktop) -------------------------------------- */
/* With room to spare, the blocks lay their cards out in a row instead of a
   tucked stack, and the pile fans in as many columns of bigger cards as fit. */
@container pcrpile (min-width: 150px) {
    .pcr-pileinner { padding: 10px 8px 12px; }
    /* The top card beside its label, count, and Expand. */
    .pcr-pilehead { flex-direction: row; justify-content: center; gap: 12px; }
    .pcr-pilemeta .pcr-discardlabel { margin-top: 0; }
    .pcr-block {
        flex-direction: row;
        flex-wrap: wrap;
        justify-content: center;
        align-items: center;
        gap: 4px 6px;
        padding: 6px 6px 7px;
        --cw: 40px;
    }
    .pcr-block > .pcr-blocktitle, .pcr-block > .pcr-reportline { flex-basis: 100%; }
    .pcr-blocktitle { font-size: 12px; }
    .pcr-reportline, .pcr-linklabel { font-size: 11px; }
    .pcr-reveal .card.pcr-colcard + .card.pcr-colcard { margin-top: 0; }
    .pcr-link { margin-bottom: 0; }
    .pcr-fan {
        grid-template-columns: repeat(auto-fill, var(--cw));
        column-gap: 3px;
        width: 100%;
        padding-top: 9px;
        --cw: 42px;
    }
    .pcr-fan .card.pcr-fancard:nth-child(n) { margin-top: -9px; }
}

@media (max-width: 560px) {
    .pcr-board {
        --pcr-fcw: 42px;
        --pcr-scw: 38px;
        --pcr-colw: calc(var(--pcr-scw) + 14px);
        gap: 8px;
    }
    .pcr-linklabel, .pcr-verdict { font-size: 10px; }
    .pcr-block { --cw: 30px; }
    .pcr-blocktitle, .pcr-vs { font-size: 10px; }
    .pcr-poplist { --cw: 40px; gap: 4px; }
    .pcr-fan { grid-template-columns: var(--cw); --cw: 34px; }
    .pcr-fan .card.pcr-fancard:nth-child(n+2) { margin-top: -9px; }
    .pcr-field { gap: 4px 6px; min-height: calc(var(--pcr-fcw) * 89 / 64 + 14px + 43px); }
    /* Narrower here, so a tag and two chips can take two lines: both are
       reserved, so nothing moves when the second fills. */
    .pcr-mods { max-width: calc(var(--pcr-fcw) * 89 / 64 + 30px); min-height: 39px; align-content: flex-start; }
    .status.pcr-status { font-size: 15px; line-height: 1.3; min-height: calc(3 * 1.3em); }
    /* A picked fighter offers up to three buttons, which take two rows here. */
    .pcr-controls { min-height: 86px; align-content: flex-start; }
    .pcr-value { min-width: 22px; height: 22px; line-height: 18px; font-size: 12px; }
}
/* =========================================================================
   Leapfrog!
   The pond (eight lily pads in two rows of four, as the track cards are laid
   out) beside the suit hierarchy, then the four-card trick between the two
   players' strips. Every box has a fixed size, so nothing above the hand
   moves: the pads and tokens are sized by --lf-pad, the trick always paints
   its four slots and reserves the line under it, and the power and route
   buttons keep their column inside the hand panel whether or not it holds a
   decision.
   ========================================================================= */

/* --- the status line and the scoreboard ----------------------------------- */
.status.lf-status { min-height: 2.9em; }
@media (max-width: 620px) {
    .status.lf-status { min-height: 4.4em; }
}
/* The shared scoreboard widget's shape (platform.css lists its opt-ins),
   restated here so this game's styling stays in its own file. */
.progress.lf-scoreboard {
    opacity: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px 16px;
    min-height: 34px;
}
.lf-scorechip {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
    white-space: nowrap;
}
.lf-scorename { font-weight: 600; }
.lf-scorechip::before {
    content: '';
    width: 10px;
    height: 10px;
    border-radius: 50%;
    align-self: center;
    background: var(--lf-frog);
}
.lf-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }

/* The two frogs' colors, used by the tokens, the chips, and the strips. */
.lf-seat0 { --lf-frog: #ffd166; }
.lf-seat1 { --lf-frog: #f48fb1; }

/* --- the board ------------------------------------------------------------ */
.lf-board { margin-bottom: 12px; }
.lf-main {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 14px 22px;
    padding: 12px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
}
.lf-lake {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    --lf-pad: 64px;
}
/* On a phone the pond and the hierarchy share one row: 4 pads, the pond's
   padding, the gap, and the hierarchy's ~104px have to fit inside the board's
   padding (342px at a 390px screen before this, when the pond overhung the
   board's left edge). */
@media (max-width: 620px) {
    .lf-main { padding: 10px 8px; }
    .lf-lake { --lf-pad: 46px; gap: 8px; }
    /* Scoped under the lake: the base rules for these come later in the file. */
    .lf-lake .lf-pond { gap: 8px 6px; padding: 8px 9px; }
    .lf-lake .lf-hier { min-width: 100px; }
    .lf-lake .lf-hierrow { padding: 4px 8px 4px 6px; gap: 5px; }
}
@media (max-width: 360px) {
    .lf-lake { --lf-pad: 40px; }
}
@media (max-width: 340px) {
    .lf-lake { --lf-pad: 36px; gap: 6px; }
    .lf-lake .lf-pond { gap: 8px 4px; padding: 8px 7px; }
    .lf-lake .lf-hier { min-width: 0; }
    .lf-lake .lf-hiername { font-size: 12px; }
    .lf-lake .lf-hierglyph { font-size: 17px; }
    .lf-lake .lf-lefttitle { font-size: 10px; }
}

/* The pond: the blue of the track cards, two rows of four pads. */
.lf-pond {
    display: grid;
    grid-template-columns: repeat(4, var(--lf-pad));
    grid-template-rows: repeat(2, calc(var(--lf-pad) + 18px));
    gap: 10px 8px;
    padding: 10px 12px;
    border-radius: 14px;
    background: linear-gradient(180deg, #3d8fe0 0%, #2a5fd0 100%);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
}

.lf-pad {
    position: relative;
    width: var(--lf-pad);
    height: var(--lf-pad);
    border-radius: 50%;
    background: radial-gradient(circle at 50% 40%, #c8f5b0 0%, #6fd456 55%, #45b33a 100%);
    display: flex;
    align-items: center;
    justify-content: center;
}
/* The notch every lily pad has. */
.lf-pad::after {
    content: '';
    position: absolute;
    right: 6%;
    bottom: 12%;
    width: 22%;
    height: 22%;
    background: #2f6fd8;
    transform: rotate(45deg);
    border-radius: 2px;
}
.lf-pad.lf-water {
    background:
        repeating-linear-gradient(170deg, rgba(20, 60, 160, 0.95) 0 5px, rgba(30, 90, 210, 0.9) 5px 10px);
}
.lf-pad.lf-water::after { display: none; }
.lf-padnum {
    font-size: calc(var(--lf-pad) * 0.46);
    font-weight: 900;
    color: #23651c;
    line-height: 1;
    z-index: 1;
}
.lf-pad.lf-water .lf-padnum { color: #0e2f73; }
/* The arrow each track card prints beside its pads: right along the top,
   left along the bottom. */
.lf-arrow {
    position: absolute;
    top: 50%;
    font-size: 11px;
    color: rgba(255, 255, 255, 0.92);
    transform: translateY(-50%);
    pointer-events: none;
}
.lf-pad.lf-top .lf-arrow { left: -9px; }
.lf-pad.lf-bottom .lf-arrow { right: -9px; }
/* The two corners are entered from the other row: the 6 from the 2 below it,
   the 0 from the 5 above it. Their arrows sit centered between the rows, under
   the tokens that hang off the top row's pads. */
.lf-pad.lf-bottom .lf-arrow.lf-arrow-v {
    top: -15px;
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    font-size: 12px;
    line-height: 1;
}
/* The viewer's next pad: where a trick won would put their frog. */
.lf-pad.lf-nexthop {
    outline: 2px dotted rgba(255, 255, 255, 0.95);
    outline-offset: 3px;
}
/* Tokens sit along the bottom edge of their pad, overlapping it. */
.lf-padtokens {
    position: absolute;
    left: 50%;
    bottom: -12px;
    transform: translateX(-50%);
    display: flex;
    gap: 2px;
    z-index: 2;
}
.lf-token {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;
    height: 20px;
    padding: 0 5px;
    border-radius: 10px;
    font-size: 11px;
    font-weight: 800;
    line-height: 1;
    white-space: nowrap;
    color: #1b1b1b;
    border: 1.5px solid rgba(0, 0, 0, 0.55);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
}
.lf-token-frog { background: var(--lf-frog); max-width: calc(var(--lf-pad) + 6px); overflow: hidden; }
.lf-token-king {
    background: #fff4c2;
    border-color: #b8860b;
    color: #8a5a00;
    font-size: 14px;
}

/* The suit hierarchy: strongest on top. */
.lf-hier {
    display: flex;
    flex-direction: column;
    gap: 5px;
    min-width: 108px;
}
.lf-hiertitle {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.7;
    text-align: center;
    white-space: nowrap;
}
/* Each suit as a pale card with the suit in ink, the way the sheet's suit
   hierarchy cards print them: three inks that cannot be confused, where the
   site's text colors make Spades and Clubs nearly the same. */
.lf-hierrow {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 4px 10px 4px 8px;
    border-radius: 8px;
    background: #f4f1e8;
    color: #1b1b1b;
    border: 2px solid transparent;
    white-space: nowrap;
}
.lf-hierrow.lf-strongest {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}
.lf-hierpos {
    font-size: 12px;
    font-weight: 800;
    opacity: 0.55;
    font-variant-numeric: tabular-nums;
}
.lf-hierglyph { font-size: 20px; line-height: 1; }
.lf-hiername { font-size: 14px; font-weight: 700; }
.lf-suit1 { color: #1b1b1b; }
.lf-suit2 { color: #c0392b; }
.lf-suit3 { color: #1e7a34; }
.lf-hierrow.lf-leapt { animation: hintpulse 0.6s ease-in-out 3 alternate; }
/* Tricks still to play this round, under the hierarchy and beside the pads it
   is counted against. */
.lf-left {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    margin-top: 4px;
    padding: 4px 10px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.28);
    border: 1px solid var(--panel-border);
    white-space: nowrap;
}
.lf-lefttitle {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.75;
}
.lf-leftnum { font-size: 18px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1; }

/* --- the table: strips and the trick -------------------------------------- */
.lf-table {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}
.lf-strip { padding: 2px 6px; }
.lf-striphead {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
}
.lf-stripname {
    font-size: 13px;
    font-weight: 700;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
}
.lf-stripname::before {
    content: '';
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--lf-frog);
}
.lf-powers {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
}
.lf-powerslabel {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.65;
    margin-right: 2px;
}
.lf-powerpip {
    width: 12px;
    height: 16px;
    border-radius: 3px;
    border: 1.5px dashed rgba(255, 255, 255, 0.55);
}
/* A spent power: a card back, as the sheet keeps the face-down card in front
   of its player. */
.lf-powerpip.lf-spent {
    border: 1.5px solid rgba(0, 0, 0, 0.4);
    background: linear-gradient(135deg, #4a5fbf, #2d3f8f);
}

.lf-trickrow {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 12px;
    padding: 14px 0;
}
.lf-trickcell { display: flex; flex-direction: column; align-items: center; }
.lf-trickcell.lf-theirs { transform: translateY(-11px); }
.lf-trickcell.lf-mine { transform: translateY(11px); }
.lf-trickcell .card { --cw: 72px; flex: none; }
@media (max-width: 620px) {
    .lf-trickrow { gap: 8px; }
    .lf-trickcell .card { --cw: 58px; }
}
.lf-trickcell.lf-next .card.empty {
    outline: 2px dashed rgba(255, 209, 102, 0.6);
    outline-offset: 2px;
}
.lf-trickcell .card.lf-facedown { position: relative; }
.lf-powertag {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    padding: 2px 6px;
    border-radius: 6px;
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.03em;
    white-space: nowrap;
    background: rgba(20, 70, 20, 0.92);
    color: #c8f5b0;
    border: 1px solid #7ed957;
    pointer-events: none;
}
.card.lf-winning { outline: 3px solid var(--accent); outline-offset: 2px; }
/* The last trick's result, under Tricks left. Exactly two lines, always: it
   wraps inside the column without widening it (width 0, min-width 100%), and
   a long name is clamped rather than adding a third line. */
.lf-lasttrick {
    width: 0;
    min-width: 100%;
    margin-top: 2px;
    text-align: center;
    font-size: 12px;
    line-height: 1.3;
    opacity: 0.85;
    height: 2.6em;
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
}

/* --- the hand panel and its action column --------------------------------- */
.lf-handrow {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    min-height: calc(var(--cw) * 89 / 64);
}
.lf-handrow #hand-cards { flex: 1 1 auto; }
.lf-actions {
    flex: 0 0 176px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    min-height: 104px;
}
.lf-actions .btn {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    line-height: 1.2;
}
.lf-powercap { font-size: 11px; font-weight: 500; opacity: 0.8; white-space: nowrap; }
.lf-actions .btn.lf-armed {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}
.lf-actions .btn.hinted {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    animation: hintpulse 0.9s ease-in-out infinite alternate;
}
@media (max-width: 760px) {
    .lf-handrow { flex-wrap: wrap; }
    /* Above the cards, so the buttons stay put while the hand box shrinks
       (reserveHandSpace: false in def.ts). */
    .lf-actions { flex: 1 1 100%; flex-direction: row; min-height: 52px; order: -1; }
    .lf-actions .btn { flex: 1 1 0; }
}
/* A card that cannot be played right now fades; with a power pressed every
   card can go face down, and they wear a dashed ring to say so. */
.lf-handpanel #hand-cards .card.lf-faded { opacity: 0.35; filter: saturate(0.55); }
.lf-handpanel #hand-cards .card.lf-powerpick {
    outline: 2px dashed rgba(126, 217, 87, 0.85);
    outline-offset: 2px;
}

/* --- the cards the viewer has not seen ------------------------------------- */
/* Three suit rows of ten, a face for each unseen card and a gap for the rest,
   so the box keeps its size all round. Simplified atlas at 24px (20px on a
   phone): read-only and small (ui-conventions, "Small cards use the
   simplified sprite"). */
.lf-unseen { --lf-ucw: 24px; }
@media (max-width: 420px) {
    .lf-unseen { --lf-ucw: 20px; }
}
.lf-unseenhead {
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 10px;
    margin-bottom: 6px;
}
.lf-unseentitle { margin: 0; white-space: nowrap; }
.lf-unseencount {
    font-size: 14px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    opacity: 0.8;
}
.lf-unseengrid {
    display: grid;
    grid-template-columns: 1.4em repeat(10, var(--lf-ucw));
    gap: 3px;
    align-items: center;
    width: max-content;
    margin-inline: auto;
}
.lf-unseengrid .card.mini { --cw: var(--lf-ucw); }
.lf-unseensuit {
    font-size: 16px;
    text-align: center;
    padding: 1px 0;
    border-radius: 4px;
    background: #f4f1e8;
}
/* A suit the other player has shown out of: its unseen cards are face down or
   undealt, never in their hand. The cards fade, and the suit is crossed out
   with two bars (a shape, not a color, as Let Me Off marks a void). */
.lf-unseengrid .card.lf-unseenout { opacity: 0.35; filter: saturate(0.5); }
.lf-unseensuit.lf-void { position: relative; }
.lf-unseensuit.lf-void::before,
.lf-unseensuit.lf-void::after {
    content: '';
    position: absolute;
    left: 1px;
    right: 1px;
    top: 50%;
    height: 2px;
    border-radius: 2px;
    background: #c0392b;
}
.lf-unseensuit.lf-void::before { transform: rotate(45deg); }
.lf-unseensuit.lf-void::after { transform: rotate(-45deg); }
.lf-unseengrid .card.lf-unseengap {
    background-image: none;
    background-color: rgba(255, 255, 255, 0.05);
    box-shadow: none;
    border: 0;
}

/* --- the log --------------------------------------------------------------- */
.logline.lf-logmove { opacity: 0.75; }
/* A trick: its caption, its four cards in two rows (logPicture), then a line
   for each power and one for the winner's move. */
#log-entries .logline.lf-logtrick { padding: 4px 0; }
.lf-logtrick .logpic.lf-logpic { margin: 3px 0 3px 1em; }
.lf-logtrick-note { font-size: 12px; opacity: 0.85; padding-left: 1em; }
.lf-logtrick-note.lf-logtrick-move { opacity: 0.7; }
#log-entries .logline.lf-livetrick { opacity: 0.9; }

/* --- the round summary ------------------------------------------------------ */
.panel.lf-summary {
    max-width: 380px;
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}
.panel.lf-summary .result-actions { justify-content: center; flex-wrap: wrap; }
.panel.lf-summary .result-note { text-align: center; }
table.lf-result {
    border-collapse: collapse;
    margin: 0 auto 8px;
    font-variant-numeric: tabular-nums;
}
table.lf-result th,
table.lf-result td { padding: 3px 12px; }
table.lf-result .lf-reslabel { text-align: left; font-weight: 600; white-space: nowrap; }
table.lf-result .lf-rescell { text-align: right; min-width: 3.6em; }
table.lf-result tr.lf-totals td { font-weight: 800; border-top: 1px solid var(--panel-border); }
/* =========================================================================
 * Against the Clock (againsttheclock)
 *
 * Prefix policy: every game-specific class is `agc-`. Shared classes are
 * reused as-is: cards are `.card` (`.simple` for the small read-only ones,
 * ui-conventions.md "Small cards use the simplified sprite"), buttons `.btn`,
 * the hint mark `.hinted`, and the result panel `.winner-banner` /
 * `.result-note` / `.result-actions`.
 *
 * The board is a size container (`agc`, on .agc-board). Under 860px the Clock,
 * the track, and the controls stack. From 860px the track takes the left and
 * the Clock and the controls a 300px column on the right, with the last and
 * the current sector in the track's infield. Every box reserves its tallest
 * ordinary shape, so nothing moves under the pointer between moves.
 * ========================================================================= */

/* --- the status line: its tallest line count, so the board never moves --- */
.status.agc-status { min-height: 26px; }

/* --- the layout ---------------------------------------------------------- */
.agc-board { container: agc / inline-size; }
.agc-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "dash" "stage" "controls";
    justify-items: center;
    gap: 10px;
    margin: 4px 0 12px;
}
.agc-dash { grid-area: dash; }
.agc-stage { grid-area: stage; }
.agc-controls { grid-area: controls; }
.agc-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    opacity: 0.7;
    white-space: nowrap;
}

/* --- the Clock and the damage ------------------------------------------- */
.agc-dash {
    display: flex;
    justify-content: center;
    align-items: stretch;
    gap: 10px;
}
.agc-clock, .agc-damage {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 6px 14px 6px;
    border-radius: 10px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
}
.agc-score {
    font-size: 30px;
    font-weight: 800;
    line-height: 1.05;
    font-variant-numeric: tabular-nums;
}
.agc-score.ahead { color: #6fcf97; }
.agc-score.behind { color: #e8a33d; }
.agc-tally {
    font-size: 13px;
    line-height: 1.3;
    text-align: center;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
/* Always painted (a no-break space when there is nothing to say), so the Clock
   keeps one height whether or not the warning shows. */
.agc-warn {
    min-height: 1.3em;
    font-size: 12px;
    font-weight: 700;
    color: #e05d5d;
    white-space: nowrap;
}
/* The Jokers on FULL art (the owner's ruling), so sized where it reads. */
.agc-jokers { --cw: 40px; display: flex; gap: 5px; margin-top: 4px; }

/* --- the track: one stage, A to B ---------------------------------------- */
/* The rules' setup picture: a hairpin open on the left. Rows 1 and 3 hold the
   two straights and row 2 the infield, where the sector panel sits in the
   middle columns (--agc-span, set per stage). The sectors are absolutely
   placed, evenly along the route and a little off it (def.ts layoutTrack,
   from --agc-w), so they take no room and move nothing; the padding takes
   the outward nudges. A shorter stage spreads the same track rather than
   shrinking it. */
.agc-stage {
    --cw: 34px;
    --agc-trackw: min(calc(100cqi - 8px), 460px);
    --agc-pad: 6px;
    --agc-wobble: calc(var(--cw) * 0.2);
    /* A card, and the mark under it. */
    --agc-rowh: calc(var(--cw) * 89 / 64 + 22px);
    position: relative;
    box-sizing: border-box;
    width: var(--agc-trackw);
    padding: calc(var(--agc-pad) + var(--agc-wobble)) var(--agc-pad);
    display: grid;
    grid-template-columns: repeat(var(--agc-span, 8), minmax(0, 1fr));
    grid-template-rows: var(--agc-rowh) auto var(--agc-rowh);
}
.agc-track { display: contents; }
/* The road, drawn through the middle of every card once the board is laid
   out (def.ts drawRoad), so the cards sit on it: the tarmac, a dashed lane
   line along its middle, a white start line, and a chequered finish line. It
   covers the stage and takes no room in the grid. */
.agc-road {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
    pointer-events: none;
    z-index: 0;
}
.agc-tarmac { fill: none; stroke: rgba(0, 0, 0, 0.28); }
.agc-lane { fill: none; stroke: rgba(255, 255, 255, 0.2); stroke-width: 2; }
.agc-line.start { fill: #f2f2f2; }
.agc-line .light { fill: #f2f2f2; }
.agc-line .dark { fill: #1b1b1b; }
.agc-slot {
    position: absolute;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
}
.agc-slot.now .card { outline: 3px solid var(--accent); outline-offset: 2px; }
/* Every slot reserves its mark's line, so a sector being passed adds nothing. */
.agc-mark {
    min-height: 1.3em;
    font-size: 12px;
    font-weight: 700;
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    padding: 0 3px;
    border-radius: 4px;
}
.agc-mark:not(:empty) { background: rgba(10, 25, 18, 0.65); }
.agc-mark.gain { color: #6fcf97; }
.agc-mark.even { color: rgba(255, 255, 255, 0.75); }
.agc-mark.loss { color: #e8a33d; }
.agc-mark.bust { color: #e05d5d; }
.agc-mark.now { color: var(--accent); }

/* --- the last sector and the current one --------------------------------- */
/* In the infield: the middle row, inside the two ends, the last sector above
   the current one. */
.agc-sector {
    grid-area: 2 / 2 / 3 / -2;
    z-index: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 6px;
    padding: 4px 0 6px;
}
.agc-bench {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.agc-bench.now { --cw: 44px; }
.agc-bench.last { --cw: 28px; }
.agc-benchrow {
    display: flex;
    align-items: flex-end;
    gap: 6px;
    min-height: calc(var(--cw) * 89 / 64);
}
/* The played cards sit apart, so each one reads, and only overlap once there
   are more than three. Three are reserved, so the sector card does not move
   as they arrive. */
.agc-played {
    display: flex;
    align-items: flex-end;
    gap: 4px;
    min-width: calc(var(--cw) * 3 + 8px);
}
.agc-played.tight { gap: 0; }
.agc-played.tight .card + .card { margin-left: calc(var(--cw) * -0.35); }
.agc-played.tighter { gap: 0; }
.agc-played.tighter .card + .card { margin-left: calc(var(--cw) * -0.55); }
/* The sector's own card, ringed so it reads apart from the cards played on it. */
.agc-bench .card.agc-target { outline: 2px solid var(--accent); outline-offset: 2px; }
.agc-readout {
    padding: 0 8px;
    border-radius: 6px;
    text-align: center;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.agc-bench.now .agc-readout { height: 26px; line-height: 26px; font-size: 16px; }
.agc-readtotal { font-size: 13px; font-weight: 700; line-height: 1.25; }
.agc-readwhat { font-size: 12px; line-height: 1.25; }
.agc-total { font-size: 22px; font-weight: 800; line-height: 1; }
.agc-total.over { color: #e05d5d; }
.agc-readout.gain { color: #6fcf97; }
.agc-readout.loss { color: #e8a33d; }
.agc-readout.bust { color: #e05d5d; }
/* A sector just resolved: its result lights up, so the move that did it reads. */
.agc-bench.last.agc-flash .agc-readout { animation: agc-flash 1.6s ease-out 1; }
@keyframes agc-flash {
    0%, 35% { background: rgba(255, 209, 102, 0.32); }
    100% { background: transparent; }
}
body.motion-off .agc-bench.last.agc-flash .agc-readout { animation: none; }
@media (prefers-reduced-motion: reduce) {
    .agc-bench.last.agc-flash .agc-readout { animation: none; }
}

/* --- the controls -------------------------------------------------------- */
.agc-controls {
    --cw: 30px;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: stretch;
    gap: 10px;
}
.agc-gears, .agc-spares { display: flex; justify-content: center; align-items: stretch; gap: 8px; }
.agc-gear, .agc-spare {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    min-width: 84px;
    padding: 6px 8px;
    border-radius: 10px;
    border: 1px solid var(--panel-border);
    background: var(--panel);
    color: inherit;
    font: inherit;
    cursor: pointer;
}
/* Hover only where there is one: a touch screen keeps :hover on the last tap,
   which would read as a hint on whichever control sat under it. */
@media (hover: hover) {
    .agc-gear:not(:disabled):hover, .agc-spare:not(:disabled):hover { border-color: var(--accent); }
}
.agc-gear:disabled, .agc-spare:disabled { cursor: default; opacity: 0.55; }
.agc-gear.hinted, .agc-spare.hinted, .btn.agc-go.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
.agc-gearname { font-size: 13px; font-weight: 700; white-space: nowrap; }
.agc-pile { display: flex; align-items: center; gap: 6px; }
.agc-count { font-size: 18px; font-weight: 800; font-variant-numeric: tabular-nums; min-width: 1.4em; }
.agc-range { font-size: 11px; opacity: 0.75; white-space: nowrap; }
/* A gear's ranks still out, as in Foolish Faces' strip: the top half of each
   rank's simplified card (the rank alone) over one bar per card of the rank, a
   lit bar a card still out and a faded one a card face up this stage. A lit bar
   is green when that card would hit the sector's number exactly and red when it
   would go over. A rank with none left fades whole. */
.agc-unseen { display: flex; justify-content: center; gap: 4px; }
.agc-rank { --agc-tile: 22px; display: inline-flex; flex-direction: column; align-items: center; gap: 2px; }
.agc-rank.agc-zero { opacity: 0.3; }
.agc-tile {
    display: block;
    width: var(--agc-tile);
    height: calc(var(--agc-tile) * 89 / 64 * 0.52);
    overflow: hidden;
    border-radius: 3px;
    background: #fff;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
}
.agc-tile .card { --cw: var(--agc-tile); display: block; margin: 0; box-shadow: none; }
.agc-bars { display: flex; gap: 2px; width: var(--agc-tile); height: 6px; }
.agc-bar { flex: 1 1 0; min-width: 0; border-radius: 2px; background: #f2efe9; }
.agc-bar.seen { background: rgba(242, 239, 233, 0.22); }
.agc-rank.agc-exact .agc-bar:not(.seen) { background: #6fcf97; }
.agc-rank.agc-over .agc-bar:not(.seen) { background: #e05d5d; }
.btn.agc-go { align-self: center; min-width: 9.5em; white-space: nowrap; }

/* --- a wider single column: bigger track cards ---------------------------- */
@container agc (min-width: 560px) {
    .agc-stage { --cw: 42px; }
    .agc-bench.now { --cw: 52px; }
    .agc-bench.last { --cw: 32px; }
}

/* --- the wide layout: the track beside a side column ---------------------- */
@container agc (min-width: 860px) {
    .agc-layout {
        grid-template-columns: auto 300px;
        grid-template-rows: auto 1fr;
        grid-template-areas: "stage dash" "stage controls";
        justify-content: center;
        justify-items: stretch;
        align-items: start;
        gap: 12px 20px;
    }
    .agc-stage {
        --cw: 52px;
        --agc-trackw: min(calc(100cqi - 320px), 620px);
    }
    .agc-dash { justify-content: stretch; }
    .agc-clock { flex: 1; }
    .agc-controls { flex-direction: column; flex-wrap: nowrap; align-items: stretch; --cw: 28px; }
    .agc-gears { flex-direction: column; gap: 6px; }
    /* A gear as one thin bar: its pile, its name, and its ranks at the right. */
    .agc-gear {
        --cw: 22px;
        display: grid;
        grid-template-columns: auto auto minmax(0, 1fr);
        grid-template-areas: "pile name unseen";
        align-items: center;
        column-gap: 10px;
        min-width: 0;
        padding: 5px 12px;
        text-align: left;
    }
    .agc-gear .agc-pile { grid-area: pile; }
    .agc-gear .agc-gearname { grid-area: name; font-size: 14px; }
    .agc-gear .agc-unseen { grid-area: unseen; justify-content: flex-end; gap: 5px; }
    .agc-gear .agc-rank { --agc-tile: 24px; }
    .agc-spares {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 6px;
    }
    .agc-spare { min-width: 0; }
    .btn.agc-go { grid-column: 1 / -1; align-self: stretch; }
}

/* --- the result -------------------------------------------------------- */
table.agc-result {
    border-collapse: collapse;
    margin: 8px auto;
    font-variant-numeric: tabular-nums;
}
table.agc-result th, table.agc-result td { padding: 3px 10px; }
table.agc-result th { text-align: left; }
table.agc-result td { text-align: right; }
table.agc-result tr:first-child th { text-align: right; font-size: 12px; opacity: 0.75; }
table.agc-result tr:first-child th:first-child { text-align: left; }
table.agc-result .agc-total-row th, table.agc-result .agc-total-row td {
    border-top: 1px solid var(--panel-border);
    font-weight: 700;
}

/* --- the log --------------------------------------------------------- */
#log-entries .logline.agc-log-single { color: #6fcf97; }
#log-entries .logline.agc-log-bust { color: #e05d5d; }

/* --- phones ------------------------------------------------------------ */
@media (max-width: 640px) {
    .status.agc-status { min-height: 52px; }
}
@media (max-width: 560px) {
    .agc-dash { gap: 8px; }
    .agc-clock, .agc-damage { padding: 5px 10px 5px; }
    .agc-score { font-size: 26px; }
    .agc-tally { font-size: 12px; }
    .agc-jokers { --cw: 36px; }
    .agc-controls { gap: 6px; --cw: 24px; }
    .agc-gears, .agc-spares { gap: 5px; }
    .agc-gear, .agc-spare { min-width: 0; padding: 5px 5px; }
    .agc-gearname { font-size: 12px; }
    .agc-unseen { gap: 2px; }
    .agc-rank { --agc-tile: 19px; }
}
/* =========================================================================
   Mate
   Top to bottom: the top seat's header, its hand face up, the 10-column move
   grid (the top seat's cards, the move numbers, the bottom seat's cards), and
   the bottom seat's header; then the hand panel, the foreplace buttons beside
   its title. Every box reserves its tallest shape in terms of --mate-cw
   and the card aspect (89/64, about 1.39), so nothing above the hand moves as
   a game runs: the grid always paints its 30 cells, the top hand keeps one
   card row, each header keeps its aside slot, and the hand's title row is the
   same height with the buttons and without. The small cards are 46px on a desktop and 30px
   on a phone, both on the simplified atlas (read-only cards at or under
   46px, ui-conventions "Small cards use the simplified sprite").
   ========================================================================= */
/* --- the status and progress lines ---------------------------------------- */
.status.mate-status { min-height: 1.45em; }
.progress.mate-progress { white-space: nowrap; }
@media (max-width: 560px) {
    /* An answer's line ("Bot has no Club, so answer the A♣ with an Ace.")
       takes two lines on a phone: reserve them in every phase. */
    .status.mate-status { min-height: 2.9em; }
}
/* --- the board ------------------------------------------------------------ */
.mate-board {
    --mate-cw: 46px;
    --mate-ch: calc(var(--mate-cw) * 89 / 64);
    --mate-gap: 4px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    margin: 0 auto 12px;
    padding: 10px 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
    width: fit-content;
    max-width: 100%;
    box-sizing: border-box;
}
@media (max-width: 560px) {
    .mate-board { --mate-cw: 30px; --mate-gap: 3px; padding: 8px 6px; gap: 6px; }
}
/* Every small card on the board follows the board's size, not .card.mini's 46px. */
.mate-board .card.mini { --cw: var(--mate-cw); }
.mate-board .card { position: relative; }
/* --- the seat headers ----------------------------------------------------- */
.mate-seat {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    min-height: calc(var(--mate-ch) + 6px);
    white-space: nowrap;
}
.mate-name { font-weight: 700; }
.mate-total {
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    padding: 1px 10px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.1);
    min-width: 2.5em;
    text-align: center;
}
.mate-aside { position: relative; display: inline-flex; }
.mate-plus {
    position: absolute;
    top: -6px;
    right: -12px;
    padding: 0 4px;
    border-radius: 8px;
    background: var(--accent);
    color: #1b1b1b;
    font-size: 11px;
    font-weight: 800;
    line-height: 16px;
}
/* --- the top hand --------------------------------------------------------- */
.mate-opphand .cardrow {
    flex-wrap: nowrap;
    justify-content: center;
    gap: var(--mate-gap);
    min-height: var(--mate-ch);
}
.mate-threat {
    position: absolute;
    top: -6px;
    right: -6px;
    padding: 0 3px;
    border-radius: 7px;
    background: #d6455d;
    color: #fff;
    font-size: 10px;
    font-weight: 800;
    line-height: 14px;
    font-variant-numeric: tabular-nums;
    pointer-events: none;
}
/* --- the move grid -------------------------------------------------------- */
.mate-grid {
    display: flex;
    flex-direction: column;
    gap: var(--mate-gap);
    margin-inline: auto;
}
.mate-gridrow {
    display: flex;
    justify-content: center;
    gap: var(--mate-gap);
}
.mate-cell,
.mate-num {
    flex: none;
    width: var(--mate-cw);
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 5px;
}
.mate-cell {
    height: var(--mate-ch);
    background: rgba(255, 255, 255, 0.04);
}
.mate-num {
    height: 18px;
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    opacity: 0.75;
}
.mate-cell.mate-now,
.mate-num.mate-now { background: rgba(255, 209, 102, 0.18); opacity: 1; }
.mate-cell.mate-off,
.mate-num.mate-off { opacity: 0.25; }
/* The led card of each move: a dot in its corner. */
.mate-led {
    position: absolute;
    left: 2px;
    bottom: 2px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #1b1b1b;
    box-shadow: 0 0 0 1px #fff;
    pointer-events: none;
}
.mate-grid .card.mate-won { outline: 2px solid var(--accent); outline-offset: 1px; }
.mate-grid .card.mate-mating { outline: 3px solid #d6455d; outline-offset: 1px; }
.mate-grid .card.mate-reused { outline: 2px dashed rgba(255, 255, 255, 0.75); outline-offset: 1px; }
.mate-matetag {
    font-size: 12px;
    font-weight: 800;
    color: #ff8a9b;
    white-space: nowrap;
}
@media (max-width: 560px) {
    .mate-num { height: 16px; font-size: 11px; }
    .mate-matetag { font-size: 9px; }
}
/* --- the hand panel ------------------------------------------------------- */
/* The title row is a three-track grid with equal outer tracks, so the title
   stays on the panel's axis whatever sits beside it; #mate-actions takes no
   box of its own (display: contents) and its two buttons fill the outer
   tracks, Foreplace against the title's left and Keep all 10 against its
   right. Both are .btn.small, shorter than the title line plus the h3's
   margin, so the row is the same height with them and without them. */
.mate-handpanel .panel-head {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    column-gap: 10px;
    align-items: start;
}
.mate-handpanel .panel-head > h3 { grid-column: 2; grid-row: 1; white-space: nowrap; }
.mate-actions { display: contents; }
.mate-actions > .btn {
    grid-row: 1;
    position: relative;
    margin-top: -2px;
    white-space: nowrap;
}
.mate-actions > .mate-fpbtn { grid-column: 1; justify-self: end; min-width: 10.5em; }
.mate-actions > .mate-keepbtn { grid-column: 3; justify-self: start; }
@media (max-width: 560px) {
    /* "Foreplace the 10♥" does not fit beside the title on a phone: the
       picked card and the tooltip say which card. */
    .mate-actions > .mate-fpbtn { min-width: 0; }
    .mate-fpcard { display: none; }
}
/* Touch screens: the site-wide coarse rule gives .btn.small a 36px minimum,
   taller than the row, which would grow the row while the buttons show and
   move the hand. Keep the desktop height and grow the tap target invisibly,
   inside the row (platform.css does the same for a .panel-head's own button). */
@media (pointer: coarse) {
    .mate-actions > .btn.small { min-height: 0; padding-top: 2px; padding-bottom: 2px; }
    .mate-actions > .btn.small::after { content: ''; position: absolute; inset: -6px -4px; }
}
.mate-handpanel .card { position: relative; }
.mate-handpanel .card.mate-dim { opacity: 0.4; }
.mate-matebadge {
    position: absolute;
    top: -8px;
    right: -8px;
    padding: 0 5px;
    border-radius: 9px;
    background: var(--accent);
    color: #1b1b1b;
    font-size: 12px;
    font-weight: 800;
    line-height: 18px;
    font-variant-numeric: tabular-nums;
    pointer-events: none;
}
/* The lead mark, bottom center: a lead the other player can't beat, or an
   answer that wins the move. Either way the player leads the next move. It
   sits just inside the card's bottom edge, between the corner indexes and
   clear of the mate badge: hung below the edge, it reached into the second
   row of a wrapped phone hand. */
.mate-keepmark {
    position: absolute;
    left: 50%;
    bottom: 3px;
    translate: -50% 0;
    min-width: 22px;
    padding: 0 5px;
    box-sizing: border-box;
    border-radius: 9px;
    /* The mate badge's colors, so the two marks read as one family. */
    background: var(--accent);
    color: #1b1b1b;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
    font-size: 19px;
    font-weight: 800;
    line-height: 17px;
    text-align: center;
    pointer-events: none;
}
/* A flight ghost is a deep clone of the hand card, so it would carry both
   marks to the grid, where they mean nothing. */
.card-fly .mate-keepmark,
.card-fly .mate-matebadge { display: none; }
/* --- the result dialog and the score table -------------------------------- */
.panel.mate-result { text-align: center; }
.panel.mate-result .result-actions { justify-content: center; flex-wrap: wrap; }
.mate-restitle { margin: 0 0 6px; }
.mate-resline { margin: 4px auto 10px; max-width: 34em; }
table.mate-scores {
    margin: 0 auto;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
}
table.mate-scores th,
table.mate-scores td { padding: 3px 10px; white-space: nowrap; }
table.mate-scores .mate-numcol { text-align: right; }
table.mate-scores .mate-textcol { text-align: left; }
table.mate-scores thead th { font-size: 13px; opacity: 0.8; border-bottom: 1px solid var(--panel-border); }
table.mate-scores tbody th { font-weight: 600; }
table.mate-scores tr.mate-totalrow th,
table.mate-scores tr.mate-totalrow td { font-weight: 800; border-top: 1px solid var(--panel-border); }
@media (max-width: 560px) {
    table.mate-scores th,
    table.mate-scores td { padding: 3px 5px; font-size: 13px; }
}
/* =========================================================================
   Six-Card Golf (sixcardgolf)
   Each player keeps a 2x3 grid of cards, mostly face down, and swaps a drawn
   card in each turn to score as low as possible. The board, top to bottom:
     one small panel per other seat, in the order they play after you
       (.scg-opponents, .scg-seat)
     the stock, the discard pile (the Discard control after a stock draw),
       and the drawn card's reserved slot   (.scg-table, .scg-pile)
     the viewed seat's own grid, large   (.scg-mine)
   CARD SIZES are custom properties on the board, picked up as --cw by the
   containers (never a rule on .card), so a breakpoint changes one number. Every
   grid card uses the simplified atlas at every size (ui-conventions, "Dense
   grids of cards"); the piles use the full art above 46px.
   ANTI-JITTER. Every box is a fixed size: a slot is always one card, a grid
   always 2x3, a pile always one card (a dashed placeholder when empty), the
   seat heads never wrap and their marks are always rendered (shown with
   visibility). The status line, the scoreboard, and the action row are
   reserved at their tallest shapes.
   Rules that name a SHARED id are scoped through a class this game adds
   (.scg-status on the status line, .scg-scoreboard on the progress line,
   .scg-summary on the result panel), because those ids are on every page.
   ========================================================================= */
.scg-board {
    --scg-oppcw: 34px;
    --scg-minecw: 72px;
    --scg-pilecw: 64px;
    --scg-gap: 8px;
    /* An opponent's grid gap, column padding, and panel padding. */
    --scg-oppgridgap: 3px;
    --scg-colpad: 2px;
    --scg-opppad: 7px;
    /* An opponent's panel: three small cards, the two gaps between columns,
       each column's padding on both sides, the panel's padding, and its border. */
    --scg-oppw: calc(3 * var(--scg-oppcw) + 2 * var(--scg-oppgridgap) + 6 * var(--scg-colpad)
        + 2 * var(--scg-opppad) + 2px);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    margin-bottom: 10px;
}
/* --- the status line ---------------------------------------------------- */
/* Two lines reserved, at an explicit line height so the reserve is exactly two
   lines: in pass and play the acting seat's name leads the line. */
.status.scg-status { line-height: 1.4; min-height: 2.8em; }
/* Lit whenever this device may act. */
.status.scg-status.scg-turn { color: var(--accent); }
/* --- the scoreboard ----------------------------------------------------- */
/* The shared scoreboard treatment (platform.css carries it for its opt-in
   list), restated for this game's class. LOW score wins here, so
   `.progress-score.leading` marks the minimum. */
.progress.scg-scoreboard {
    opacity: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    flex-wrap: wrap;
    min-height: 34px;
}
.progress.scg-scoreboard .progress-score { white-space: nowrap; }
.scg-scorename { font-size: 12px; opacity: 0.8; margin-right: 5px; }
.scg-scorenum { font-weight: 800; font-variant-numeric: tabular-nums; }
/* --- the opponents ------------------------------------------------------ */
/* Fixed-width panels in a centered, wrapping row, capped at 5 a row, so the
   number of rows is a function of the seat count and fixed for the match. */
.scg-opponents {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--scg-gap);
    width: 100%;
    max-width: calc(5 * var(--scg-oppw) + 4 * var(--scg-gap));
}
.scg-opponents .scg-seat { width: var(--scg-oppw); padding-inline: var(--scg-opppad); }
/* An opponent's score chip on a line of its own under the name, so a name
   still fits beside the marks in a small panel. */
.scg-opponents .scg-seat-head { flex-wrap: wrap; row-gap: 2px; }
.scg-opponents .scg-seat-head .scg-showing { order: 3; }
.scg-opponents .scg-seat-head::after { content: ''; order: 2; flex-basis: 100%; height: 0; }
.scg-opponents .scg-grid { --cw: var(--scg-oppcw); gap: var(--scg-oppgridgap); }
.scg-opponents .scg-col { gap: var(--scg-oppgridgap); padding: var(--scg-colpad); }
/* --- a seat's panel ------------------------------------------------------ */
.scg-seat {
    box-sizing: border-box;
    padding: 5px 7px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default, so the accent ring never shifts the layout. */
    outline: 2px solid transparent;
}
.scg-seat.scg-active { outline-color: var(--accent); }
.scg-seat-head {
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 5px;
    min-width: 0;
    margin-bottom: 4px;
    white-space: nowrap;
    line-height: 1.3;
}
/* Whose turn it is, and a seat that has gone out, as SHAPES as well as the
   ring. Always rendered, shown with visibility, so the head never resizes. */
.scg-toact,
.scg-outmark { flex: none; visibility: hidden; color: var(--accent); }
.scg-toact { font-size: 11px; }
.scg-toact.scg-on,
.scg-outmark.scg-on { visibility: visible; }
.scg-seat-name {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: 700;
    font-size: 13px;
}
/* The face-up score: the one number a seat's go-out decision needs. */
.scg-showing {
    flex: none;
    font-weight: 800;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    padding: 0 6px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
}
/* --- a grid: three columns of two --------------------------------------- */
.scg-grid { display: flex; justify-content: center; gap: 3px; }
.scg-col {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 2px;
    border-radius: 7px;
    /* Transparent until paired, so the outline never shifts the layout. */
    outline: 2px solid transparent;
    outline-offset: -1px;
}
/* A pair in a column scores 0: outlined and slightly dimmed, no text. */
.scg-col.scg-paired { outline-color: rgba(255, 209, 102, 0.85); }
.scg-col.scg-paired .card { opacity: 0.82; }
.scg-slot { line-height: 0; }
/* --- the viewer's own grid ---------------------------------------------- */
.scg-mine { width: fit-content; margin-inline: auto; padding: 6px 10px 10px; }
.scg-mine .scg-grid { --cw: var(--scg-minecw); gap: var(--scg-gap); }
.scg-mine .scg-col { gap: var(--scg-gap); padding: 3px; border-radius: 9px; }
.scg-mine .scg-seat-name,
.scg-mine .scg-showing { font-size: 15px; }
/* The face-down count after the face-up score: "14 + ?×2". */
.scg-unknown { font-weight: 600; opacity: 0.7; }
/* A 2's and a King's points ("−2", "0") in the ability badge's corner; two
   characters, so the box grows to its text rather than staying square. */
.card.scg-valcard .ability-badge { width: auto; min-width: clamp(14px, calc(var(--cw) * 0.26), 22px); padding: 0 3px; }
/* The small grids keep their corner clear: at 30 to 34px a badge covers the
   rank (ui-conventions, the ability badge is not for small cards). */
.scg-opponents .card.scg-valcard .ability-badge { display: none; }
/* The hover preview while placing a drawn card: how the face-up score moves.
   Mouse only, so a touch screen shows nothing extra. */
.scg-mine .card.scg-hasdelta { position: relative; }
.scg-delta {
    display: none;
    position: absolute;
    left: 50%;
    bottom: 4px;
    transform: translateX(-50%);
    z-index: 3;
    padding: 1px 6px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.78);
    color: #fff;
    font-size: 13px;
    font-weight: 800;
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    pointer-events: none;
}
.scg-delta.scg-better { color: #8ef0a6; }
.scg-delta.scg-worse { color: #ffb0a8; }
@media (hover: hover) {
    .scg-mine .card.scg-hasdelta:hover .scg-delta { display: block; }
}
/* --- the table: stock, discard pile, drawn card ------------------------- */
.scg-table {
    --cw: var(--scg-pilecw);
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 16px;
}
/* Always one card's size, empty or full. */
.scg-pile {
    position: relative;
    flex: none;
    width: var(--cw);
    height: calc(var(--cw) * 89 / 64);
    line-height: 0;
}
/* The discard pile's top two cards, stacked in one spot. */
.scg-discard .card { position: absolute; left: 0; top: 0; }
/* The drawn card's slot, waiting: outlined, not blank. */
.scg-held .card.empty { border-color: rgba(255, 255, 255, 0.35); }
/* --- the discard pile as the Discard control ---------------------------- */
/* After a stock draw a tap on the pile discards the drawn card. The pile says
   so with a label over its card, and a dashed ring tells it from the pile's
   take (draw phase), whose meaning is the opposite. */
.scg-discard .card.scg-discardtarget { outline: 2px dashed var(--accent); outline-offset: 2px; }
.scg-droplabel {
    position: absolute;
    left: 50%;
    bottom: 6px;
    transform: translateX(-50%);
    z-index: 3;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--accent);
    color: #3a2c00;
    font-size: 12px;
    font-weight: 800;
    line-height: 1.35;
    white-space: nowrap;
    pointer-events: none;
}
/* --- the scorecard ------------------------------------------------------ */
/* A REAL table, so every column is sized once for every row. Names left,
   numbers right in tabular figures, header cells included, and an explicit
   alignment on every cell because the lifted dialog centers its text. */
.scg-cards {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    margin: 6px 0 4px;
}
.scg-scorecard {
    border-collapse: collapse;
    margin: 0 auto;
    font-size: 13px;
}
.scg-scorecard th,
.scg-scorecard td {
    text-align: right;
    padding: 3px 6px;
    border-bottom: 1px solid var(--panel-border);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.scg-scorecard th { font-weight: 600; }
.scg-scorecard thead th { font-size: 12px; opacity: 0.75; }
.scg-scorecard .scg-name {
    text-align: left;
    max-width: 7em;
    overflow: hidden;
    text-overflow: ellipsis;
}
.scg-scorecard .scg-num { text-align: right; min-width: 1.4em; }
.scg-scorecard .scg-sum { font-weight: 700; }
/* The lowest total, when nobody shares it. */
.scg-scorecard tr.scg-best .scg-name,
.scg-scorecard tr.scg-best .scg-sum { color: var(--accent); font-weight: 800; }
/* In the log's narrow column. */
.scg-logcards .scg-scorecard { font-size: 11px; }
.scg-logcards .scg-scorecard th,
.scg-logcards .scg-scorecard td { padding: 2px 3px; }
.scg-log-win { color: var(--accent); font-weight: 700; }
/* --- a finished board, scored by column (boardBreakdown) ---------------- */
/* In the hole summary at 34px, in the log at 24px; simplified art at both. */
.scg-breakdowns {
    --cw: 34px;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px 14px;
    margin: 4px 0 8px;
}
.scg-breakdowns .card.mini { --cw: inherit; }
.scg-breakdown {
    padding: 4px 6px 2px;
    border-radius: 8px;
    border: 1px solid var(--panel-border);
}
.scg-breakdown.scg-bdmine { border-color: var(--accent); }
.scg-bdhead {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    font-size: 12px;
    line-height: 1.3;
    margin-bottom: 3px;
    white-space: nowrap;
}
.scg-bdname { font-weight: 700; max-width: 7em; overflow: hidden; text-overflow: ellipsis; }
.scg-bdtotal { font-weight: 800; font-variant-numeric: tabular-nums; }
.scg-bdgrid { display: flex; gap: 3px; }
.scg-bdcol {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 1px;
    border-radius: 5px;
    outline: 2px solid transparent;
    outline-offset: -1px;
}
.scg-bdcol.scg-bdpair { outline-color: rgba(255, 209, 102, 0.85); }
.scg-bdpts {
    font-size: 12px;
    font-weight: 700;
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
}
.scg-logboards { --cw: 24px; gap: 6px 8px; margin: 2px 0 6px; }
.scg-logboards .scg-bdhead,
.scg-logboards .scg-bdpts { font-size: 11px; }
/* --- the worked example in the Cheat_Sheet ------------------------------ */
.scg-extable th,
.scg-extable td { text-align: center; }
.scg-extable th:first-child { text-align: left; }
/* --- tablets ------------------------------------------------------------ */
/* The board column narrows beside the log, so six chips can wrap. */
@media (max-width: 1000px) {
    .progress.scg-scoreboard { min-height: 64px; }
}
/* --- phones ------------------------------------------------------------- */
/* Every reservation above is re-derived here rather than left at its desktop
   figure. Three opponents a row: one row at 2 to 4 seats, two at 5 and 6. */
@media (max-width: 600px) {
    .scg-board {
        --scg-oppcw: 30px;
        --scg-minecw: 60px;
        --scg-pilecw: 52px;
        --scg-gap: 6px;
        /* Three 108px panels and two 4px gaps fit a 360px phone's 340px board. */
        --scg-oppgridgap: 2px;
        --scg-colpad: 1px;
        --scg-opppad: 3px;
    }
    .scg-opponents { gap: 4px; max-width: calc(3 * var(--scg-oppw) + 2 * 4px); }
    .scg-seat { padding-top: 4px; padding-bottom: 5px; }
    .scg-opponents .scg-seat-name,
    .scg-opponents .scg-showing { font-size: 12px; }
    .scg-mine { padding: 5px 8px 8px; }
    /* Inside a 52px pile card. */
    .scg-droplabel { font-size: 10px; padding: 0 3px; bottom: 5px; }
    .status.scg-status { font-size: 16px; }
    .progress.scg-scoreboard { gap: 6px 8px; min-height: 64px; }
    .scg-scorecard { font-size: 12px; }
    .scg-scorecard th,
    .scg-scorecard td { padding: 2px 3px; }
}
/* ---------- one-eyed jack ---------- */

/*
 * The side colors, from the Okabe-Ito palette so they stay apart for every
 * kind of color vision, and each side also has its own SHAPE (disc, rounded
 * square, triangle), so color is never the only thing telling two chips apart.
 * The class goes on anything that belongs to a side: a chip token, a row line,
 * a progress chip, a result row.
 */
.oej-side0 { --oej-side: #0072B2; }
.oej-side1 { --oej-side: #E69F00; }
.oej-side2 { --oej-side: #009E73; }

/* ---- the board ---- */

/*
 * THE CELL SIZE. Above 560px it follows the viewport HEIGHT, so the board and
 * the whole hand fit on a laptop screen without scrolling: 360px of the height
 * is everything that is not the board (the top bar, the status and progress
 * lines, the board panel's padding, and the hand panel), measured at 1280x800,
 * 1440x900, and 1512x860. About 31px cells at 800px tall, up to 52px on a
 * tall screen, and 28px at the smallest, which still fits the hand at
 * 1280x720. `svh`, so a phone's URL bar showing and hiding never resizes it.
 * At 560px and below it follows the WIDTH: ten cells across the panel, with
 * 34px left for the page padding (10px a side), the panel padding (6px a side),
 * and the borders. Rounded down to a whole pixel where `round()` exists, so the
 * atlas shows no seam down a card's edge.
 *
 * Both are custom properties, which accept any tokens at parse time, so the
 * newer forms sit behind @supports rather than after a fallback.
 */
.panel.oej-board {
    --oej-cell: clamp(28px, calc((100vh - 360px) / 14), 52px);
    padding: 6px;
}
@supports (height: 100svh) and (width: round(down, 1.5px, 1px)) {
    .panel.oej-board { --oej-cell: round(down, clamp(28px, calc((100svh - 360px) / 14), 52px), 1px); }
}

/*
 * The 10x10 grid. FIXED tracks and NO gap: each cell is exactly one card plus
 * 2px, with the card inset 1px, so the box is a constant size (nothing on the
 * board ever re-centers it) and every cell center sits at an exact tenth of the
 * grid. The row overlay draws in those cell units.
 */
.oej-grid {
    --oej-cw: calc(var(--oej-cell) - 2px);
    --oej-ch: calc(var(--oej-cw) * 89 / 64 + 2px);
    position: relative;
    display: grid;
    grid-template-columns: repeat(10, var(--oej-cell));
    grid-template-rows: repeat(10, var(--oej-ch));
    gap: 0;
    width: fit-content;
    margin: 0 auto;
}
.oej-cell {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
}
/* The card size comes from the board's own property, never from a rule on .card. */
.oej-grid .oej-card { --cw: var(--oej-cw); }
/* A lit space is a button, but a dense grid has no room for the hand's 5px lift. */
.oej-grid .oej-card.clickable:hover { transform: none; filter: brightness(1.08); }

/* A free corner: no card and no atlas, a star on the board's cream. */
.oej-free {
    box-sizing: border-box;
    width: var(--oej-cw);
    height: calc(var(--oej-cw) * 89 / 64);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 6%;
    border: 1px solid rgba(0, 0, 0, 0.35);
    background: #f3ead2;
    color: #8a6a00;
    font-size: calc(var(--oej-cw) * 0.55);
    line-height: 1;
    cursor: default;
}

/* The card under a chip is dimmed (a filter, never opacity) so the token reads. */
.oej-grid .oej-card.oej-occupied { filter: brightness(0.62) saturate(0.6); }

/*
 * The highlight states: outline, box-shadow, and filter only, so nothing
 * changes size. Outlines are drawn INSIDE the card (a negative offset),
 * because the cells have no gap for them to spill into. Two jobs, two tools:
 * OUTLINE says where your cards can go, and BOX-SHADOW says what the other
 * seats just did, so a space can carry both.
 */
/*
 * Every open space a natural card in your hand can take: a dashed blue ring,
 * 3px and saturated so it reads on a white card at 28px, but dashed and
 * unglowing, so it stays quieter than a picked card's solid gold ring.
 */
.oej-grid .oej-card.oej-possible {
    outline: 3px dashed #1f8fff;
    outline-offset: -3px;
}
/* A picked card: everything it cannot use is dimmed, so its spaces stand out. */
.oej-grid.oej-picking .oej-card:not(.oej-target):not(.oej-removable):not(.oej-taken-twin) {
    filter: brightness(0.45) saturate(0.4);
}
.oej-grid.oej-picking .oej-free { filter: brightness(0.6); }
.oej-grid .oej-card.oej-target {
    z-index: 1;
    outline: 3px solid var(--accent);
    outline-offset: -3px;
    box-shadow: 0 0 10px 3px rgba(255, 209, 102, 0.85);
    animation: oej-target-pulse 1.4s ease-in-out infinite;
}
@keyframes oej-target-pulse {
    0%, 100% { box-shadow: 0 0 6px 2px rgba(255, 209, 102, 0.6); }
    50% { box-shadow: 0 0 14px 5px rgba(255, 209, 102, 0.95); }
}
body.motion-off .oej-grid .oej-card.oej-target { animation: none; }
/*
 * ROW SPACES: an open space where a chip would make a new row. A light tint
 * over the card (an overlay span in the cell, under any chip), so it combines
 * with any ring: red where another side would make one (DANGER, lighter than it
 * was at the owner's request, 2026-11-05), green where you would (GAIN), and
 * split corner to corner where both would, red to the top left and green to the
 * bottom right.
 */
.oej-tint {
    position: absolute;
    left: 50%;
    top: 50%;
    width: var(--oej-cw);
    height: calc(var(--oej-cw) * 89 / 64);
    transform: translate(-50%, -50%);
    border-radius: 3px;
    z-index: 1;
    pointer-events: none;
}
.oej-dangertint { background: rgba(255, 40, 40, 0.18); }
.oej-gaintint { background: rgba(40, 200, 90, 0.22); }
.oej-bothtint {
    background: linear-gradient(135deg, rgba(255, 40, 40, 0.24) 0 50%, rgba(40, 200, 90, 0.28) 50% 100%);
}
/*
 * A BLOCK WITHIN REACH: a danger space a natural card in your hand can take.
 * A solid red ring in place of the dashed blue one, and a small shield in the
 * corner, so it stands out from the danger spaces you can't fill.
 */
.oej-grid .oej-card.oej-possible.oej-blockable {
    outline: 3px solid #ff4d4d;
    outline-offset: -3px;
}
.oej-blockmark {
    position: absolute;
    right: 1px;
    top: 0;
    z-index: 3;
    font-size: 11px;
    line-height: 1;
    color: #fff;
    text-shadow: 0 0 2px #b00000, 0 0 2px #b00000;
    pointer-events: none;
}
/*
 * The hovered space and the other space with the same card, rung in white for
 * as long as a mouse is over one of them. It overrides the other rings while
 * it lasts. The OTHER space is the one being looked for, so it also lifts
 * and glows, and pulses once (the owner asked for a stronger mark,
 * 2026-11-05).
 */
.oej-grid .oej-card.oej-hovered,
.oej-grid .oej-card.oej-twinhover {
    z-index: 2;
    outline: 3px solid #ffffff;
    outline-offset: -3px;
}
.oej-grid .oej-card.oej-twinhover {
    z-index: 4;
    outline-width: 4px;
    outline-offset: -4px;
    box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.55), 0 0 14px 6px rgba(255, 255, 255, 0.9);
    scale: 1.12;
    animation: oej-twin-find 0.6s ease-out 1;
}
@keyframes oej-twin-find {
    0% { box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.55), 0 0 26px 14px rgba(255, 255, 255, 1); }
    100% { box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.55), 0 0 14px 6px rgba(255, 255, 255, 0.9); }
}
body.motion-off .oej-grid .oej-card.oej-twinhover { animation: none; }
.oej-grid.oej-picking .oej-card.oej-twinhover { filter: none; }
/* A two-eyed Jack lights every open space, so its ring is a quieter one. */
.oej-grid .oej-card.oej-target.oej-anywhere {
    outline: 2px solid rgba(255, 209, 102, 0.9);
    outline-offset: -2px;
    box-shadow: none;
    animation: none;
}
/* Where the other copy of a picked card went. */
.oej-grid .oej-card.oej-taken-twin {
    outline: 2px dashed #ffffff;
    outline-offset: -2px;
}
/* A chip a one-eyed Jack can remove. */
.oej-grid .oej-card.oej-removable {
    outline: 2px dashed #ff6b6b;
    outline-offset: -2px;
}
/*
 * A space another seat changed since your last move: a thick ring and a glow
 * in the mover's color (the oej-side<n> class on the card sets --oej-side). A
 * removed chip's space gets the same ring, and its title says so. Lifted out of
 * the occupied dimming, so the ring keeps its color.
 */
.oej-grid .oej-card.oej-last {
    z-index: 1;
    box-shadow: inset 0 0 0 3px var(--oej-side), 0 0 0 2px var(--oej-side), 0 0 10px 3px var(--oej-side);
}
.oej-grid .oej-card.oej-occupied.oej-last { filter: brightness(0.85); }
.oej-grid.oej-picking .oej-card.oej-last:not(.oej-target):not(.oej-removable) { filter: brightness(0.7); }
.oej-grid .oej-card.oej-hint {
    outline: 3px solid var(--accent);
    outline-offset: -3px;
    box-shadow: 0 0 12px 4px rgba(255, 209, 102, 0.75);
}
.oej-xmark {
    position: absolute;
    top: 1px;
    right: 2px;
    z-index: 4;
    font-size: calc(var(--oej-cell) * 0.34);
    font-weight: 800;
    line-height: 1;
    color: #ff6b6b;
    text-shadow: 0 0 2px #000000, 0 0 2px #000000;
    pointer-events: none;
}

/* ---- the chips ---- */

/*
 * A chip is a piece, drawn as an inline SVG token: the side's shape, a dark
 * outline, and a light halo, so it reads on a red card and a black one alike.
 * A locked chip (part of a row) carries a white center pip.
 */
.oej-token { display: block; overflow: visible; }
.oej-tokenhalo { fill: none; stroke: rgba(255, 255, 255, 0.9); stroke-width: 3.2; stroke-linejoin: round; }
.oej-tokenbody { fill: var(--oej-side); stroke: #161616; stroke-width: 1.3; stroke-linejoin: round; }
.oej-tokenpip { fill: #ffffff; stroke: #161616; stroke-width: 0.9; }
.oej-chip {
    position: absolute;
    left: 50%;
    top: 50%;
    width: calc(var(--oej-cell) * 0.64);
    height: calc(var(--oej-cell) * 0.64);
    transform: translate(-50%, -50%);
    z-index: 2;
    pointer-events: none;
}
.oej-chip-in { animation: oej-chip-in 360ms ease-out both; }
.oej-chip-out { animation: oej-chip-out 480ms ease-in forwards; }
@keyframes oej-chip-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes oej-chip-out { from { opacity: 1; } to { opacity: 0; } }
body.motion-off .oej-chip-in,
body.motion-off .oej-chip-out { animation: none; }
body.motion-off .oej-chip-out { display: none; }

/*
 * The rows: one line per counted run, over the grid in cell units (the grid has
 * no gap, so a cell center is an exact tenth). The stroke keeps its width
 * however the box is stretched.
 */
.oej-rows {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 3;
    overflow: visible;
    pointer-events: none;
}
.oej-rowcase {
    stroke: #161616;
    stroke-width: 7px;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
    opacity: 0.75;
}
.oej-rowline {
    stroke: var(--oej-side);
    stroke-width: 3.5px;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
}

/* ---- Swap dead card and Pass, in the hand panel's head ---- */
/*
 * At the left edge of the head and out of the flow, the way the sort button is
 * at the right (platform.css `.panel-head > .btn.small`), so the title stays
 * centered and a button appearing moves nothing. Small buttons, so they fit the
 * head's reserved height.
 */
.oej-handpanel .panel-head > .oej-controls {
    position: absolute;
    left: 0;
    top: 0;
    display: flex;
    align-items: center;
    gap: 6px;
}
.oej-controls .btn { white-space: nowrap; }
/* Touch screens: the same treatment platform.css gives the sort button. The tap
   target grows invisibly, so the button keeps its desktop height in the head. */
@media (pointer: coarse) {
    .oej-handpanel .oej-controls > .btn.small {
        position: relative;
        min-height: 0;
        top: -2px;
        padding-top: 2px;
        padding-bottom: 2px;
    }
    .oej-handpanel .oej-controls > .btn.small::after {
        content: '';
        position: absolute;
        inset: -6px -4px;
    }
}
/* ---- the hand ---- */

/* A dead card: dimmed, with a small tag, and still tappable so it can be swapped. */
.oej-handpanel #hand-cards .card.oej-dead {
    position: relative;
    filter: grayscale(0.7) brightness(0.72);
}
.oej-deadtag {
    position: absolute;
    left: 50%;
    bottom: 6%;
    transform: translateX(-50%);
    padding: 0 4px;
    border-radius: 4px;
    background: rgba(0, 0, 0, 0.78);
    color: #ffffff;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    line-height: 1.5;
    pointer-events: none;
}
.oej-handpanel #hand-cards .card.oej-hint { box-shadow: 0 0 12px 4px rgba(255, 209, 102, 0.75); }

/* ---- status and progress ---- */

#status-line.oej-status { min-height: 26px; }

#progress-line.oej-progress {
    opacity: 1;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 6px 10px;
    /* The Last played card is 30px wide and 42px tall: one line holds it. */
    min-height: 44px;
}
/* The Last played card, at the end of the progress line. */
.oej-lastslot {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    white-space: nowrap;
}
.oej-lastlabel { opacity: 0.75; }
/* Three classes, so the phone breakpoint's `.card.mini { --cw: 34px }` does not grow it. */
.oej-progress .oej-lastslot .card { --cw: 30px; }
.oej-progress .oej-sidechip {
    align-items: center;
    gap: 6px;
    padding: 3px 10px;
    white-space: nowrap;
}
.oej-progress .oej-sidechip.oej-turnside { border-color: var(--accent); }
.oej-sidetoken { width: 16px; height: 16px; flex: none; }
.oej-pips { display: inline-flex; gap: 3px; }
.oej-pip {
    box-sizing: border-box;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    border: 1.5px solid rgba(255, 255, 255, 0.7);
}
.oej-pip.oej-pipfilled { background: var(--accent); border-color: var(--accent); }
.oej-stock { font-size: 13px; opacity: 0.7; white-space: nowrap; }

/* ---- the result ---- */

.oej-result {
    border-collapse: collapse;
    margin: 10px auto 4px;
    font-variant-numeric: tabular-nums;
}
.oej-result th,
.oej-result td {
    padding: 4px 12px;
    font-size: 14px;
    border-bottom: 1px solid var(--panel-border);
}
.oej-result .oej-result-side { text-align: left; white-space: nowrap; }
.oej-result .oej-result-num { text-align: right; }
.oej-result thead th { font-size: 12px; font-weight: 600; opacity: 0.7; }
.oej-result tr.oej-won { background: rgba(255, 209, 102, 0.12); }
.oej-namebox { display: inline-flex; align-items: center; gap: 6px; }

/* ---- the log and the rules ---- */

.oej-logover { font-weight: 600; }
.oej-suit { white-space: nowrap; }

/* ---- phones ---- */

@media (max-width: 560px) {
    .panel.oej-board {
        --oej-cell: calc((100vw - 34px) / 10);
        padding: 6px;
    }
    @supports (width: round(down, 1.5px, 1px)) {
        .panel.oej-board { --oej-cell: round(down, calc((100vw - 34px) / 10), 1px); }
    }
    /* Two lines of status at 16px, the longest prompt and hint included. */
    #status-line.oej-status { min-height: 52px; font-size: 16px; }
    /* Three or four sides wrap to two lines here, one of them as tall as the
       Last played card, so both are held from the start. */
    #progress-line.oej-progress.oej-progress-many { min-height: 92px; align-content: center; }
    /* The label goes on phones, so two sides and the card fit one line; the
       card's title still says what it is. */
    .oej-lastlabel { display: none; }
    /* A hand of 7 on ONE row, so the hand panel is one card tall and the board
       and the hand share the screen. The tray is 100vw - 54px wide here (page
       and panel padding); with a 4px gap, 7 cards fit at (100vw - 78px) / 7,
       which is 44px at 390px and 40px at 360px, the phone hand's own floor.
       Never larger than the site's phone hand size. */
    .oej-handpanel #hand-cards { gap: 4px; }
    .oej-handpanel #hand-cards .card { --cw: min(clamp(40px, 22.4px + 6vw, 56px), calc((100vw - 78px) / 7)); }
    @supports (width: round(down, 1.5px, 1px)) {
        .oej-handpanel #hand-cards .card {
            --cw: round(down, min(clamp(40px, 22.4px + 6vw, 56px), calc((100vw - 78px) / 7)), 1px);
        }
    }
}
/* =========================================================================
   Ninety-Nine (ninetynine)
   David Parlett's exact-bid trick game for 3 or 4: lay three cards aside as a
   coded bid (clubs 3, hearts 2, spades 1, diamonds 0), may declare or reveal it
   for a premium, then take exactly that many tricks.
     the deal strip, the first row at every size: the trump, and an opponent's
       revealed hand face up                                  (.nn-deal)
     the table as a table: the other seats round the trick in play order,
       clockwise from the viewer's left, this player South   (.nn-board, .pos-*)
     one center cell holding two layers, the trick cross and the bid or call
       picker, so the cell is the larger of the two all game  (.nn-center, .nn-off)
     a four-line panel per seat: name and dealer chip, total, the three bid
       card slots and the premium chip, the standing        (.nn-seat-*)
     a finished trick held in the cross until the next lead   (.nn-winning)
   CONTAINERS MUST NOT MOVE. Every track of the desktop board is fixed or a
   fixed share of a fixed width, the center is a fixed width, every panel line
   has a height derived from its own size property, the picker is four fixed
   rows whatever it holds, and the deal strip is one fixed height whether it
   holds a revealed hand or not. Every card size is a custom property on
   .nn-board and every height is derived from it (x 1.39, the card aspect).
   #hand-cards gets no min-height: src/shared/ui/handspace.ts reserves it.
   SHARED IDS only through a class this game owns: .nn-handpanel on the hand
   panel, .nn-status and .nn-progress added by the renderers, .nn-summary on
   the result panel.
   ========================================================================= */

/* Visually hidden words beside an aria-hidden glyph or short form. */
.nn-vh {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* --- status and progress ------------------------------------------------ */
/* Two lines reserved: the action wanted, and a hint when one is asked for.
   The phone block reserves three, because two statuses wrap there. */
.status.nn-status { min-height: 2.6em; }
.status.nn-status.nn-turn { color: var(--accent); }
.nn-statusmain { display: block; }
.nn-hintnote { display: block; font-size: 14px; font-weight: 400; opacity: 0.85; }
.progress.nn-progress { min-height: 20px; white-space: nowrap; }
/* Suit glyphs in prose outside the log (status line, summary notes). The log's
   own are colored by the shared #log-entries rules. */
.nn-status .red,
.nn-summary .red,
.nn-legend .red,
.nn-trumpglyph.red { color: var(--red); }
.nn-status .red.diamond,
.nn-summary .red.diamond,
.nn-legend .red.diamond,
.nn-trumpglyph.red.diamond { color: var(--red-diamond); }
.nn-status .club,
.nn-summary .club,
.nn-legend .club,
.nn-trumpglyph.club { color: var(--blue-club); }

/* --- the board ----------------------------------------------------------- */
/* ONE GRID. #nn-table is `display: contents`, so the panels are cells of it.
   THE CENTER TRACK IS A FIXED WIDTH, never `auto`, or the flank panels would
   move sideways as the picker's contents change. Derived once, rounded up:
     trick  = 3 * --nn-playcw + 2 * --nn-trickgap + 20px padding  (56px: 200px)
     picker = the widest call button, "Let Bot North reveal" at 14px plus its
              padding, about 190px
     --nn-centerw = 224px, with slack for the picker's side padding. */
.nn-board {
    --nn-playcw: 56px;
    --nn-trickgap: 6px;
    /* Small cards, always the simplified atlas (ui-conventions: 34px and under). */
    --nn-bidcw: 26px;
    --nn-opencw: 34px;
    --nn-pickcw: 40px;
    --nn-centerw: 224px;
    --nn-panelw: 176px;
    --nn-namefs: 13px;
    --nn-totalfs: 15px;
    --nn-stdfs: 11px;
    --nn-chipfs: 11px;
    --nn-pad: 6px 9px 7px;
    --nn-dealfs: 12px;
    --nn-dealpad: 6px;
    --nn-trumpw: 64px;
    --nn-glyphfs: 28px;
    --nn-btnh: 32px;
    --nn-legendh: 18px;
    --nn-readh: 18px;
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--nn-centerw) minmax(0, 1fr);
    column-gap: 12px;
    row-gap: 8px;
    align-items: center;
    margin-bottom: 10px;
}
.nn-board.nn-seats-3 { grid-template-areas: "deal deal deal" "w center e" ". mine ."; }
.nn-board.nn-seats-4 { grid-template-areas: "deal deal deal" ". n ." "w center e" ". mine ."; }
.nn-table { display: contents; }
/* The left flank hugs the trick from the left, the right flank from the right. */
.nn-seat.pos-w { grid-area: w; justify-self: end; }
.nn-seat.pos-e { grid-area: e; justify-self: start; }
.nn-seat.pos-n { grid-area: n; justify-self: center; }
.nn-seat.nn-mine { grid-area: mine; justify-self: center; }

/* --- the deal strip -------------------------------------------------------- */
/* ONE FIXED HEIGHT, holding a revealed hand or not, so a reveal moves nothing:
   the hand's title line, one row of --nn-opencw cards, and the padding. */
/* The strip itself is unpainted and full width; its two blocks carry their own
   backgrounds, so the trump sits on the board's axis in an ordinary deal and
   moves left only when a revealed hand joins it, inside the same height. */
.nn-deal {
    --nn-opengap: 3px;
    --nn-boxh: calc(var(--nn-dealfs) * 1.3 + 3px + var(--nn-opencw) * 1.39 + 2 * var(--nn-dealpad));
    grid-area: deal;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-width: 0;
    height: var(--nn-boxh);
    font-size: var(--nn-dealfs);
    line-height: 1.3;
    overflow: hidden;
}
/* The trump: a large glyph over its word, in a box of one fixed size. */
.nn-trump {
    position: relative;
    box-sizing: border-box;
    flex: 0 0 var(--nn-trumpw);
    width: var(--nn-trumpw);
    height: var(--nn-boxh);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    white-space: nowrap;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.16);
}
.nn-trumpglyph {
    display: block;
    height: var(--nn-glyphfs);
    font-size: var(--nn-glyphfs);
    line-height: 1;
}
.nn-notrump .nn-trumpglyph { display: none; }
.nn-trumpword { opacity: 0.85; }
.nn-notrump .nn-trumpword { font-weight: 700; opacity: 1; white-space: normal; text-align: center; }
/* The revealed hand: its title line and one row of cards. */
.nn-open {
    box-sizing: border-box;
    min-width: 0;
    height: var(--nn-boxh);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    padding: 0 var(--nn-dealpad);
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.16);
}
.nn-open:empty { display: none; }
.nn-open-title {
    height: calc(var(--nn-dealfs) * 1.3);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
    opacity: 0.85;
}
.nn-open-cards {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: var(--nn-opengap);
    height: calc(var(--nn-opencw) * 1.39);
}
.nn-open-cards .card.nn-opencard { --cw: var(--nn-opencw); }

/* --- the center: two layers in one cell ---------------------------------- */
/* Whichever layer is not in use keeps its box but is hidden, so the cell is
   always the larger of the two. */
.nn-center {
    grid-area: center;
    display: grid;
    box-sizing: border-box;
    width: var(--nn-centerw);
    justify-self: center;
    justify-items: center;
    align-items: center;
}
.nn-center > * { grid-area: 1 / 1; }
.nn-center > .nn-off { visibility: hidden; }

/* --- the trick: a cross --------------------------------------------------- */
.nn-trick {
    display: grid;
    grid-template-columns: repeat(3, var(--nn-playcw));
    grid-auto-rows: calc(var(--nn-playcw) * 1.39);
    gap: var(--nn-trickgap);
    justify-content: center;
    justify-items: center;
    align-items: center;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.nn-board.nn-seats-3 .nn-trick { grid-template-areas: "w . e" ". s ."; }
.nn-board.nn-seats-4 .nn-trick { grid-template-areas: ". n ." "w . e" ". s ."; }
.nn-play.pos-s { grid-area: s; }
.nn-play.pos-w { grid-area: w; }
.nn-play.pos-n { grid-area: n; }
.nn-play.pos-e { grid-area: e; }
.nn-play .card { --cw: var(--nn-playcw); }
/* A slot still owed a card: outlined, so it reads as waiting. */
.nn-play .card.empty { opacity: 0.35; }
/* The seat to play, so the eye knows where the next card is coming from. */
.nn-play.onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
/* The card taking the trick, or that took the finished trick still shown:
   ringed, and glowing, so it is not a matter of color alone. */
.nn-play .card.nn-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}

/* --- the picker -------------------------------------------------------------- */
/* Four fixed rows, whatever they hold: the suit code, three pick slots (a card
   and its value under it), the live number, and the confirm button. The call
   picker spans all four with its column of buttons, so both pickers are one
   size. */
.nn-picker {
    display: grid;
    grid-template-rows:
        var(--nn-legendh)
        calc(var(--nn-pickcw) * 1.39 + 16px)
        var(--nn-readh)
        var(--nn-btnh);
    row-gap: 6px;
    justify-self: stretch;
    align-content: center;
    box-sizing: border-box;
    width: 100%;
    padding: 0 4px;
    white-space: nowrap;
}
.nn-legend {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 10px;
    font-size: 13px;
    line-height: var(--nn-legendh);
    font-variant-numeric: tabular-nums;
}
.nn-legend-item { white-space: nowrap; }
.nn-picks {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: 8px;
}
.nn-pick {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    width: var(--nn-pickcw);
}
.nn-picks .card.nn-pickcard { --cw: var(--nn-pickcw); }
.nn-pickvalue {
    height: 14px;
    font-size: 12px;
    line-height: 14px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.nn-readout {
    text-align: center;
    font-size: 14px;
    line-height: var(--nn-readh);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.nn-confirm { display: flex; justify-content: center; align-items: center; }
.nn-picker .btn.nn-layaside,
.nn-picker .btn.nn-callbtn {
    box-sizing: border-box;
    height: var(--nn-btnh);
    min-height: 0;
    margin: 0;
    padding: 0 14px;
    white-space: nowrap;
}
.nn-calls {
    grid-row: 1 / -1;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: stretch;
    gap: 8px;
    padding: 0 6px;
}
.nn-picker .btn.nn-callbtn:disabled { opacity: 0.45; text-decoration: line-through; }
.nn-picker .btn.nn-callbtn.hinted { outline: 3px solid var(--accent); outline-offset: 1px; }

/* --- a seat's panel: four lines at every size ---------------------------- */
.nn-seat {
    position: relative;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 2px;
    width: var(--nn-panelw);
    max-width: 100%;
    min-width: 0;
    padding: var(--nn-pad);
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default, so the accent ring never shifts the layout. */
    outline: 2px solid transparent;
}
.nn-seat.nn-active { outline-color: var(--accent); }
.nn-seat-head,
.nn-seat-score,
.nn-seat-bid,
.nn-seat-line {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
}
.nn-seat-head {
    line-height: 1.3;
    height: calc(var(--nn-namefs) * 1.3);
    font-size: var(--nn-namefs);
}
.nn-seat-score {
    line-height: 1.2;
    height: calc(var(--nn-totalfs) * 1.2);
    font-size: var(--nn-totalfs);
}
.nn-seat-bid { height: calc(var(--nn-bidcw) * 1.39 + 2px); }
.nn-seat-line {
    height: calc(var(--nn-stdfs) * 1.6 + 2px);
    font-size: var(--nn-stdfs);
}
.nn-seat-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: 700;
}
/* Whose turn, as a shape as well as the ring. Always rendered, so the name
   never shifts sideways when the turn moves. */
.nn-toact { visibility: hidden; color: var(--accent); }
.nn-seat.nn-active .nn-toact { visibility: visible; }
.nn-seat-total { font-weight: 800; font-variant-numeric: tabular-nums; }
/* Always rendered, so the name sits in one place in every deal. */
.nn-dealer {
    position: relative;
    visibility: hidden;
    font-size: calc(var(--nn-namefs) * 0.8);
    font-weight: 700;
    line-height: 1.2;
    padding: 0 3px;
    border: 1px solid var(--accent);
    border-radius: 4px;
    color: var(--accent);
}
.nn-seat.nn-isdealer .nn-dealer { visibility: visible; }
/* The bid line: three small cards and the premium chip's slot. */
.nn-bidslots { display: flex; gap: 3px; }
.nn-seat-bid .card.nn-bidcard { --cw: var(--nn-bidcw); }
/* Painted only on the premium seat, so the bid cards sit on the panel's axis
   everywhere else; the line's height is reserved either way. */
.nn-premchip:empty { display: none; }
.nn-premchip {
    position: relative;
    box-sizing: border-box;
    padding: 0 5px;
    font-size: var(--nn-chipfs);
    line-height: 1.5;
    text-align: center;
    border-radius: 999px;
    border: 1px solid transparent;
}
.nn-premchip.nn-prem-declare { border-color: var(--accent); color: var(--accent); }
.nn-premchip.nn-prem-reveal { border-color: var(--accent); background: var(--accent); color: var(--accent-ink); font-weight: 700; }
.nn-chip-short { display: none; }
/* The standing: hidden (dashed), short (outlined), exact (filled), past the
   bid (struck through), so it is not a matter of color alone. */
.nn-stdbox {
    position: relative;
    line-height: 1.6;
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    padding: 0 5px;
    font-variant-numeric: tabular-nums;
}
.nn-stdbox.nn-std-none { visibility: hidden; }
.nn-stdbox.nn-std-none::before { content: "\00a0"; }
.nn-stdbox.nn-std-hidden { border-style: dashed; opacity: 0.85; }
.nn-stdbox.nn-std-bid,
.nn-stdbox.nn-std-short { border-color: rgba(255, 255, 255, 0.45); }
.nn-stdbox.nn-std-choosing,
.nn-stdbox.nn-std-calling { border-color: var(--accent); color: var(--accent); }
.nn-stdbox.nn-std-exact {
    border-color: rgba(60, 200, 120, 0.75);
    background: rgba(60, 160, 90, 0.3);
    font-weight: 700;
}
.nn-stdbox.nn-std-over {
    border-color: rgba(255, 138, 128, 0.8);
    color: #ffd7d2;
    text-decoration: line-through;
}

/* --- the hand ---------------------------------------------------------- */
/* A card that cannot be picked or played now: dimmed rather than removed. */
.nn-handpanel .nn-mute { opacity: 0.4; }
/* A revealer's own cards wear the shared corner eye (.eye-badge). Two classes,
   so it out-specifies .card-fly's position on a ghost; def.ts strips the class
   from ghosts too (panels.ts eyeBadge). */
.card.nn-badged { position: relative; }

/* --- the summary ------------------------------------------------------ */
/* A real table, one row per seat, centered as a block. Every cell carries its
   own alignment, because the lifted dialog centers text and cells inherit it. */
.nn-result {
    border-collapse: collapse;
    margin: 6px auto 0;
    font-size: 14px;
}
.nn-result th,
.nn-result td {
    padding: 3px 8px;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    border-bottom: 1px solid var(--panel-border);
}
/* The same table is each finished deal's body in the grouped log, where
   nothing under the heading is bold (ui-conventions). So a header cell is
   normal weight by default, and the header and Total weights belong to the
   summary alone. */
.nn-result th { font-size: 12px; font-weight: 400; opacity: 0.8; }
.nn-summary .nn-result th { font-weight: 600; }
.nn-result .nn-name { text-align: left; }
.nn-result .nn-num { text-align: right; }
.nn-result .nn-bid { text-align: left; }
.nn-summary .nn-result td.nn-total { font-weight: 800; }
.nn-result tr.nn-maderow td.nn-name { color: var(--accent); }
/* The bid cell: the three cards, then the number in a fixed slot, so the
   numbers line up down the column. */
.nn-bidcards { display: inline-flex; gap: 2px; vertical-align: middle; margin-right: 6px; }
.nn-result .card.nn-rescard { --cw: 22px; }
.nn-bidnum { display: inline-block; min-width: 4em; text-align: right; vertical-align: middle; }
/* Took: every cell reserves the made bid's check slot, and the header is
   padded by the same slot so it sits over the numbers. */
.nn-result { --nn-markw: 11px; }
.nn-result .nn-took { position: relative; }
.nn-result th.nn-took { padding-right: calc(8px + var(--nn-markw)); }
.nn-result .nn-mademark {
    display: inline-block;
    width: var(--nn-markw);
    text-align: right;
    color: var(--accent);
}
/* In the log the table sits in a narrow panel, so it is tightened there, and
   only there. The dialog keeps the sizes above. */
#log-entries .nn-result { --nn-markw: 9px; font-size: 12px; margin-top: 4px; }
#log-entries .nn-result th,
#log-entries .nn-result td { padding: 2px 4px; }
#log-entries .nn-result th { font-size: 11px; }
#log-entries .nn-result th.nn-took { padding-right: calc(4px + var(--nn-markw)); }
#log-entries .nn-result .nn-bidnum { min-width: 3.4em; }
.nn-log-win { color: var(--accent); font-weight: 700; }

/* --- the unseen cards (renderUnseen) ------------------------------------ */
/* The pack as a grid below the hand: a row a suit, a column a rank, unseen
   cards face up in the simplified atlas (they are small), the rest as faint
   blanks. A card that must be in a bid wears the accent ring. */
.nn-unseen { --cw: 28px; }
.nn-unseenhead {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: baseline;
    gap: 4px 10px;
    margin-bottom: 6px;
}
.nn-unseentitle { margin: 0; }
.nn-unseencount,
.nn-unseennote { font-size: 0.85rem; opacity: 0.75; white-space: nowrap; }
.nn-unseengrid {
    display: grid;
    grid-template-columns: 16px repeat(var(--nn-ucols), var(--cw));
    gap: 3px 4px;
    justify-content: center;
    align-items: center;
}
.nn-unseengrid .card.mini { --cw: inherit; }
.nn-unseenrank,
.nn-unseensuit {
    text-align: center;
    font-size: 12px;
    font-weight: 700;
    opacity: 0.8;
}
.nn-unseenknown {
    background: none;
    border: 1px dashed rgba(255, 255, 255, 0.18);
    box-shadow: none;
}
.nn-unseengrid .card.nn-inbid { outline: 2px solid var(--accent); outline-offset: 1px; }
@media (max-width: 720px) {
    .nn-unseen { --cw: 22px; }
    .nn-unseengrid { grid-template-columns: 14px repeat(var(--nn-ucols), var(--cw)); gap: 3px 2px; }
}

/* --- a finished trick in the log (trickCross) ---------------------------- */
/* Hearts' cross in this board's places: the viewer's card at the bottom and
   the others where they sit, a caret on the lead and a check on the winner.
   A fixed footprint whatever the trick holds. */
.nn-trickcross {
    --tx-cw: 19px;
    display: grid;
    grid-template-columns: repeat(3, var(--tx-cw));
    grid-template-rows: repeat(3, calc(var(--tx-cw) * 1.39));
    grid-template-areas: '. up .' 'left . right' '. down .';
    align-items: center;
    justify-items: center;
    gap: 1px 4px;
    padding: 0 9px;
    flex: none;
}
.nn-trickcross.seats-3 {
    grid-template-rows: repeat(2, calc(var(--tx-cw) * 1.39));
    grid-template-areas: 'left . right' '. down .';
}
.nn-trickcross .card { --cw: var(--tx-cw); }
.nn-trickcross-cell { position: relative; line-height: 0; }
.nn-trickcross-cell.pos-up { grid-area: up; }
.nn-trickcross-cell.pos-left { grid-area: left; }
.nn-trickcross-cell.pos-right { grid-area: right; }
.nn-trickcross-cell.pos-down { grid-area: down; }
.nn-trickcross-led,
.nn-trickcross-won {
    position: absolute;
    font-size: 10px;
    line-height: 1;
    font-weight: 800;
    pointer-events: none;
}
.nn-trickcross-led { top: -1px; left: -8px; color: var(--ink); opacity: 0.85; }
.nn-trickcross-won { bottom: -1px; right: -8px; color: var(--accent); }
#log-entries .nn-logtrick {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 3px 0;
}
.nn-logtrick-cap {
    font-size: 12px;
    min-width: 0;
    flex: 1 1 auto;
    opacity: 0.85;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --- the Examples in the Cheat_Sheet ------------------------------------ */
.nn-extable .num { text-align: right; font-variant-numeric: tabular-nums; }

/* --- larger desktop windows ---------------------------------------------- */
/* The owner asked for the board to grow with a bigger window (2026-11-05).
   Every size above is its value at a 1280x720 window, and here each one
   scales with the smaller of the window's width over 1280 and its height over
   720, never below that value and at most 1.45 times it. So at 1280x720 the
   board is exactly as before, and at 1920x1080 it is 1.45 times as large.
   The hand already grows with the width (--cw in platform.css, to 96px),
   which keeps 12 cards on one row; grown further they wrapped. Above
   the phone width only; the app layout's fit still shrinks the play area if
   a window is wide but short. The ragged edges of a fluid size round
   per element, which nothing here relies on. */
@media (min-width: 951px) {
    .nn-board {
        --nn-playcw: clamp(56px, min(4.375vw, 7.778vh), 81.2px);
        --nn-trickgap: clamp(6px, min(0.469vw, 0.833vh), 8.7px);
        --nn-bidcw: clamp(26px, min(2.031vw, 3.611vh), 37.7px);
        --nn-opencw: clamp(34px, min(2.656vw, 4.722vh), 49.3px);
        --nn-pickcw: clamp(40px, min(3.125vw, 5.556vh), 58px);
        --nn-centerw: clamp(224px, min(17.5vw, 31.111vh), 324.8px);
        --nn-panelw: clamp(176px, min(13.75vw, 24.444vh), 255.2px);
        --nn-namefs: clamp(13px, min(1.016vw, 1.806vh), 18.85px);
        --nn-totalfs: clamp(15px, min(1.172vw, 2.083vh), 21.75px);
        --nn-stdfs: clamp(11px, min(0.859vw, 1.528vh), 15.95px);
        --nn-chipfs: clamp(11px, min(0.859vw, 1.528vh), 15.95px);
        --nn-dealfs: clamp(12px, min(0.938vw, 1.667vh), 17.4px);
        --nn-dealpad: clamp(6px, min(0.469vw, 0.833vh), 8.7px);
        --nn-trumpw: clamp(64px, min(5vw, 8.889vh), 92.8px);
        --nn-glyphfs: clamp(28px, min(2.188vw, 3.889vh), 40.6px);
        --nn-btnh: clamp(32px, min(2.5vw, 4.444vh), 46.4px);
        --nn-legendh: clamp(18px, min(1.406vw, 2.5vh), 26.1px);
        --nn-readh: clamp(18px, min(1.406vw, 2.5vh), 26.1px);
    }
}

/* --- touch screens above the phone width ---------------------------------- */
/* A taller button where the pointer is a finger. The picker's rows are
   derived from it, so they grow with it. */
@media (pointer: coarse) {
    .nn-board { --nn-btnh: 36px; }
}

/* --- phones: one row across the top ------------------------------------- */
/* Every reservation is re-derived here. The board is a flex column: the deal
   strip, the other seats in one row in play order (West to East), the center
   at its fixed width, and this player's panel.
     strip at 360px: the hand block is 10 cards * 25px + 9 * 2px + 2 * 6px
       padding = 280px, the trump 48px, and a 6px gap: 334px, inside the 340px
       board a 360px screen leaves (measured in Chromium)
     center: 3 * 44px + 2 * 4px + 16px = 156px; the call buttons at 13px about
       176px, so --nn-centerw is 200px */
@media (max-width: 720px) {
    .nn-board {
        --nn-playcw: 44px;
        --nn-trickgap: 4px;
        --nn-bidcw: 20px;
        --nn-opencw: 25px;
        --nn-pickcw: 36px;
        --nn-centerw: 200px;
        --nn-namefs: 12px;
        --nn-totalfs: 14px;
        --nn-stdfs: 11px;
        --nn-chipfs: 10px;
        --nn-pad: 4px 5px 5px;
        --nn-dealfs: 11px;
        --nn-trumpw: 48px;
        --nn-glyphfs: 22px;
        --nn-btnh: 36px;
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
    }
    /* Three status lines reserved here, not two: the call path's two longest
       statuses ("X reveals over your declare. Reveal to keep the premium, or
       pass." and "X declares. Pass, or reveal to overcall.") take two lines
       at 390 and 360px, and a hint adds a third (measured with "Bot North",
       the longest bot name). The line height is set so the reservation is
       exact: two 18px lines and one 14px hint line at 1.4. */
    .status.nn-status { line-height: 1.4; min-height: calc(2 * 1.4em + 1.4 * 14px); }
    .nn-deal { --nn-opengap: 2px; gap: 6px; }
    .nn-table {
        display: flex;
        flex-wrap: nowrap;
        justify-content: center;
        gap: 4px;
    }
    .nn-table .nn-seat { flex: 1 1 0; width: auto; }
    .nn-center { margin-inline: auto; }
    .nn-seat.nn-mine { align-self: center; width: var(--nn-panelw); }
    .nn-trick { padding: 6px 8px; }
    .nn-picker .btn.nn-layaside,
    .nn-picker .btn.nn-callbtn { padding: 0 10px; font-size: 13px; }
    .nn-calls { gap: 6px; padding: 0 2px; }
    .nn-legend { gap: 8px; font-size: 12px; }
    /* The premium chip's short form, so three bid cards and the chip fit a
       third of a 360px row. */
    .nn-premchip { padding: 0 3px; }
    .nn-chip-full { display: none; }
    .nn-chip-short { display: inline; }
    /* The summary table on a phone: at the desktop sizes it ran 423px wide in
       a 357px dialog at 390px (493 in 329 at 360px with a Premium column),
       so Deal and Total sat behind a sideways scroll. The bid cards stack
       over the bid's number, a two-word name may wrap, and the cells
       tighten. */
    .nn-summary .nn-result { --nn-markw: 9px; font-size: 12px; }
    .nn-summary .nn-result th,
    .nn-summary .nn-result td { padding: 3px 2px; }
    .nn-summary .nn-result td.nn-name { white-space: normal; }
    .nn-summary .nn-result th { font-size: 11px; }
    .nn-summary .nn-result th.nn-took { padding-right: calc(2px + var(--nn-markw)); }
    .nn-summary .nn-result .nn-bidcards { display: flex; gap: 1px; width: max-content; margin: 0 0 2px; }
    .nn-summary .nn-result .card.nn-rescard { --cw: 18px; }
    .nn-summary .nn-result .nn-bidnum { display: block; min-width: 0; text-align: left; }
}
/* ==========================================================================
   Piquet (src/piquet/, design.md "The UI"; ui-conventions.md)

   One centered column: the opponent's line, the opponent's known cards (with
   the carte blanche proof), the trick, the talon and this player's discards,
   this player's line, and the declarations table; then the actions row and
   the hand. EVERY ROW HAS A FIXED HEIGHT from the deal's first repaint, so
   nothing moves while a deal is played ("Containers must not move"): strips
   are one card row plus a thin scrollbar, labels and table lines are single
   nowrap lines cut by an ellipsis, the trick is a fixed box, and the actions
   row is a fixed height whatever it holds.

   Sizes come from custom properties on .pqt-board, re-derived at the phone
   breakpoint platform.css uses for .card.mini (560px):
     --pqt-mcw     strip cards (mini, simplified atlas)
     --pqt-striph  a strip's fixed height: one card row plus 10px of scrollbar
     --pqt-label   a zone label's fixed height
     --pqt-trickcw the two trick cards (full art)

   Shared ids are scoped through a class this game owns (.pqt-handpanel,
   .pqt-status, .pqt-board), never named bare. Every class is pqt-.
   ========================================================================== */

/* --- colors ------------------------------------------------------------- */
/* Suit glyphs, colored wherever they appear (suits decide the point and the
   sequences). `#log-entries .red` and its companions already cover the log. */
.pqt-status .red, .pqt-board .red, .pqt-actions .red, .pqt-result .red,
.pqt-handpanel .red { color: var(--red); }
.pqt-status .red.diamond, .pqt-board .red.diamond, .pqt-actions .red.diamond,
.pqt-result .red.diamond, .pqt-handpanel .red.diamond { color: var(--red-diamond); }
.pqt-status .club, .pqt-board .club, .pqt-actions .club, .pqt-result .club,
.pqt-handpanel .club { color: var(--blue-club); }

/* --- status and progress ------------------------------------------------ */
/* One status line above 560px and two below; the progress line one line. */
.status.pqt-status { line-height: 1.3; min-height: 1.3em; }
.progress.pqt-progress { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* --- the board ---------------------------------------------------------- */
.pqt-board {
    --pqt-mcw: 46px;
    --pqt-striph: calc(var(--pqt-mcw) * 89 / 64 + 10px);
    --pqt-label: 1.3em;
    --pqt-trickcw: 72px;
    display: grid;
    justify-items: center;
    gap: 6px;
    width: 100%;
    max-width: 960px;
    margin-inline: auto;
}

/* Rows 1 and 5: one nowrap line; the name is the one item that shrinks, so a
   long online name cannot push the line onto a second row. */
.pqt-seat {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 8px;
    height: 26px;
    max-width: 100%;
    white-space: nowrap;
}
.pqt-seat > * { flex: none; }
.pqt-seat > .pqt-name {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: 600;
}
.pqt-role { font-size: 0.8rem; opacity: 0.75; }
.pqt-pts { font-variant-numeric: tabular-nums; }
/* The tricks pile: one fixed-size back with the count on it, an outline at 0.
   The vanish target of a won trick, so it never names a card. */
.pqt-pile {
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 24px;
    border-radius: 3px;
    background: #3949ab;
    border: 1px solid rgba(0, 0, 0, 0.4);
    color: #fff;
    font-size: 0.75rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.pqt-pile.pqt-pileempty { background: transparent; border: 1px dashed rgba(255, 255, 255, 0.35); color: rgba(255, 255, 255, 0.6); }
.pqt-chip {
    font-size: 0.75rem;
    padding: 1px 7px;
    border-radius: 10px;
    background: rgba(255, 214, 102, 0.18);
    border: 1px solid rgba(255, 214, 102, 0.45);
}

/* Rows 2 and 4: a fixed label line over a fixed-height strip. */
.pqt-zone { width: 100%; min-width: 0; }
.pqt-label {
    height: var(--pqt-label);
    line-height: var(--pqt-label);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 0.8rem;
    opacity: 0.8;
    text-align: center;
}
/* Centered on the board axis without clipping (ui-conventions, "Center, do not
   left-align"): the strip is as wide as its cards and centered, and only when
   they outgrow the zone does it reach 100% and scroll, from its left edge.
   justify-content stays flex-start, since centering inside a scrolling box
   clips the left overflow. The same pattern as .pqt-actrow. */
.pqt-strip {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: flex-start;
    gap: 3px;
    box-sizing: border-box;
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
    height: var(--pqt-striph);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}
.pqt-strip .card.mini { --cw: var(--pqt-mcw); flex: none; }
.pqt-proof { display: inline-flex; flex: none; align-items: flex-start; gap: 3px; margin-right: 8px; }
.pqt-prooflabel {
    flex: none;
    line-height: calc(var(--pqt-mcw) * 89 / 64);
    font-size: 0.75rem;
    white-space: nowrap;
    opacity: 0.8;
}

/* Row 3: the trick, a fixed box. The opponent's slot is at the top left and
   the viewer's at the bottom right, so each card reads by the side it lands
   on. The two cards overlap: at 72px cards (above 560px) by about 29 x 55 px
   (72 - 0.6 x 72 across, 100 - 0.45 x 100 down; plan amendment 8.4), and at
   52px by about 21 x 40 px. So
   the viewer's slot is stacked on top: its top-left index shows over the
   opponent's card, and the opponent's top-left index stays clear of the
   viewer's card at both sizes. */
.pqt-mid { display: flex; justify-content: center; width: 100%; }
.pqt-trick {
    position: relative;
    width: calc(var(--pqt-trickcw) * 1.6);
    height: calc(var(--pqt-trickcw) * 89 / 64 * 1.45);
}
.pqt-slot {
    position: absolute;
    box-sizing: border-box;
    width: var(--pqt-trickcw);
    height: calc(var(--pqt-trickcw) * 89 / 64);
}
.pqt-trick .pqt-slotopp { top: 0; left: 0; z-index: 0; }
.pqt-trick .pqt-slotme { right: 0; bottom: 0; z-index: 1; }
.pqt-slot.pqt-slotempty { border: 1px dashed rgba(255, 255, 255, 0.28); border-radius: 6px; }
.pqt-slot .card { --cw: var(--pqt-trickcw); }
/* The winner of a held trick: an outline, which takes no space. */
.pqt-slot .card.pqt-won { outline: 3px solid var(--accent); outline-offset: 1px; }

/* Row 4: the talon and the discards side by side, stacked on a phone. */
.pqt-zones {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 12px;
    width: 100%;
}

/* Row 6: the declarations table. A real table with the game's own class:
   fixed columns, and every line a fixed block, so no text can change its
   height (in table layout a row's height is only a minimum). */
table.pqt-decl {
    width: 100%;
    max-width: 560px;
    table-layout: fixed;
    border-collapse: collapse;
    margin: 0 auto;
    font-size: 0.8rem;
    font-variant-numeric: tabular-nums;
}
table.pqt-decl col.pqt-ccat { width: 19%; }
table.pqt-decl col.pqt-cseat { width: 27%; }
/* Scores 14% and Points 13% (the design's 16/11 cut the "Points" header at
   390px: 41px is narrower than the word plus its padding). */
table.pqt-decl col.pqt-cscore { width: 14%; }
table.pqt-decl col.pqt-cpts { width: 13%; }
table.pqt-decl th, table.pqt-decl td {
    padding: 2px 4px;
    vertical-align: top;
    border-bottom: 1px solid rgba(255, 255, 255, 0.12);
    font-weight: 400;
}
table.pqt-decl thead th { font-weight: 600; opacity: 0.85; }
table.pqt-decl tbody th { font-weight: 600; }
table.pqt-decl .pqt-tname { text-align: left; }
table.pqt-decl .pqt-tnum { text-align: right; }
table.pqt-decl tfoot td { border-bottom: none; opacity: 0.85; }
table.pqt-decl tr.pqt-now { background: rgba(255, 214, 102, 0.12); }
/* 1.45em, not 1.25em: the page font's content area is about 1.41em tall, so a
   1.25em line overflowed its own box by a pixel (scrollHeight 17 against
   clientHeight 16, probe check 5) and could clip a descender. */
.pqt-ln {
    display: block;
    height: 1.45em;
    line-height: 1.45em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --- the actions row ---------------------------------------------------- */
/* A FIXED height (review finding 6): the 38px button plus 16px for a thin
   horizontal scrollbar, at every breakpoint, the 36px coarse-pointer minimum
   included (the buttons are 38px everywhere). The outer box scrolls from its
   left edge; the inner row is fit-content and centered, so it centers when it
   fits. An empty decision leaves the row empty at this height. */
.pqt-actions {
    box-sizing: border-box;
    height: 54px;
    width: 100%;
    max-width: 960px;
    margin-inline: auto;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
    padding-top: 6px;
}
.pqt-actrow {
    display: flex;
    flex-wrap: nowrap;
    gap: 6px;
    width: fit-content;
    margin-inline: auto;
}
.pqt-actions .btn.pqt-act {
    box-sizing: border-box;
    flex: none;
    height: 38px;
    min-height: 38px;
    padding: 0 12px;
    white-space: nowrap;
}
.pqt-actions .btn.pqt-toggle { opacity: 0.7; }
.pqt-actions .btn.pqt-toggle.pqt-on {
    opacity: 1;
    background: rgba(255, 214, 102, 0.2);
    border-color: rgba(255, 214, 102, 0.7);
}
.pqt-actions .btn.hinted { outline: 3px solid var(--accent); outline-offset: 2px; }

/* --- the hand ----------------------------------------------------------- */
.pqt-handpanel #hand-cards .card.pqt-mute { opacity: 0.45; }
/* The cards of a claim being chosen or hovered. */
.pqt-handpanel #hand-cards .card.pqt-claimcard { outline: 3px solid var(--accent); outline-offset: 1px; }
/* The eye badge on a card the opponent can be sure of: positioned by its card,
   a TWO-class rule so it out-specifies a flight ghost's position (panels.ts). */
.card.pqt-badged { position: relative; }

/* --- the unseen cards (renderUnseen) ------------------------------------ */
/* A 4 x 8 grid of the pack below the hand: unseen cards face up in the
   simplified atlas (they are small), the rest as faint blanks. */
.pqt-unseen { --cw: 30px; }
.pqt-unseenhead {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: baseline;
    gap: 4px 10px;
    margin-bottom: 6px;
}
.pqt-unseentitle { margin: 0; }
.pqt-unseencount { font-size: 0.85rem; opacity: 0.75; }
.pqt-unseengrid {
    display: grid;
    grid-template-columns: 16px repeat(8, var(--cw));
    gap: 3px 4px;
    justify-content: center;
    align-items: center;
}
.pqt-unseengrid .card.mini { --cw: inherit; }
.pqt-unseenrank,
.pqt-unseensuit {
    text-align: center;
    font-size: 12px;
    font-weight: 700;
    opacity: 0.8;
}
.pqt-unseenknown {
    background: none;
    border: 1px dashed rgba(255, 255, 255, 0.18);
    box-shadow: none;
}
/* --- the result panel --------------------------------------------------- */
table.pqt-dealsum, table.pqt-partie {
    border-collapse: collapse;
    margin: 0 auto 10px;
    font-variant-numeric: tabular-nums;
    min-width: 240px;
}
table.pqt-dealsum th, table.pqt-dealsum td, table.pqt-partie th, table.pqt-partie td {
    padding: 3px 10px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}
table.pqt-dealsum .pqt-tname, table.pqt-partie .pqt-tname { text-align: left; }
table.pqt-dealsum .pqt-tnum, table.pqt-partie .pqt-tnum { text-align: right; }
table.pqt-dealsum col.pqt-clabel { width: 9em; }
table.pqt-dealsum col.pqt-cnum, table.pqt-partie col.pqt-cnum { width: 6em; }
table.pqt-partie col.pqt-cdeal { width: 4em; }
table.pqt-partie col.pqt-cdealer { width: 7em; }
table.pqt-dealsum tr.pqt-totalrow, table.pqt-partie tr.pqt-totalrow { font-weight: 600; }
/* A total is ruled off from the rows it adds up. */
table.pqt-dealsum tr.pqt-totalrow > *, table.pqt-partie tr.pqt-totalrow > * {
    border-top: 2px solid rgba(255, 255, 255, 0.5);
}
.pqt-sumtitle { text-align: center; margin: 8px 0 4px; }

/* The rules' scoring table. */
table.pqt-ruletable { border-collapse: collapse; margin: 0 auto 10px; }
table.pqt-ruletable th, table.pqt-ruletable td { padding: 2px 8px; text-align: left; }
table.pqt-ruletable .num { text-align: right; font-variant-numeric: tabular-nums; }

/* The talon's face-down cards overlap in a fan, so eight backs fit beside the
   discards; one element per card still, and a face-up card keeps its full width. */
.pqt-talonstrip .card.back + .card.back { margin-left: calc(var(--pqt-mcw) * -0.6); }
/* --- THE WHOLE HAND ON SCREEN (ui-conventions "Everything you play with is on screen") ---
   Measured without scrolling at 1280x720, 1440x800, 1512x860, and 390x844, in
   the exchange (the 12-card hand), the declarations, and the play.
   On a wide screen the board is two columns: the opponent's line, the known
   cards, the trick, and your line on the left; the talon and your discards,
   then the declarations table, on the right. That halves its height. The hand
   is one row of 12, its cards no wider than the tray allows (cqw: the tray is
   the container). */
@media (min-width: 900px) {
    .pqt-board {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
        grid-template-areas: "opp zones" "known zones" "mid decl" "me decl";
        align-items: start;
        column-gap: 20px;
    }
    .pqt-board > .pqt-opp { grid-area: opp; }
    .pqt-board > .pqt-knownzone { grid-area: known; }
    .pqt-board > .pqt-mid { grid-area: mid; }
    .pqt-board > .pqt-zones { grid-area: zones; }
    .pqt-board > .pqt-me { grid-area: me; }
    .pqt-board > .pqt-decl { grid-area: decl; }
}
@media (min-width: 561px) {
    .pqt-handpanel { --pqt-handcw: var(--cw); }
    .pqt-handpanel #hand-cards { container-type: inline-size; }
    .pqt-handpanel #hand-cards .card { --cw: min(var(--pqt-handcw), calc((100cqw - 88px) / 12)); }
}
/* --- the phone breakpoint ----------------------------------------------- */
/* One column. To keep the two-row hand on a 390x844 screen: the talon and the
   discards sit side by side, the known cards' label sits beside its strip, the
   trick's two cards overlap more (the opponent's index stays clear of yours,
   which sits 0.6 of a card to the right), and the rows are 3px apart. */
@media (max-width: 560px) {
    .pqt-board { --pqt-mcw: 34px; --pqt-trickcw: 52px; gap: 3px; }
    .pqt-zones { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 6px; }
    .pqt-knownzone { display: flex; align-items: center; gap: 6px; }
    .pqt-knownzone > .pqt-label { flex: none; max-width: 38%; }
    .pqt-knownzone > .pqt-strip { flex: 1 1 auto; min-width: 0; margin-inline: 0; }
    .pqt-trick { height: calc(var(--pqt-trickcw) * 89 / 64 * 1.25); }
    .pqt-handpanel #hand-cards { gap: 4px; }
    .status.pqt-status { min-height: 2.6em; }
}
/* =========================================================================
   Carousel (carousel)
   A manipulation rummy for 2 to 5: lay hand cards onto a shared table whose
   melds anyone may rearrange, as long as every card ends in a meld.
     the seats row: one chip per seat, the viewer first, then the stock chip
       (.crsl-seats, .crsl-seat, .crsl-stock)
     the table area: a fixed-height box of meld rows that scrolls inside
       itself (.crsl-tablewrap, .crsl-table, .crsl-meld, .crsl-mslot)
     the Joker panel: an absolute overlay of the table area only (#crsl-jokers)
     the action bar: four fixed slots (.crsl-actions, .crsl-slot)
     a staged Joker's caption inside its own card box (.crsl-jcap)
   CONTAINERS MUST NOT MOVE. The table area is three meld rows tall whatever
   it holds, the action row reserves its own height, the status line reserves
   its lines, and #hand-cards gets no min-height: src/shared/ui/handspace.ts
   reserves it at runtime.
   SHARED IDS only through a class this game owns: .crsl-handpanel on the hand
   panel, .crsl-status and .crsl-progress added by the renderers, .crsl-result
   on the result panel.
   ========================================================================= */

/* --- status and progress ------------------------------------------------ */
.status.crsl-status { min-height: 1.4em; }
.status.crsl-status.crsl-turn { color: var(--accent); }
.crsl-hintnote { font-weight: 400; }
.progress.crsl-progress { min-height: 20px; white-space: nowrap; }
.crsl-status .red, .crsl-jokers .red, .crsl-table .red, .crsl-handpanel .red, .crsl-result .red { color: var(--red); }
.crsl-status .red.diamond, .crsl-jokers .red.diamond, .crsl-table .red.diamond,
.crsl-handpanel .red.diamond, .crsl-result .red.diamond { color: var(--red-diamond); }
.crsl-status .club, .crsl-jokers .club, .crsl-table .club, .crsl-handpanel .club, .crsl-result .club { color: var(--blue-club); }

/* --- the board ----------------------------------------------------------- */
.crsl-board {
    --crsl-meldcw: 34px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    margin-bottom: 10px;
}
@media (min-width: 1000px) {
    .crsl-board { --crsl-meldcw: 36px; }
}

/* --- seats row ----------------------------------------------------------- */
.crsl-seats {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 6px;
    min-height: 34px;
}
.crsl-seat {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 9px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.08);
    border: 2px solid transparent;
    white-space: nowrap;
    font-size: 14px;
}
.crsl-seat.crsl-active { border-color: var(--accent); }
.crsl-seatname { font-weight: 600; }
.crsl-seattotal { font-variant-numeric: tabular-nums; font-weight: 700; }
.crsl-dealer {
    font-size: 11px;
    font-weight: 700;
    padding: 0 4px;
    border-radius: 4px;
    background: rgba(255, 255, 255, 0.2);
}
.crsl-seatcount { font-variant-numeric: tabular-nums; opacity: 0.8; }
.crsl-seatcount { display: inline-flex; align-items: center; gap: 3px; }
.crsl-seatcount .card.crsl-countback { --cw: 12px; box-shadow: none; }
.crsl-stock {
    display: flex;
    align-items: center;
    gap: 5px;
    padding: 2px 6px;
    white-space: nowrap;
    font-size: 13px;
}
.crsl-stock .crsl-stockcard { --cw: 20px; }
.crsl-stockcount { font-variant-numeric: tabular-nums; }

/* --- the table area ------------------------------------------------------ */
/* Three meld rows: a card (meldcw x 1.39) plus its 14px caption line and the
   meld box's own 8px, plus 24px for the area's padding and gaps. */
.crsl-tablewrap {
    position: relative;
    width: 100%;
    max-width: 1000px;
    height: calc(3 * (var(--crsl-meldcw) * 1.39 + 22px) + 24px);
}
.crsl-table {
    box-sizing: border-box;
    height: calc(3 * (var(--crsl-meldcw) * 1.39 + 22px) + 24px);
    overflow-y: auto;
    scrollbar-gutter: stable;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-content: flex-start;
    gap: 6px;
    padding: 6px;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.12);
}
.crsl-empty {
    width: 100%;
    margin: auto 0;
    text-align: center;
    opacity: 0.7;
}
.crsl-meld {
    display: flex;
    flex-wrap: wrap;
    max-width: 100%;
    gap: 2px;
    padding: 3px;
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.1);
}
.crsl-mslot {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: var(--crsl-meldcw);
}
.crsl-table .card.crsl-mcard { --cw: var(--crsl-meldcw); }
.crsl-table .card.crsl-added { outline: 2px solid #ffd166; outline-offset: 1px; }
.crsl-table .card.crsl-moved { outline: 2px solid #5aa9ff; outline-offset: 1px; }
.crsl-table .card.crsl-releasable { cursor: pointer; box-shadow: 0 0 0 2px var(--accent); }
.crsl-mcap {
    height: 14px;
    line-height: 14px;
    font-size: 10px;
    font-weight: 700;
    white-space: nowrap;
}
@media (max-width: 520px) {
    .crsl-board { --crsl-meldcw: 28px; }
    .crsl-mcap { font-size: 9px; }
    /* Two rows of chips at 4 or 5 seats. */
    .crsl-board.crsl-seats-4 .crsl-seats,
    .crsl-board.crsl-seats-5 .crsl-seats { min-height: 74px; }
    .status.crsl-status { min-height: 2.8em; }
}

/* --- the Joker panel: over the table area only --------------------------- */
.crsl-jokers {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    max-height: 100%;
    overflow-y: auto;
    z-index: 5;
    box-sizing: border-box;
    padding: 8px 10px;
    border-radius: 10px;
    background: var(--panel-bg, #20352a);
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
}
.crsl-jokers[hidden] { display: none; }
.crsl-jhead {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 12px;
    margin-bottom: 6px;
    font-weight: 700;
}
.crsl-jrow {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 4px;
    margin: 4px 0;
}
.crsl-jlabel { font-weight: 600; margin-right: 4px; white-space: nowrap; }
.crsl-jnone { opacity: 0.75; }
.crsl-jokers .btn.crsl-face { white-space: nowrap; font-weight: 700; background: #f2efe9; color: #1a1a1a; }
.crsl-jokers .btn.crsl-face[aria-pressed="true"] { outline: 3px solid var(--accent); outline-offset: 1px; }

/* --- the action bar: four fixed slots ------------------------------------ */
.crsl-actions {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: 6px;
    min-height: 40px;
}
.crsl-actions .btn.crsl-slot { white-space: nowrap; }
.crsl-actions .crsl-draw,
.crsl-actions .crsl-end { min-width: 118px; }
.crsl-actions .crsl-laydown,
.crsl-actions .crsl-knock { min-width: 92px; }
.crsl-actions .crsl-jokerbtn { min-width: 70px; }
.crsl-actions .crsl-clear { min-width: 60px; }
.crsl-actions .btn.hinted { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 389px) {
    .crsl-actions { flex-wrap: wrap; min-height: 84px; align-content: flex-start; }
}
@media (max-width: 520px) {
    .crsl-actions { gap: 4px; }
    .crsl-actions .btn.crsl-slot { padding-left: 8px; padding-right: 8px; }
    .crsl-actions .crsl-draw,
    .crsl-actions .crsl-end { min-width: 104px; }
    .crsl-actions .crsl-laydown,
    .crsl-actions .crsl-knock { min-width: 78px; }
    .crsl-actions .crsl-jokerbtn { min-width: 60px; }
    .crsl-actions .crsl-clear { min-width: 52px; }
}

/* --- the hand ------------------------------------------------------------- */
.crsl-handpanel #hand-cards .card.crsl-new { outline: 2px dashed #ffd166; outline-offset: 1px; }
/* On your turn, a card no lay-down this turn can use: faded, and not armed. */
.crsl-handpanel #hand-cards .card.crsl-unplayable { opacity: 0.45; }
/* Zero specificity, so a flight ghost's `.card-fly { position: fixed }` wins. */
:where(.crsl-handpanel .card.crsl-jstaged) { position: relative; }
.crsl-handpanel .crsl-jcap {
    position: absolute;
    left: 50%;
    bottom: 4%;
    translate: -50% 0;
    padding: 1px 4px;
    border-radius: 4px;
    background: rgba(255, 255, 255, 0.9);
    color: #1a1a1a;
    font-size: 11px;
    font-weight: 700;
    line-height: 1.2;
    white-space: nowrap;
    pointer-events: none;
}
.card-fly > .crsl-jcap { display: none; }

/* --- the result panel ------------------------------------------------------ */
.crsl-deal,
.crsl-final {
    border-collapse: collapse;
    margin: 0 auto 10px;
}
.crsl-deal th, .crsl-deal td,
.crsl-final th, .crsl-final td { padding: 4px 8px; vertical-align: middle; }
.crsl-deal th, .crsl-final th { font-size: 12px; font-weight: 600; opacity: 0.85; }
.crsl-deal .crsl-name, .crsl-final .crsl-name { text-align: left; white-space: nowrap; }
.crsl-deal .crsl-left { text-align: left; }
.crsl-deal .crsl-num, .crsl-final .crsl-num { text-align: right; font-variant-numeric: tabular-nums; }
.crsl-deal td.crsl-total, .crsl-final td.crsl-total { font-weight: 800; }
.crsl-deal tr.crsl-winrow td.crsl-name, .crsl-final tr.crsl-winrow td.crsl-name { color: var(--accent); }
.crsl-leftcards { display: flex; flex-wrap: wrap; gap: 2px; max-width: 220px; }
.crsl-leftcards .card.mini { --cw: 28px; }
.crsl-result .crsl-dealline { display: block; }
/* =========================================================================
   Grapplin (grapplin)
   A two-player climbing and shedding game: beat the play to beat, or match one
   of its ranks to take from the card row, add to the play, or remove from it.
   The board is built around what a player reads every turn:
     both seat panels: points, joker, and for the other player its cards
       left and the cards it is known to hold     (.grp-opp, .grp-mine)
     the rank order, the play to beat at full size, and the exchange so far
                                                          (.grp-center)
     the draw deck, the card row, and the discard pile    (.grp-supply)
   On a wide screen the seat panels stack left of the center and the supply
   sits right of it, in ONE row, and the two button rows share one line, so
   the whole hand is on screen under them (ui-conventions, "Everything you
   play with is on screen"). On a phone the seat panels share a row.
   EVERY CARD SIZE is a custom property on .grp-board (or on .grp-actions),
   never a rule on .card or a shared id, so each reservation follows it.
   ANTI-JITTER (ui-conventions.md, "Containers must not move"). Every box above
   the hand is reserved at its tallest ordinary shape, re-derived at the phone
   breakpoint: the seat panels at one height in every state (the ▶ is switched
   by .grp-off, which keeps its space, and the other hand is one reserved row
   of known cards and backs), the play to beat at one row of four,
   the earlier plays at one scrolling row, the row at three slots, both
   control rows always present, the note at two lines (three on phones), and
   the status line likewise. Nothing on the board is ever `hidden` in a game.
   SHARED IDS are reached only through a class this game owns: the hand panel
   through .grp-handpanel, the status and progress lines through .grp-status
   and .grp-progress. No rule here reserves the hand tray's height.
   ========================================================================= */

/* --- sizes ------------------------------------------------------------- */
.grp-board {
    --grp-playcw: 60px;
    --grp-rowcw: 56px;
    --grp-pilecw: 40px;
    --grp-earliercw: 28px;
    /* Full art, so a hair larger than the simplified 22px it replaced. */
    --grp-jokercw: 24px;
    --grp-knowncw: 22px;
    --grp-backcw: 10px;
    --grp-backstep: 4px;
    --grp-gap: 6px;
    --grp-centerw: calc(4 * var(--grp-playcw) + 3 * var(--grp-gap) + 24px);
    --grp-seatw: 184px;
}

/* Switched on and off without moving anything: the space is kept. */
.grp-off { visibility: hidden; }

/* --- suit glyphs in prose ---------------------------------------------- */
/* Suits decide suited runs, so a glyph is always colored. The platform's own
   log rules cover the log; these cover the board, the controls, and the note. */
.grp-suited .red,
.grp-note .red { color: var(--red); }
.grp-suited .red.diamond,
.grp-note .red.diamond { color: var(--red-diamond); }
.grp-suited .club,
.grp-note .club { color: var(--blue-club); }

/* --- the board --------------------------------------------------------- */
.grp-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto auto minmax(0, 1fr);
    grid-template-areas:
        ".  opp  center supply ."
        ".  mine center supply .";
    align-items: center;
    justify-items: center;
    gap: 8px 16px;
    margin-bottom: 6px;
}
.grp-opp { align-self: end; }
.grp-mine { align-self: start; }
.grp-opp { grid-area: opp; }
.grp-center { grid-area: center; }
.grp-supply { grid-area: supply; }
.grp-mine { grid-area: mine; }

/* --- a seat's panel ------------------------------------------------------ */
/* FIXED: a long name ellipsizes, it never grows the panel. */
.grp-seat {
    box-sizing: border-box;
    width: var(--grp-seatw);
    max-width: 100%;
    min-width: 0;
    padding: 6px 10px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default, so the ring never shifts the layout. */
    outline: 2px solid transparent;
}
/* The seat to act: the ring AND the ▶, so turn does not rest on color. */
.grp-seat.grp-active { outline-color: var(--accent); }
.grp-seat-head {
    display: flex;
    align-items: baseline;
    gap: 6px;
    margin-bottom: 4px;
    white-space: nowrap;
}
.grp-toact { flex: none; color: var(--accent); font-size: 11px; }
.grp-seat-name {
    flex: 1 1 0;
    min-width: 0;
    font-weight: 700;
    font-size: 14px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Points as pips out of 3: filled and hollow circles, not color alone. */
.grp-points { flex: none; display: flex; gap: 2px; font-size: 14px; line-height: 1; }
.grp-pip { opacity: 0.55; }
.grp-pip.grp-pip-on { opacity: 1; color: var(--accent); }
.grp-seat-line {
    display: flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
    min-height: calc(var(--grp-jokercw) * 89 / 64);
}
.grp-jokerbox { flex: none; display: flex; align-items: center; gap: 5px; }
.grp-jokerbox .card.grp-joker { --cw: var(--grp-jokercw); flex: none; }
.grp-jokerword { font-size: 12px; opacity: 0.85; white-space: nowrap; }
/* The other player's hand as ONE reserved row: the known cards face up (the
   ones it took from the row and still holds), then a back for each card
   nobody has seen. It scrolls rather than wraps, so it is NOT centered. */
.grp-holds {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 3px;
    height: calc(var(--grp-knowncw) * 89 / 64);
    margin-top: 4px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
}
.grp-holds .card.grp-knowncard { --cw: var(--grp-knowncw); flex: none; }
.grp-backs { flex: none; display: flex; align-items: center; margin-left: 3px; }
.grp-backs .card.grp-back {
    --cw: var(--grp-backcw);
    flex: none;
    margin-left: calc(var(--grp-backstep) - var(--grp-backcw));
    border-radius: 2px;
    box-shadow: -1px 0 0 rgba(0, 0, 0, 0.35);
}
.grp-backs .card.grp-back:first-child { margin-left: 0; }
.grp-count {
    flex: none;
    margin-left: auto;
    min-width: 2ch;
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* --- the center ---------------------------------------------------------- */
/* A FIXED box: four reserved rows, the same size empty or full. */
.grp-center {
    box-sizing: border-box;
    width: var(--grp-centerw);
    max-width: 100%;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
/* The rank order, said in words; reversed also gets a ring. */
.grp-order {
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 6px;
    width: max-content;
    max-width: 100%;
    margin: 0 auto 4px;
    padding: 1px 10px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    font-size: 12px;
    white-space: nowrap;
}
.grp-order-word { opacity: 0.8; }
.grp-order-value { font-weight: 700; }
.grp-order.grp-reversed { border-color: var(--accent); }
.grp-order.grp-reversed .grp-order-value { color: var(--accent); }
/* The play to beat, at full size: one reserved row of four. */
.grp-attack {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    min-height: calc(var(--grp-playcw) * 89 / 64 + 6px);
    margin-top: 2px;
}
.grp-attack .grp-group {
    position: relative;
    display: flex;
    flex-wrap: nowrap;
    gap: var(--grp-gap);
    border-radius: 8px;
}
.grp-attack .card.grp-attackcard { --cw: var(--grp-playcw); flex: none; }
/* A card added this turn: a dashed ring, not color alone. */
.grp-attack .card.grp-added { outline: 2px dashed var(--accent); outline-offset: 2px; }
/* A bomb: ringed and labeled. */
.grp-attack .grp-group.grp-bomb { outline: 2px solid var(--accent); outline-offset: 4px; }
.grp-bombmark {
    position: absolute;
    top: -12px;
    right: -10px;
    padding: 0 6px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    line-height: 18px;
    color: var(--accent-ink);
    background: var(--accent);
}
/* The exchange's earlier plays, oldest left: ONE row that scrolls rather than
   wraps, NOT centered (a centered scrolling strip clips its left end). */
.grp-earlier {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: center;
    gap: 8px;
    height: calc(var(--grp-earliercw) * 89 / 64 + 8px);
    margin-top: 6px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}
.grp-earlier .grp-group { flex: none; display: flex; gap: 2px; }
.grp-earlier .card.grp-earliercard { --cw: var(--grp-earliercw); flex: none; }

/* --- the supply: deck, row, discard -------------------------------------- */
.grp-supply {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 10px;
    min-height: calc(var(--grp-rowcw) * 89 / 64 + 8px);
}
.grp-pile {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    width: calc(var(--grp-pilecw) + 26px);
}
.grp-pile .card.grp-pilecard { --cw: var(--grp-pilecw); flex: none; }
.grp-pilecount {
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    opacity: 0.85;
}
.grp-row {
    display: flex;
    flex-wrap: nowrap;
    gap: var(--grp-gap);
    padding: 4px 6px;
    border-radius: 10px;
    border: 1px dashed var(--panel-border);
}
.grp-row .card.grp-rowcard { --cw: var(--grp-rowcw); flex: none; }

/* --- the action box ---------------------------------------------------- */
/* Controls first, prose after, each in its own reserved box. Every button is
   always in its row with a FIXED width, so a disabled one keeps its place. */
.grp-actions { margin-bottom: 6px; }
/* Both button rows on one centered line where there is room, each keeping its
   own reserved height; stacked on a phone. */
.grp-buttons {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: stretch;
    gap: 16px;
}
.grp-buttons .grp-controls { margin: 4px 0; }
.grp-controls {
    flex-wrap: nowrap;
    align-items: stretch;
    margin: 4px 0;
    /* A button's own height: a 20px line, 14px of padding, and its border, in
       whole pixels, so an empty row and a full one are the same height and
       nothing below rounds to a different pixel. */
    height: 36px;
}
/* Exactly the row's height: left to its content a button came out 36.33px,
   so the row grew a third of a pixel whenever its buttons were painted, and
   everything under it moved a pixel on a zoomed phone board (measured). */
.grp-controls .btn {
    flex: none;
    box-sizing: border-box;
    height: 36px;
    white-space: nowrap;
    line-height: 20px;
}
.grp-controls .btn.grp-play,
.grp-controls .btn.grp-pass { width: 7.5em; }
.grp-controls .btn.grp-special,
.grp-controls .btn.grp-reverse { width: 6.2em; }
.grp-controls .btn.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
/* What the selection does, why it is refused, or the hint: two lines reserved. */
.grp-note {
    min-height: 38px;
    line-height: 19px;
    font-size: 14px;
    text-align: center;
}

/* --- the hand ---------------------------------------------------------- */
/* On the viewer's own turn: a card no play or match uses. Lightly faded, no
   filter, so rank and suit stay readable. */
.grp-handpanel .card.grp-dim { opacity: 0.45; }
/* A card taken from the row this turn: a dot above it, not color alone. */
.grp-handpanel .card.grp-new { position: relative; outline: 2px dashed var(--accent); outline-offset: 2px; }
.grp-handpanel .card.grp-new::after {
    content: '';
    position: absolute;
    top: -7px;
    left: 50%;
    width: 7px;
    height: 7px;
    margin-left: -3.5px;
    border-radius: 50%;
    background: var(--accent);
}

/* --- status and progress ----------------------------------------------- */
.status.grp-status { line-height: 1.35; min-height: 2.7em; }
/* Lit whenever this device may act. */
.status.grp-status.grp-turn { color: var(--accent); font-weight: 700; }
.progress.grp-progress { min-height: 1.4em; white-space: nowrap; }

/* --- the summary ------------------------------------------------------- */
/* A real table: each column sized once for every row. Names left, numbers
   right in tabular figures, the table centered as a block. */
.grp-result {
    border-collapse: collapse;
    margin: 0 auto 10px;
    font-size: 14px;
}
.grp-result th,
.grp-result td {
    padding: 4px 12px;
    border-bottom: 1px solid var(--panel-border);
    white-space: nowrap;
}
.grp-result th { font-size: 12px; font-weight: 600; opacity: 0.85; }
.grp-result .grp-name { text-align: left; }
.grp-result .grp-num { text-align: right; font-variant-numeric: tabular-nums; }
.grp-result tr.grp-best td { color: var(--accent); font-weight: 700; }

/* --- phones ------------------------------------------------------------ */
/* The two seat panels share a row, then the center, then the supply. Every
   reservation above is re-derived here. */
@media (max-width: 720px) {
    .grp-board {
        grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
        grid-template-areas:
            "opp    mine"
            "center center"
            "supply supply";
        gap: 6px;
    }
    /* Both seat panels in a row, stretched: the other player's wider, for its
       backs and known cards. */
    .grp-board { --grp-seatw: auto; }
    .grp-opp, .grp-mine { justify-self: stretch; align-self: stretch; }
    .grp-buttons { flex-direction: column; gap: 0; }
    .grp-note { min-height: 57px; }
    /* Two lines: the longest status (a run of 4 matched with two ranks) is 80
       characters, two lines at 360px (measured). */
    .status.grp-status { min-height: 2.7em; }
    .grp-board { --grp-earliercw: 22px; }
}
@media (max-width: 400px) {
    .grp-board {
        --grp-playcw: 54px;
        --grp-rowcw: 50px;
        --grp-pilecw: 34px;
        --grp-earliercw: 20px;
        --grp-backcw: 10px;
        --grp-backstep: 4px;
        --grp-gap: 5px;
    }
    .grp-supply { gap: 6px; }
    .grp-pile { width: calc(var(--grp-pilecw) + 20px); }
    .grp-controls .btn.grp-play,
    .grp-controls .btn.grp-pass { width: 6.8em; }
    /* Four of them in one row inside 360px: 4 x 80 + 3 x 6 = 338px. */
    .grp-specials { gap: 6px; }
    .grp-controls .btn.grp-special,
    .grp-controls .btn.grp-reverse { width: 5em; padding-left: 4px; padding-right: 4px; }
}
/* =========================================================================
   Peelin (peelin)
   A two-player shedding duel with a hand that is never rearranged: lead from
   a side, beat from anywhere, pick a card when you pass.
   The board is built around what a player reads every turn:
     both seat panels: score and stake, and for the other player its hand
       in order, backs and the cards it picked face up   (.pln-opp, .pln-mine)
     the last combination at full size, and the trick so far  (.pln-center)
     the discard pile and the four unused cards               (.pln-piles)
   On a wide screen the seat panels stack left of the center and the piles sit
   right of it, in ONE row, so the whole hand is on screen under them
   (ui-conventions, "Everything you play with is on screen"). On a phone the
   seat panels stack above the center.
   EVERY CARD SIZE is a custom property on .pln-board, never a rule on .card or
   a shared id, so each reservation follows it.
   ANTI-JITTER (ui-conventions.md, "Containers must not move"). Every box above
   the hand is reserved at its tallest ordinary shape: the seat panels at one
   height in every state (the ▶ is switched by .pln-off, which keeps its
   space, and the other hand is one reserved row), the last combination at one
   row of two, the earlier plays at one scrolling row, both buttons always
   present, the note at two lines, and the status line likewise.
   SHARED IDS are reached only through a class this game owns: the hand panel
   through .pln-handpanel, the status and progress lines through .pln-status
   and .pln-progress. No rule here reserves the hand tray's height.
   ========================================================================= */
/* --- sizes ------------------------------------------------------------- */
.pln-board {
    --pln-playcw: 64px;
    --pln-earliercw: 28px;
    --pln-pilecw: 34px;
    --pln-stakecw: 26px;
    --pln-oppcw: 22px;
    --pln-backstep: 9px;
    --pln-gap: 6px;
    --pln-centerw: 300px;
    --pln-seatw: 240px;
}
.pln-off { visibility: hidden; }
/* --- the board --------------------------------------------------------- */
.pln-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto auto minmax(0, 1fr);
    grid-template-areas:
        ".  opp  center piles ."
        ".  mine center piles .";
    align-items: center;
    justify-items: center;
    gap: 8px 16px;
    margin-bottom: 6px;
}
.pln-opp { grid-area: opp; align-self: end; }
.pln-mine { grid-area: mine; align-self: start; }
.pln-center { grid-area: center; }
.pln-piles { grid-area: piles; }
/* --- a seat's panel ------------------------------------------------------ */
.pln-seat {
    box-sizing: border-box;
    width: var(--pln-seatw);
    max-width: 100%;
    min-width: 0;
    padding: 6px 10px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.pln-seat.pln-active { outline-color: var(--accent); }
.pln-seat-head {
    display: flex;
    align-items: baseline;
    gap: 6px;
    margin-bottom: 4px;
    white-space: nowrap;
}
.pln-toact { flex: none; color: var(--accent); font-size: 11px; }
.pln-seat-name {
    flex: 1 1 0;
    min-width: 0;
    font-weight: 700;
    font-size: 14px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.pln-score {
    flex: none;
    font-size: 15px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.pln-seat-line {
    display: flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
    min-height: calc(var(--pln-stakecw) * 89 / 64);
}
.pln-stakebox { flex: none; display: flex; align-items: center; gap: 5px; }
.pln-stakebox .card.pln-stake { --cw: var(--pln-stakecw); flex: none; }
.pln-stakeword { font-size: 12px; opacity: 0.85; }
/* The other player's hand as ONE reserved row in its true order: a back for
   every unseen card, stepped, and the cards it picked face up where it put
   them. It scrolls rather than wraps, so it is NOT centered. */
.pln-holds {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    min-width: 0;
    height: calc(var(--pln-oppcw) * 89 / 64);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
}
.pln-holds .card { --cw: var(--pln-oppcw); flex: none; }
.pln-holds .card.pln-back {
    margin-right: calc(var(--pln-backstep) - var(--pln-oppcw));
    border-radius: 2px;
    box-shadow: -1px 0 0 rgba(0, 0, 0, 0.35);
}
.pln-holds .card.pln-back:last-child { margin-right: 0; }
.pln-holds .card.pln-known { margin: 0 2px; position: relative; z-index: 1; }
/* --- the center ---------------------------------------------------------- */
.pln-center {
    box-sizing: border-box;
    width: var(--pln-centerw);
    max-width: 100%;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
/* The last combination, at full size: one reserved row of two. */
.pln-last {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    min-height: calc(var(--pln-playcw) * 89 / 64 + 6px);
}
.pln-last .pln-group { display: flex; flex-wrap: nowrap; gap: var(--pln-gap); }
.pln-last .card.pln-lastcard { --cw: var(--pln-playcw); flex: none; }
/* The trick's earlier combinations, oldest left: ONE row that scrolls rather
   than wraps, NOT centered (a centered scrolling strip clips its left end). */
.pln-earlier {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: center;
    gap: 8px;
    height: calc(var(--pln-earliercw) * 89 / 64 + 8px);
    margin-top: 6px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}
.pln-earlier .pln-group { flex: none; display: flex; gap: 2px; }
.pln-earlier .card.pln-earliercard { --cw: var(--pln-earliercw); flex: none; }
/* Under the last combination: picking is a tap on the card, so say so. One
   reserved line, hidden (not removed) when there is nothing to pick. */
.pln-pickcue {
    min-height: 18px;
    margin-top: 2px;
    font-size: 12px;
    line-height: 18px;
    text-align: center;
    white-space: nowrap;
    color: var(--accent);
}
/* A card you may pick: a dashed ring, not color alone. */
.pln-last .card.pln-pickable { outline: 2px dashed var(--accent); outline-offset: 3px; }
.pln-last .card.pln-pickable.sel { outline-style: solid; }
/* --- the piles ----------------------------------------------------------- */
.pln-piles {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 8px;
}
.pln-pile {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    width: calc(var(--pln-pilecw) + 40px);
}
.pln-pile .card.pln-pilecard { --cw: var(--pln-pilecw); flex: none; }
.pln-discard { cursor: pointer; border-radius: 8px; }
.pln-discard:hover .pln-pilecount,
.pln-discard:focus-visible .pln-pilecount { text-decoration: underline; opacity: 1; }
.pln-pilecount {
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    opacity: 0.85;
}
/* --- the discard viewer ------------------------------------------------ */
.pln-pileview {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: rgba(0, 0, 0, 0.6);
}
.pln-pilebox {
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-width: min(560px, calc(100vw - 32px));
    max-height: calc(100vh - 48px);
    overflow-y: auto;
    padding: 16px 20px;
    border-radius: 14px;
    border: 1px solid var(--panel-border);
    background: var(--felt-dark);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
}
.pln-pilehead { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-weight: 700; }
.pln-pilecards { flex-wrap: wrap; justify-content: center; gap: 4px; }
.pln-pilecards .card { --cw: 36px; }
.pln-pilenote, .pln-pilecounts { margin: 0; font-size: 13px; }
.pln-pilenote { opacity: 0.8; }
/* --- the value badges -------------------------------------------------- */
/* "0" on a Jack, "10" on a Queen; "10" is two characters, so the box grows
   to its text rather than staying square. */
.card.pln-valcard .ability-badge { width: auto; min-width: clamp(14px, calc(var(--cw) * 0.26), 22px); padding: 0 3px; }
/* --- the action box ---------------------------------------------------- */
.pln-actions { margin-bottom: 6px; }
.pln-controls {
    flex-wrap: nowrap;
    justify-content: center;
    align-items: stretch;
    margin: 4px 0;
    height: 36px;
}
.pln-controls .btn {
    flex: none;
    box-sizing: border-box;
    height: 36px;
    width: 7.5em;
    white-space: nowrap;
    line-height: 20px;
}
.pln-controls .btn.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
.pln-note {
    min-height: 38px;
    line-height: 19px;
    font-size: 14px;
    text-align: center;
}
/* --- the hand ---------------------------------------------------------- */
/* ONE ROW, ALWAYS. The order of the hand is the game (lead from a side, a
   Queen only from a side), so the tray never wraps. The tray is a size
   container; with n cards (--pln-n, set by the renderer) each card takes the
   site size, or shrinks to share the row, down to a floor; past the floor the
   cards overlap, every index corner (top left) still showing. */
.pln-handpanel { --pln-handcw: var(--cw); --pln-handfloor: 56px; }
.pln-handpanel #hand-cards {
    container-type: inline-size;
    flex-wrap: nowrap;
    gap: 0;
}
.pln-handpanel #hand-cards .card {
    --cw: clamp(var(--pln-handfloor), calc((100cqw - (var(--pln-n) - 1) * 6px) / var(--pln-n)), var(--pln-handcw));
    margin-left: min(6px, calc((100cqw - var(--pln-n) * var(--cw)) / (var(--pln-n) - 1)));
}
.pln-handpanel #hand-cards .card:first-child { margin-left: 0; }
/* Each card its own stacking layer, so an overlapped card's badge stays under
   the card that covers it instead of painting over its neighbor. */
.pln-handpanel #hand-cards .card:not(.card-fly) { position: relative; z-index: 0; }
.pln-handpanel .card.pln-dim { opacity: 0.45; }
/* A card this player picked: the other player saw where it went. */
.pln-handpanel .card.pln-picked { outline: 2px dashed var(--accent); outline-offset: 2px; }
/* Picking: a gap marker on every card's left edge, and on the last card's
   right edge too. Positioned out of the flow, so the hand does not move. */
.pln-gap {
    position: absolute;
    top: -6px;
    bottom: -6px;
    left: -13px;
    z-index: 3;
    width: 20px;
    padding: 0;
    border: 0;
    border-radius: 6px;
    background: transparent;
    cursor: pointer;
}
.pln-gap.pln-gap-after { left: auto; right: -13px; }
.pln-gap::before {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: 8px;
    width: 4px;
    border-radius: 2px;
    background: var(--accent);
    opacity: 0.75;
}
.pln-gap:hover::before,
.pln-gap:focus-visible::before,
.pln-gap.hinted::before { opacity: 1; left: 6px; width: 8px; }
/* --- status and progress ----------------------------------------------- */
.status.pln-status { line-height: 1.35; min-height: 2.7em; }
.status.pln-status.pln-turn { color: var(--accent); font-weight: 700; }
.progress.pln-progress { min-height: 1.4em; white-space: nowrap; }
/* --- the summary ------------------------------------------------------- */
.pln-result {
    border-collapse: collapse;
    margin: 0 auto 10px;
    font-size: 14px;
}
.pln-result th,
.pln-result td {
    padding: 4px 10px;
    border-bottom: 1px solid var(--panel-border);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.pln-result th { font-size: 12px; font-weight: 600; opacity: 0.85; }
.pln-result .pln-name { text-align: left; }
.pln-result .pln-num { text-align: right; }
.pln-result tr.pln-best td { color: var(--accent); font-weight: 700; }
/* --- phones ------------------------------------------------------------ */
@media (max-width: 720px) {
    .pln-board {
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas:
            "opp    opp"
            "mine   mine"
            "center piles";
        gap: 6px;
        --pln-seatw: 100%;
        --pln-centerw: 260px;
        --pln-earliercw: 22px;
    }
    .pln-opp, .pln-mine { justify-self: stretch; }
    .pln-piles { flex-direction: column; gap: 4px; }
}
@media (max-width: 400px) {
    .pln-board {
        --pln-playcw: 56px;
        --pln-pilecw: 28px;
        --pln-centerw: 236px;
        --pln-earliercw: 20px;
        --pln-oppcw: 20px;
        --pln-backstep: 8px;
    }
    .pln-pile { width: calc(var(--pln-pilecw) + 30px); }
    .pln-controls .btn { width: 6.8em; }
}
@media (max-width: 720px) {
    .pln-handpanel { --pln-handfloor: 44px; }
}
/* =========================================================================
   Chameleon Tricks (chameleontricks)
   A trick taker for 2 or 3: after your first card, add cards of the same rank
   on top, and only your last card counts. The highest hand score is halved.
     the table as a table: the other seats round the trick, this player South,
       and the trick a cross with each seat's stack on its side
                                             (.chm-board, .chm-trick, .pos-*)
     a stack per seat, every card's corner showing and the last on top
                                             (.chm-stack, .chm-at-*, .chm-depth-*)
     the action row under the trick, holding Done   (.chm-actions)
     a three-line panel per seat: name, total and dealer chip, the points taken
       this hand and the cards held          (.chm-seat, .chm-taken, .chm-held)
     the suit order, in the Hand_Order's direction   (.chm-suits)
     a seat's out-first chip                 (.chm-outfirst)
     the cards still out, a grid of the whole pack below the hand
                                             (.chm-stillout)
     each finished trick in the log, a small cross of stacks
                                             (.chm-logtrick, .chm-logcross)
     a finished trick held in the cross until the next lead   (.chm-winning)
   CONTAINERS MUST NOT MOVE. Every track of the desktop board is fixed or a
   fixed share of a fixed width; every trick slot is as wide as a stack of four
   whatever it holds; the action row keeps its height with or without Done; the
   panels are three lines whose heights derive from their own font sizes.
   #hand-cards gets no min-height: src/shared/ui/handspace.ts reserves it.
   SHARED IDS only through a class this game owns: .chm-handpanel on the hand
   panel, .chm-status and .chm-progress added by the renderers, .chm-summary on
   the result panel.
   ========================================================================= */

/* Visually hidden words beside an aria-hidden glyph or short form. */
.chm-vh {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* --- status and progress ------------------------------------------------ */
/* Two lines reserved: the action wanted, and a hint when one is asked for. */
.status.chm-status { min-height: 2.6em; }
.status.chm-status.chm-turn { color: var(--accent); }
.chm-statusmain { display: block; }
.chm-hintnote { display: block; font-size: 14px; font-weight: 400; opacity: 0.85; }
.progress.chm-progress { min-height: 20px; white-space: nowrap; }
.chm-suited .red { color: var(--red); }
.chm-suited .red.diamond { color: var(--red-diamond); }
.chm-suited .club { color: var(--blue-club); }

/* --- the board ----------------------------------------------------------- */
/* ONE GRID. #chm-table and .chm-foot are `display: contents`, so the panels
   and the suit box are cells of it. THE CENTER TRACK IS A FIXED WIDTH:
     slot   = --chm-playcw + 3 * --chm-off          (52px, 18px: 106px)
     trick  = 3 * slot + 2 * --chm-trickgap + 20px  (350px)
   so the flanks never move as stacks grow. */
.chm-board {
    --chm-playcw: 52px;
    --chm-off: 18px;
    --chm-slotw: calc(var(--chm-playcw) + 3 * var(--chm-off));
    --chm-trickgap: 6px;
    --chm-centerw: 354px;
    --chm-actionh: 40px;
    --chm-namefs: 13px;
    --chm-totalfs: 15px;
    --chm-linefs: 11px;
    --chm-pad: 6px 9px 7px;
    --chm-suitsw: 150px;
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--chm-centerw) minmax(0, 1fr);
    column-gap: 12px;
    row-gap: 8px;
    align-items: center;
    margin-bottom: 10px;
}
.chm-board.chm-seats-3 { grid-template-areas: "w center e" "suits mine ."; }
.chm-board.chm-seats-2 { grid-template-areas: ". n ." ". center ." "suits mine ."; }
.chm-table,
.chm-foot { display: contents; }
.chm-seat.pos-w { grid-area: w; justify-self: end; }
.chm-seat.pos-e { grid-area: e; justify-self: start; }
.chm-seat.pos-n { grid-area: n; justify-self: center; }
.chm-seat.chm-mine { grid-area: mine; justify-self: center; }
.chm-suits { grid-area: suits; justify-self: end; }

/* --- the center: the trick over the action row ---------------------------- */
.chm-center {
    grid-area: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    box-sizing: border-box;
    width: var(--chm-centerw);
    justify-self: center;
}
.chm-actions {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 8px;
    min-height: var(--chm-actionh);
    margin: 0;
}
.chm-actions .btn.chm-done { min-width: 96px; white-space: nowrap; }
.chm-actions .btn.chm-done.hinted { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- the trick: a cross of stacks ----------------------------------------- */
.chm-trick {
    display: grid;
    grid-template-columns: repeat(3, var(--chm-slotw));
    gap: var(--chm-trickgap);
    justify-content: center;
    justify-items: center;
    align-items: center;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.chm-board.chm-seats-3 .chm-trick { grid-template-areas: "w . e" ". s ."; }
.chm-board.chm-seats-2 .chm-trick { grid-template-areas: ". n ." ". s ."; }
.chm-play.pos-s { grid-area: s; }
.chm-play.pos-w { grid-area: w; }
.chm-play.pos-n { grid-area: n; }
.chm-play.pos-e { grid-area: e; }
/* Every slot holds a stack of four, so the cross keeps its size. The cards sit
   side by side with --chm-off of each showing, centered in the slot. */
.chm-stack {
    --chm-depth: 1;
    position: relative;
    width: var(--chm-slotw);
    height: calc(var(--chm-playcw) * 1.39);
}
.chm-stack.chm-depth-2 { --chm-depth: 2; }
.chm-stack.chm-depth-3 { --chm-depth: 3; }
.chm-stack.chm-depth-4 { --chm-depth: 4; }
.chm-stack .card {
    --cw: var(--chm-playcw);
    position: absolute;
    top: 0;
    left: calc((var(--chm-slotw) - var(--chm-playcw) - (var(--chm-depth) - 1) * var(--chm-off)) / 2);
}
.chm-stack .card.chm-at-1 { left: calc((var(--chm-slotw) - var(--chm-playcw) - (var(--chm-depth) - 1) * var(--chm-off)) / 2 + var(--chm-off)); }
.chm-stack .card.chm-at-2 { left: calc((var(--chm-slotw) - var(--chm-playcw) - (var(--chm-depth) - 1) * var(--chm-off)) / 2 + 2 * var(--chm-off)); }
.chm-stack .card.chm-at-3 { left: calc((var(--chm-slotw) - var(--chm-playcw) - (var(--chm-depth) - 1) * var(--chm-off)) / 2 + 3 * var(--chm-off)); }
/* A card under the top one no longer counts: dimmed, so the eye reads the top. */
.chm-stack .card.chm-under { filter: brightness(0.82); }
.chm-play .card.empty { opacity: 0.35; }
.chm-play.onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
/* A seat out of cards plays no more this hand: its slot is barely there. */
.chm-play.out .card.empty { opacity: 0.12; }
/* The card taking the trick, or that took the finished trick still shown:
   ringed, and glowing, so it is not a matter of color alone. */
.chm-play .card.chm-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}

/* --- a seat's panel: three lines at every size ---------------------------- */
.chm-seat {
    position: relative;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 2px;
    width: 176px;
    max-width: 100%;
    min-width: 0;
    padding: var(--chm-pad);
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.chm-seat.chm-active { outline-color: var(--accent); }
/* Out of cards for the rest of the hand: faded, and the held chip says 0. */
.chm-seat.chm-isout { opacity: 0.6; }
.chm-seat-head {
    line-height: 1.3;
    min-height: calc(var(--chm-namefs) * 1.3);
    font-size: var(--chm-namefs);
}
.chm-seat-score {
    line-height: 1.2;
    min-height: calc(var(--chm-totalfs) * 1.2);
    font-size: var(--chm-totalfs);
}
.chm-seat-line {
    min-height: calc(var(--chm-linefs) * 1.6 + 2px);
    font-size: var(--chm-linefs);
}
.chm-seat-head,
.chm-seat-score,
.chm-seat-line {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
}
.chm-seat-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: 700;
}
.chm-toact { visibility: hidden; color: var(--accent); }
.chm-seat.chm-active .chm-toact { visibility: visible; }
.chm-seat-total { font-weight: 800; font-variant-numeric: tabular-nums; }
.chm-dealer {
    visibility: hidden;
    font-size: calc(var(--chm-totalfs) * 0.7);
    font-weight: 700;
    line-height: 1.2;
    padding: 0 3px;
    border: 1px solid var(--accent);
    border-radius: 4px;
    color: var(--accent);
}
.chm-seat.chm-isdealer .chm-dealer { visibility: visible; }
/* Fewest cards left, so out first if everyone plays one a trick. A filled
   chip with its words, so it is not a matter of color alone. OUT OF THE FLOW
   at the line's right edge, the way a .panel-head button is: left in the
   centered line it pushed the total off center by half its width, on every
   panel, whether it showed or not. */
.chm-seat-score { position: relative; }
.chm-outfirst {
    position: absolute;
    right: 0;
    top: 50%;
    translate: 0 -50%;
    visibility: hidden;
    font-size: calc(var(--chm-totalfs) * 0.7);
    font-weight: 700;
    line-height: 1.2;
    padding: 0 4px;
    border-radius: 4px;
    white-space: nowrap;
    color: #2b1600;
    background: #ffb36b;
}
.chm-seat.chm-isfirstout .chm-outfirst { visibility: visible; }
/* Points taken this hand, and the cards held: two pills. The highest taken is
   filled and carries a half mark as well as the color. */
.chm-taken,
.chm-held {
    position: relative;
    line-height: 1.6;
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    padding: 0 6px;
    font-variant-numeric: tabular-nums;
}
.chm-taken { font-weight: 700; }
.chm-taken-num { font-variant-numeric: tabular-nums; }
.chm-half { margin-left: 3px; }
.chm-taken.chm-top {
    border-color: rgba(255, 196, 92, 0.85);
    background: rgba(255, 196, 92, 0.22);
}
.chm-held { opacity: 0.85; }

/* --- the suit order ------------------------------------------------------- */
.chm-suits {
    box-sizing: border-box;
    width: var(--chm-suitsw);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 6px 4px;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.16);
}
.chm-suits-label { font-size: 11px; opacity: 0.75; white-space: nowrap; }
.chm-suits-row {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 20px;
    line-height: 1.2;
    white-space: nowrap;
}
.chm-suits-gt { font-size: 12px; opacity: 0.6; }
.chm-suits-glyph.red { color: var(--red); }
.chm-suits-glyph.red.diamond { color: var(--red-diamond); }
.chm-suits-glyph.club { color: var(--blue-club); }

/* --- the hand ---------------------------------------------------------- */
.chm-mute { opacity: 0.4; }

/* --- the cards still out ------------------------------------------------ */
/* One fixed grid of the whole pack: a suit glyph and nine cells a row, a face
   for a card still out and an empty slot for the rest, so the panel is one
   size all game. Faces are simplified art at 24px (ui-conventions). */
.chm-stillout {
    --chm-socw: 24px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
}
.chm-stillout-head {
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 8px;
    white-space: nowrap;
}
.chm-stillout-head .chm-stillout-title { margin: 0; }
.chm-stillout-note { font-size: 12px; opacity: 0.75; }
.chm-stillout-grid {
    display: grid;
    grid-template-columns: 18px repeat(9, var(--chm-socw));
    gap: 3px;
    align-items: center;
    justify-items: center;
}
.chm-stillout-grid .card.chm-stillout-card { --cw: var(--chm-socw); }
.chm-stillout-slot {
    box-sizing: border-box;
    width: var(--chm-socw);
    /* The card aspect exactly (platform.css .card), so a row of slots is as
       tall as a row with a card in it. */
    height: calc(var(--chm-socw) * 89 / 64);
    border: 1px dashed rgba(255, 255, 255, 0.16);
    border-radius: 3px;
}
.chm-stillout-suit { font-size: 15px; line-height: 1; }
.chm-stillout-suit.red { color: var(--red); }
.chm-stillout-suit.red.diamond { color: var(--red-diamond); }
.chm-stillout-suit.club { color: var(--blue-club); }
.chm-stillout-grid.chm-veiled { opacity: 0.5; }

/* --- the log: each finished trick as a cross ----------------------------- */
/* Hearts' shape: a caption beside a small cross of the trick, each seat's
   cards on the side it sits, a stack overlapped left to right with its last
   card on top. The lead caret and the win check hang outside the outer cells,
   so the cross carries side padding for them. */
#log-entries .chm-logtrick {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px 10px;
    padding: 3px 0;
}
.chm-logtrick-cap {
    font-size: 12px;
    min-width: 0;
    flex: 1 1 auto;
    opacity: 0.85;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.chm-logcross {
    --chm-lxcw: 19px;
    display: grid;
    grid-template-columns: repeat(3, minmax(var(--chm-lxcw), max-content));
    grid-template-areas: 'w . e' '. s .';
    align-items: center;
    justify-items: center;
    gap: 1px 4px;
    padding: 0 9px;
    flex: none;
}
.chm-logcross.seats-2 {
    grid-template-columns: minmax(var(--chm-lxcw), max-content);
    grid-template-areas: 'n' 's';
}
.chm-logcross-cell { position: relative; line-height: 0; }
.chm-logcross-cell.pos-w { grid-area: w; }
.chm-logcross-cell.pos-e { grid-area: e; }
.chm-logcross-cell.pos-n { grid-area: n; }
.chm-logcross-cell.pos-s { grid-area: s; }
.chm-logstack { display: inline-flex; }
.chm-logstack .card { --cw: var(--chm-lxcw); flex: none; }
.chm-logstack .card + .card { margin-left: calc(var(--chm-lxcw) * -0.32); }
.chm-logstack .card.chm-logwon { outline: 2px solid var(--accent); outline-offset: 0; }
.chm-logcross-led,
.chm-logcross-won {
    position: absolute;
    font-size: 10px;
    line-height: 1;
    font-weight: 800;
    pointer-events: none;
}
.chm-logcross-led { top: -1px; left: -8px; color: var(--ink); opacity: 0.85; }
.chm-logcross-won { bottom: -1px; right: -8px; color: var(--accent); }
/* A seat that ran out of cards in this trick, on its own row under the
   caption and the cross. */
.chm-logtrick-out { flex: 1 0 100%; font-size: 12px; opacity: 0.85; }

/* --- the summary ------------------------------------------------------ */
/* A real table, centered as a block, every cell aligned on its own. */
.chm-result {
    border-collapse: collapse;
    margin: 6px auto 0;
    font-size: 14px;
}
.chm-result th,
.chm-result td {
    padding: 3px 8px;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    border-bottom: 1px solid var(--panel-border);
}
/* The same table is each finished hand's body in the grouped log, where nothing
   under the heading is bold, so header weight belongs to the summary alone. */
.chm-result th { font-size: 12px; font-weight: 400; opacity: 0.8; }
.chm-summary .chm-result th { font-weight: 600; }
.chm-result .chm-name { text-align: left; }
.chm-result .chm-num { text-align: right; }
.chm-summary .chm-result td.chm-total { font-weight: 800; }
.chm-result tr.chm-halvedrow td.chm-name { color: var(--accent); }
.chm-log-win { color: var(--accent); font-weight: 700; }

/* --- phones -------------------------------------------------------------- */
/* Every reservation is re-derived here. The board is a flex column: the other
   seats in one row, the center at its fixed width, and the foot (the suit box
   left of this player's panel).
     slot  = 40px + 3 * 14px = 82px
     trick = 3 * 82px + 2 * 4px + 16px = 270px, --chm-centerw 274px
   The foot is 110px + 8px + 150px = 268px, inside the 340px row at 360px. */
@media (max-width: 720px) {
    .chm-board {
        --chm-playcw: 40px;
        --chm-off: 14px;
        --chm-trickgap: 4px;
        --chm-centerw: 274px;
        --chm-actionh: 44px;
        --chm-namefs: 12px;
        --chm-totalfs: 14px;
        --chm-linefs: 11px;
        --chm-pad: 4px 5px 5px;
        --chm-suitsw: 110px;
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
    }
    .chm-table {
        display: flex;
        flex-wrap: nowrap;
        justify-content: center;
        gap: 4px;
    }
    .chm-table .chm-seat { flex: 1 1 0; width: auto; max-width: 176px; }
    .chm-center { margin-inline: auto; }
    .chm-trick { padding: 6px 8px; }
    .chm-foot {
        display: flex;
        justify-content: center;
        align-items: center;
        gap: 8px;
    }
    .chm-suits { flex: 0 0 var(--chm-suitsw); }
    .chm-suits-row { font-size: 17px; }
    .chm-seat.chm-mine { flex: 0 0 150px; width: 150px; }
    /* 18px + 9 x 22px + 9 x 3px = 243px, inside the 340px panel at 360px. */
    .chm-stillout { --chm-socw: 22px; }
}
/* =========================================================================
   The Capture of Mr. Raffles (raffles)
   A partnership trick taker with two named Jokers: the Detective, which takes
   any trick it is played to, and Mr. Raffles, the lowest card, who moves into
   the hand of whoever takes the trick he is played to.
     the table as a table: West, North and East around the trick, this player
       South, partners opposite, and the trick a cross with each card on its
       seat's side (.raf-board, .raf-trick, .pos-*)
     a small panel per seat: name and team, cards held, the dealer chip, and
       Mr. Raffles when he is known to be there (.raf-seat, .raf-teamdot,
       .raf-rafchip)
     the scoreboard: one row per team, its tricks and Aces this round and its
       total (.raf-teams); the log draws each finished trick as a cross, as
       Hearts does (.raf-trickcross)
     a finished trick held in the cross until the next lead, winner ringed
       (.raf-winning); a trick a round ended in the middle of, dimmed
       (.raf-abandoned); Mr. Raffles' copy in a held trick once he is in this
       player's hand, dimmed (.raf-gone)
     both Jokers labeled with a ribbon (.raf-jokertag)
   ANTI-JITTER. Nothing above the hand may change height as a round runs. The
   panels are a fixed width and every line in them is reserved at its height,
   so a chip coming and going (dealer, Mr. Raffles, Aces) moves nothing; the
   cross is the same size empty or full; the status line reserves two lines.
   Every shared class (.card, .panel, .progress-score, .status, .logline, .btn,
   .winner-banner, .result-note) is reused as is; rules that name a SHARED id
   are scoped through a class this game owns (.raf-status on the status line,
   .raf-scoreboard on the progress line, .raf-handpanel on the hand panel),
   because those ids are on every page.
   ========================================================================= */
/* --- the scoreboard ----------------------------------------------------- */
/* The round and the trump suit, then one row per team: tricks and Aces this
   round, and the total toward 100. Partners score together, so the tally is the
   team's. The round line and the table sit side by side, centered as a group;
   the phone block below stacks them. The same two rows from the first trick to
   the last, and every cell nowrap, so the scoreboard never changes height. */
.progress.raf-scoreboard {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 6px 16px;
    min-height: 66px;
    opacity: 1;
}
.raf-roundline {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
}
/* Which suit is trump: reference rather than news, so quiet, but always there. */
.raf-refchip {
    font-size: 12px;
    padding: 1px 8px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    font-weight: 700;
    opacity: 0.9;
    white-space: nowrap;
}
/* A real table, so the columns line up (ui-conventions "Score tables line up
   by column"): names left, numbers right, tabular figures. */
.raf-teams {
    border-collapse: collapse;
    font-size: 13px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 8px;
}
.raf-teams th,
.raf-teams td {
    padding: 1px 8px;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    line-height: 18px;
}
.raf-teams th { font-size: 11px; font-weight: 700; opacity: 0.7; }
.raf-teams .raf-name { text-align: left; }
.raf-teams .raf-num { text-align: right; min-width: 2.6em; }
.raf-teams td.raf-name { display: flex; align-items: center; gap: 6px; }
.raf-teams tr.mine td.raf-name { font-weight: 700; }
.raf-teams .raf-total { font-weight: 700; }
/* Whoever is ahead, as a color and the bold the total already has. */
.raf-teams tr.leading .raf-total { color: var(--accent); }
/* The two team colors, set on every box that paints them (the score table also
   sits in the log, so it carries its own). */
.raf-board,
.raf-teams,
.raf-result {
    --raf-team0: #6fb7ff;
    --raf-team1: #ffb36b;
}
/* --- suit glyphs in prose ---------------------------------------------- */
.raf-suited .red { color: var(--red); }
.raf-suited .red.diamond { color: var(--red-diamond); }
.raf-suited .club { color: var(--blue-club); }
.raf-refchip .red { color: var(--red); }
/* --- the status line ---------------------------------------------------- */
/* Two lines reserved: the action wanted, and a hint when one is asked for. */
.status.raf-status { min-height: 2.6em; }
.status.raf-status.raf-turn { color: var(--accent); font-weight: 700; }
.raf-hintnote { display: block; opacity: 0.85; font-weight: 400; }
/* --- the board: the table as a table ------------------------------------ */
/* ONE GRID for the whole table. #raf-table is `display: contents`, so the other
   seats' panels are cells of it and take the areas their seat sits in. */
.raf-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas:
        ".    up    ."
        "left trick right"
        ".    down  .";
    align-items: center;
    gap: 10px 16px;
    margin-bottom: 10px;
}
.raf-table { display: contents; }
.raf-seat.pos-left { grid-area: left; justify-self: end; }
.raf-seat.pos-right { grid-area: right; justify-self: start; }
.raf-seat.pos-up { grid-area: up; justify-self: center; }
.raf-seat.pos-down { grid-area: down; justify-self: center; }
.raf-trick { grid-area: trick; }
/* --- a seat's panel ------------------------------------------------------ */
/* SMALL, and the same for every seat including this one. A FIXED width, so a
   long name or a chip cannot resize the grid. */
.raf-seat {
    box-sizing: border-box;
    width: 176px;
    max-width: 100%;
    min-width: 0;
    padding: 6px 9px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    /* Transparent by default, so the accent ring never shifts the layout. */
    outline: 2px solid transparent;
}
.raf-seat.raf-team-0 { border-top: 3px solid var(--raf-team0); }
.raf-seat.raf-team-1 { border-top: 3px solid var(--raf-team1); }
/* The seat being waited on. */
.raf-seat.raf-active { outline-color: var(--accent); }
.raf-seat-head {
    display: flex;
    align-items: center;
    gap: 5px;
    margin-bottom: 3px;
    min-height: 18px;
}
/* Whose turn it is, as a SHAPE as well as the ring. Always rendered, so the
   name never shifts sideways as the turn comes and goes. */
.raf-toact { color: var(--accent); font-size: 11px; width: 10px; flex: 0 0 auto; }
.raf-seat-name {
    flex: 1 1 auto;
    min-width: 0;
    font-weight: 700;
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* The team mark: the same color on both partners' panels. */
.raf-teamdot {
    flex: 0 0 auto;
    width: 9px;
    height: 9px;
    border-radius: 50%;
}
.raf-teamdot.raf-team-0 { background: var(--raf-team0); }
.raf-teamdot.raf-team-1 { background: var(--raf-team1); }
/* Each line always rendered at its height, so chips can come and go. */
.raf-seat-line {
    display: flex;
    align-items: center;
    gap: 6px;
    min-height: 22px;
    font-size: 12px;
    white-space: nowrap;
}
.raf-held { font-variant-numeric: tabular-nums; }
.raf-chip {
    font-size: 11px;
    padding: 0 7px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    white-space: nowrap;
    line-height: 18px;
}
.raf-chip.raf-dealer { font-weight: 700; padding: 0 6px; }
/* Mr. Raffles is here, worth 10 if he is still here when the round ends. */
.raf-chip.raf-rafchip {
    border-color: #c9a0ff;
    color: #c9a0ff;
    font-weight: 700;
}
/* --- the trick: a cross ------------------------------------------------- */
/* Each seat's slot on the side that seat sits, so a card is read by where it is
   and no slot needs a caption. Same size empty or full. */
.raf-trick {
    --raf-playcw: 56px;
    display: grid;
    grid-template-columns: auto auto auto;
    grid-template-areas:
        ".    up    ."
        "left .     right"
        ".    down  .";
    align-items: center;
    justify-items: center;
    gap: 2px 8px;
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.raf-play.pos-up { grid-area: up; }
.raf-play.pos-left { grid-area: left; }
.raf-play.pos-right { grid-area: right; }
.raf-play.pos-down { grid-area: down; }
.raf-play .card { --cw: var(--raf-playcw); }
/* A slot still owed a card: outlined, not blank, so it reads as "waiting". */
.raf-play .card.empty { border-style: dashed; opacity: 0.35; }
.raf-play.onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
/* The card taking the trick, or that took the finished trick still on the
   table: ringed, and glowing so it is not a matter of color alone. */
.raf-play .card.raf-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
/* Mr. Raffles has already gone into this player's hand. */
.raf-play .card.raf-gone { opacity: 0.35; }
/* The cards a round ended in the middle of: they score for nobody. */
.raf-trick.raf-abandoned .raf-play .card { opacity: 0.45; filter: grayscale(0.6); }
/* --- the Jokers' ribbons ------------------------------------------------ */
/* Which Joker is which, on every painted Joker: a ribbon across the bottom of
   the face, sized with the card so it fits from the 40px phone hand up. */
/* SCOPED THROUGH THE CONTAINERS, never on the Joker classes alone: a flight
   ghost is a clone appended to <body> that keeps the card's classes, and
   `.card.raf-raffles { position: relative }` outranked `.card-fly { position:
   fixed }`, so the ghost dropped to the foot of the page while the card it
   stood in for was hidden. Both Jokers vanished for the length of every flight,
   which read as the screen blinking. A ghost is position: fixed, which is a
   containing block for the ribbon already. */
.raf-board .card,
.raf-handpanel #hand-cards .card { position: relative; }
.raf-jokertag {
    position: absolute;
    left: 50%;
    bottom: 7%;
    transform: translateX(-50%);
    max-width: 94%;
    box-sizing: border-box;
    padding: 1px 3px;
    border-radius: 4px;
    font-size: max(6.5px, calc(var(--cw, 56px) * 0.15));
    letter-spacing: -0.01em;
    font-weight: 800;
    line-height: 1.05;
    text-align: center;
    white-space: normal;
    pointer-events: none;
    border: 1px solid rgba(255, 255, 255, 0.4);
}
.raf-detective .raf-jokertag { background: rgba(20, 28, 40, 0.9); color: #e8eef7; }
.raf-raffles .raf-jokertag { background: rgba(70, 30, 110, 0.92); color: #f0e0ff; }
/* --- the hand --------------------------------------------------------- */
/* A card that cannot be played right now: dimmed rather than removed. */
.raf-mute { opacity: 0.4; }
/* --- the summary ------------------------------------------------------ */
.raf-endnote { max-width: 440px; margin-left: auto; margin-right: auto; }
.raf-result {
    border-collapse: collapse;
    margin: 6px auto 4px;
    font-size: 13px;
}
.raf-result th,
.raf-result td {
    padding: 3px 8px;
    border-bottom: 1px solid var(--panel-border);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.raf-result th { font-size: 12px; font-weight: 700; opacity: 0.85; }
.raf-result .raf-name { text-align: left; }
.raf-result .raf-num { text-align: right; }
.raf-result .raf-total { font-weight: 800; }
.raf-result tr.raf-team-0 td.raf-name { border-left: 3px solid var(--raf-team0); }
.raf-result tr.raf-team-1 td.raf-name { border-left: 3px solid var(--raf-team1); }
.raf-log-win { color: var(--accent); font-weight: 700; }
/* --- the log: a finished trick as a cross ------------------------------- */
/* Hearts' shape: each card where its player sits as the viewer sees the table,
   a caret on the card that led and a check on the one that took it, beside a
   one-line caption. A FIXED footprint whatever the trick holds. */
#log-entries .raf-logtrick {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 3px 0;
}
.raf-logtrick-cap { font-size: 12px; flex: 1 1 auto; min-width: 0; }
.raf-trickcross {
    --raf-tx-cw: 19px;
    display: grid;
    grid-template-columns: repeat(3, var(--raf-tx-cw));
    grid-template-rows: repeat(3, calc(var(--raf-tx-cw) * 1.39));
    grid-template-areas: '. up .' 'left . right' '. down .';
    align-items: center;
    justify-items: center;
    gap: 1px 4px;
    padding: 0 9px;
    flex: none;
}
.raf-trickcross .card { --cw: var(--raf-tx-cw); position: relative; }
.raf-trickcross-cell { position: relative; line-height: 0; }
.raf-trickcross-cell.pos-up { grid-area: up; }
.raf-trickcross-cell.pos-left { grid-area: left; }
.raf-trickcross-cell.pos-right { grid-area: right; }
.raf-trickcross-cell.pos-down { grid-area: down; }
.raf-trickcross-led,
.raf-trickcross-won {
    position: absolute;
    font-size: 10px;
    line-height: 1;
    font-weight: 800;
    pointer-events: none;
}
.raf-trickcross-led { top: -1px; left: -8px; color: var(--ink); opacity: 0.85; }
.raf-trickcross-won { bottom: -1px; right: -8px; color: var(--accent); }
.raf-trickcross .card.raf-lit { box-shadow: 0 0 0 1.5px var(--accent); }
/* A Joker in the log is 19px wide, too small for its name, so its face is
   covered by a short label in the board ribbon's colors (painted from
   data-joker, so the line's text is only its caption). */
.raf-trickcross .card.raf-logjoker::after {
    content: attr(data-joker);
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: inherit;
    font-size: 7px;
    font-weight: 800;
    line-height: 1;
}
.raf-trickcross .card.raf-logjoker.raf-detective::after { background: #1c2633; color: #e8eef7; }
.raf-trickcross .card.raf-logjoker.raf-raffles::after { background: #46206e; color: #f0e0ff; }
/* The cards a round ended in the middle of: they score for nobody. */
.raf-trickcross.raf-unfinished { opacity: 0.55; margin: 2px 0 4px; }
.raf-logend .raf-trickcross { padding-left: 2px; }

/* --- phones ----------------------------------------------------------- */
/* Every reservation above is re-derived here. The board restacks: the three
   other seats in a row ACROSS THE TOP, still West, North and East from left to
   right, the cross under them, this player under that. */
@media (max-width: 720px) {
    .raf-board {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        grid-template-areas:
            "left  up    right"
            "trick trick trick"
            "down  down  down";
        gap: 8px 6px;
        align-items: start;
    }
    .raf-seat.pos-left,
    .raf-seat.pos-up,
    .raf-seat.pos-right { width: auto; justify-self: stretch; padding: 5px 6px 6px; }
    .raf-trick { justify-self: center; --raf-playcw: 44px; padding: 8px 10px; }
    /* A third of a phone is too narrow for two chips on one line, so the line
       wraps, and it is reserved at the two lines it wraps to. */
    .raf-seat:not(.pos-down) .raf-seat-line {
        flex-wrap: wrap;
        row-gap: 3px;
        min-height: 44px;
        align-content: flex-start;
    }
    .raf-seat-name { font-size: 12px; }
    /* The round line goes above the table, as one row. */
    .progress.raf-scoreboard { flex-direction: column; gap: 6px; min-height: 90px; }
    .raf-roundline { flex-direction: row; gap: 10px; }
    .raf-teams { font-size: 12px; }
    .raf-teams th,
    .raf-teams td { padding: 1px 6px; }
    .status.raf-status { min-height: 3.9em; }
}
/* ------------------------------------------------------------------------- */
/* Treasure Hunt                                                             */
/* ------------------------------------------------------------------------- */
/*
 * Treasure Hunt paints with Techu on the Chair's board and stylesheet
 * (src/techu/style.css, the tch- classes, and its trh-treasure block for the
 * badge src/techu/treasure.ts puts on the Treasure). What is this game's own:
 * the rules picture's badge, and the one-screen layout below.
 *
 * The rules picture's Treasure (src/treasurehunt/examplediagrams.ts, the step
 * trh-take): the diagram draws the black Joker from the simplified atlas
 * (column 13, row 0, so background-position 100% 0%) and takes no child, so
 * the badge is its ::after, in the board's place and size (bottom center,
 * 36% of the width, its bottom 8% of the height below the card's, at most 4px).
 */
figure[data-sdg-still="trh-take"] .sdg-card[style*="background-position:100% 0%"]::after {
    content: '';
    position: absolute;
    left: 50%;
    translate: -50% 0;
    bottom: max(-8%, -4px);
    width: 36%;
    aspect-ratio: 1;
    pointer-events: none;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='1' y='1' width='22' height='22' rx='5' fill='%231d2230' stroke='%23fff' stroke-width='2'/%3E%3Cpath d='M5 11.5V10c0-2.8 2.6-4.5 7-4.5s7 1.7 7 4.5v1.5z' fill='%23ffcf4a'/%3E%3Crect x='5' y='11.5' width='14' height='7' rx='1' fill='%23e39b17'/%3E%3Crect x='10.4' y='9.8' width='3.2' height='4' rx='.8' fill='%23fff'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}
/*
 * ONE SCREEN (the user, 2026-11-04: "I had to scroll down to see all of my
 * hand"). The game is on the app layout's frame at every width
 * (pages/app-layout.config.mjs lists it nowhere), so the play area is exactly
 * the window's height under the topbar, and the board and your hand are sized
 * to fit it as well as its width. The classes are the page's
 * (pages/techu.mjs techuBoardMarkup({ treasure: true })), so Techu's board
 * never matches any rule here.
 *
 * THE HEIGHT. The play area is a size container in the frame, so 100cqh is
 * its height, whatever the topbar, the dock under it on a phone, or a
 * tutorial's coach panel take. --trh-fixed is everything in it that does not
 * scale: the status line at its reservation, the score line and its 8px, the
 * other seat's row and its 4px, the board's bands, gaps, padding and border
 * and its 6px, your hand box's padding and head, and 4px to spare. What is
 * left, --trh-avail, holds the board's 5 rows of cells (5 c) and one row of
 * hand cards (h times 89/64, the card's height):
 *
 *     5 c + 1.39 h = avail
 *
 * The board takes the most of it it can while your cards stay at least 1.35
 * times as wide as the board's cards ((c - 14) times 64/89, so 1.39 h is
 * 1.349 (c - 14)) and at least --trh-hfloor: the smaller of the two
 * solutions, never above 80px or the width's own cell (Techu's open-board
 * formula with numbers, so the hand box can compute it too), and never
 * below --trh-cmin. Your cards then take what is left, between the floor and
 * the site's card width at this window (--trh-sitecw). So a short window gets
 * a smaller board, and your cards stay the larger. Where a floor binds (a
 * phone of 360x600) the board runs past the play area, and the frame's fit
 * (src/shared/ui/app-fit.ts) draws the whole play area smaller instead of
 * scrolling.
 *
 * Every term is a function of the window, so nothing here changes between
 * turns: the board, the hand, and every panel keep their places all game.
 */
body.app-desk:has(> #screen-game:not([hidden])) .playarea:has(> #tch-board.trh-board),
body.app-phone:has(> #screen-game:not([hidden])) .playarea:has(> #tch-board.trh-board) {
    container-type: size;
    --trh-sitecw: var(--cw);
}
.tch-board.trh-board,
.panel.tch-handpanel.trh-handpanel {
    /* 18px status at 2 lines (48.6) + 34 + 8 + 58 + 4 + 56 + 6 + 40 + 4. */
    --trh-fixed: 259px;
    --trh-hfloor: 52px;
    --trh-cmin: 48px;
    /* Techu's open-board cell across: (100vw - 48px - 2 (24px + 5px)) / 5. */
    --trh-wc: calc((100vw - 106px) / 5);
    --trh-avail: calc(100cqh - var(--trh-fixed));
    --trh-c: max(var(--trh-cmin), min(80px, var(--trh-wc), calc((var(--trh-avail) + 18.9px) / 6.349),
        calc((var(--trh-avail) - 1.39 * var(--trh-hfloor)) / 5)));
    --trh-hw: clamp(var(--trh-hfloor), calc((var(--trh-avail) - 5 * var(--trh-c)) / 1.39), var(--trh-sitecw, 72px));
}
.tch-board.tch-open.trh-board { --tch-cell: var(--trh-c); }
.panel.tch-handpanel.trh-handpanel { --cw: var(--trh-hw); }
/* The status line reserves 1 line where every form fits one (status-form
   budget in src/techu/def.ts, thFit), else 2. Measured in Chromium over every
   form at every size (the treasurehunt-fit run): the budget's longest forms
   need 2 lines below about 1100px. */
.status.tch-status.trh-status { min-height: calc(2 * 1.35em); }
@media (min-width: 1140px) {
    .tch-board.trh-board,
    .panel.tch-handpanel.trh-handpanel { --trh-fixed: 235px; }
    .status.tch-status.trh-status { min-height: calc(1 * 1.35em); }
}
/*
 * YOUR DISCARD BUTTON beside your cards (the user: "maybe the discard button
 * could be put somewhere horizontally parallel with my hand (it's always at
 * most 3 cards)"). #tch-actions is in the hand box here, out of the flow:
 * its left edge 10px right of a row of 3 cards (whatever the hand holds, so
 * it never moves), its middle on the cards' middle, and it runs to 4px from
 * the box's right edge. The cards stay centered (#hand-cards, the site-wide
 * rule). "Leads" takes the same place on the left.
 */
.trh-handpanel .controls.tch-actions.trh-handact {
    position: absolute;
    top: calc(6px + 26px + var(--cw) * 89 / 128);
    left: calc(50% + (3 * var(--cw) + 16px) / 2 + 10px);
    right: 4px;
    translate: 0 -50%;
    /* One height for the plain button and the discard-only primary (42px). */
    height: 42px;
    min-height: 0;
    margin: 0;
    align-items: center;
    justify-content: flex-start;
    flex-wrap: nowrap;
    /* The slot's width is the window's (its left and right are fixed), so a
       query on it says whether the longest label fits beside 3 cards. */
    container: trh-discard / inline-size;
}
.trh-handpanel .trh-handact > .btn { white-space: nowrap; }
/* Where the slot is narrower than the widest full label (the discard-only
   "Discard your Treasure" at 16px, 209px measured in Chromium), the button
   reads "Discard" (the status line names the picked card), so no label runs
   past the hand box and the frame never zooms the board mid-game. Its
   accessible name stays the whole label: the painted word's alt text is
   empty, after a plain declaration for an engine without the syntax. */
@container trh-discard (max-width: 224px) {
    .trh-handact > #tch-discard { font-size: 0; padding: 7px 10px; }
    .trh-handact > #tch-discard::after { content: 'Discard'; content: 'Discard' / ''; font-size: 14px; }
    .trh-handact > #tch-discard.tch-discard-only { padding: 8px 12px; }
    .trh-handact > #tch-discard.tch-discard-only::after { font-size: 15px; }
}
.trh-handpanel .tch-mynote {
    left: auto;
    right: calc(50% + (3 * var(--cw) + 16px) / 2 + 10px);
}
@media (max-width: 560px) {
    .tch-board.trh-board,
    .panel.tch-handpanel.trh-handpanel {
        /* 15px status at 2 lines (39) + 34 + 8 + 46 + 4 + 58 + 6 + 40 + 4. */
        --trh-fixed: 239px;
        --trh-hfloor: 40px;
        --trh-cmin: 44px;
        --trh-wc: calc((100vw - 104px) / 5);
    }
    .status.tch-status.trh-status { min-height: calc(2 * 1.3em); }
    .trh-handpanel .controls.tch-actions.trh-handact { left: calc(50% + (3 * var(--cw) + 16px) / 2 + 6px); }
    .trh-handpanel .tch-mynote { right: calc(50% + (3 * var(--cw) + 16px) / 2 + 6px); }
}
/* =========================================================================
   Lots
   Top to bottom: the board (the other player's header with the cards known to
   be in its hand, the table with the talon, the lot, and the consolation, and
   the two bid rows), the hand panel with the lot buttons above the hand, and
   the discards by suit (beside the board on a wide column, see the layout
   section). Every box reserves its tallest shape in terms of
   --lots-cw and the card aspect (89/64, about 1.39), so nothing above the
   hand moves as a game runs: the table always paints 3 slots a lot, each bid
   row 3 slots, the known strip one card row, the discards 5 rows, and
   #lots-actions a button row in every phase. The board's cards are 46px on a
   desktop and 34px on a phone, the strips' 28px and 22px, all on the
   simplified atlas (read-only cards at or under 46px, ui-conventions "Small
   cards use the simplified sprite").
   ========================================================================= */
/* --- the layout ------------------------------------------------------------ */
/* Board, hand, and discards in one column; where the game column is wide
   enough (a desktop with the log beside it), the discards stand beside the
   board, so the whole table fits a 900px-high window without scrolling (the
   user found the discards below the fold at 1280 x 900). The tracks are
   fixed, so nothing moves as the discards fill. The order in the page stays
   board, hand, discards, which is the phone's order. */
.lots-layout { container-type: inline-size; }
@container (min-width: 880px) {
    .lots-grid {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(418px, auto) 12px 440px minmax(0, 1fr);
        grid-template-areas:
            ". board . discards ."
            "hand hand hand hand hand";
        align-items: stretch;
        margin-bottom: 14px;
    }
    .lots-grid > .lots-board { grid-area: board; margin: 0 0 12px; justify-self: center; }
    .lots-grid > #hand-panel { grid-area: hand; margin-bottom: 0; }
    .lots-grid > .panel.lots-discards {
        grid-area: discards;
        max-width: none;
        margin: 0 0 12px;
        padding: 10px;
    }
    .lots-grid .lots-discrow { grid-template-columns: 2.4em 1.1em minmax(0, 1fr); }
    .lots-grid .lots-disccards { gap: 1px; }
    .lots-grid .lots-discjokers .lots-discsuit { font-size: 11px; }
}
/* --- the status and progress lines ---------------------------------------- */
.status.lots-status { min-height: 1.45em; }
.progress.lots-progress { white-space: nowrap; }
@media (max-width: 560px) {
    /* "Bid your last card. Bot's last card is face down." takes two lines on
       a phone: reserve them in every phase. */
    .status.lots-status { min-height: 2.9em; }
    .progress.lots-progress { white-space: normal; min-height: 2.9em; }
}
/* --- the board ------------------------------------------------------------ */
.lots-board {
    --lots-cw: 46px;
    --lots-ch: calc(var(--lots-cw) * 89 / 64);
    --lots-sw: 28px;
    --lots-sh: calc(var(--lots-sw) * 89 / 64);
    --lots-gap: 5px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    margin: 0 auto 12px;
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
    width: fit-content;
    max-width: 100%;
    box-sizing: border-box;
}
@media (max-width: 560px) {
    .lots-board { --lots-cw: 34px; --lots-sw: 22px; --lots-gap: 3px; padding: 8px 6px; gap: 8px; }
}
.lots-board .card.mini { --cw: var(--lots-cw); }
.lots-board .card { position: relative; }
/* --- score bars on the simplified J, Q, and K ------------------------------ */
/* Lots scores a Jack 11, a Queen 0, and a King 15, which no card shows and
   which is easy to get wrong, so the simplified J, Q, and K carry their score
   in a gold bar along the bottom edge (def.ts faceClass; the user's ask,
   2026-11-02). Not a restated rank (ui-conventions, "No derivable text"):
   the number is the one fact the card does not print. An ::after clipped by
   the card, so the atlas and every layout rule are untouched. Hidden on a
   phone's 22px strips, where the number would be too small to read. */
.card.simple.lots-face { position: relative; overflow: hidden; }
.card.simple.lots-face::after {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    background: #7a5a0e;
    color: #fff;
    /* 11px on the 46px board, and no smaller than 7px on the 26 to 28px strips. */
    font-size: max(7px, calc(var(--cw) * 0.24));
    font-weight: 800;
    line-height: 1.1;
    text-align: center;
    font-variant-numeric: tabular-nums;
    pointer-events: none;
}
.card.simple.lots-face-j::after { content: '11'; }
.card.simple.lots-face-q::after { content: '0'; }
.card.simple.lots-face-k::after { content: '15'; }
@media (max-width: 560px) {
    .lots-knownrow .lots-face::after,
    .lots-discards .lots-face::after,
    .lots-finalhand .lots-face::after { display: none; }
}
/* --- the cards the last auction gave the hand ------------------------------ */
/* A dotted outline until the next auction settles (def.ts justTaken); the
   hint's solid ring wins while it flashes. */
.lots-handpanel #hand-cards .card.lots-new:not(.hinted) {
    outline: 2px dotted var(--accent);
    outline-offset: 2px;
}
/* --- the top seat: the cards known to be in the other hand ---------------- */
.lots-seat {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.lots-known {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: calc(var(--lots-sh) + 2px);
    white-space: nowrap;
}
.lots-knownlabel { font-size: 13px; font-weight: 700; opacity: 0.85; }
.lots-knownrow {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: 2px;
}
.lots-board .lots-knownrow .card.mini { --cw: var(--lots-sw); }
/* --- the table: talon, lot, consolation ----------------------------------- */
.lots-table {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    gap: 14px;
}
.lots-pile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.lots-pilelabel {
    font-size: 12px;
    font-weight: 700;
    opacity: 0.85;
    white-space: nowrap;
}
.lots-pilenote {
    font-size: 11px;
    opacity: 0.7;
    white-space: nowrap;
    min-height: 1.3em;
}
.lots-taloncount { font-weight: 700; font-variant-numeric: tabular-nums; opacity: 0.9; }
.lots-slots {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: var(--lots-gap);
    min-height: var(--lots-ch);
}
@media (max-width: 560px) {
    .lots-table { gap: 8px; }
    .lots-pilelabel { font-size: 11px; }
    .lots-pilenote { font-size: 10px; }
}
/* --- the bids ------------------------------------------------------------- */
.lots-bids {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.lots-bidcaption {
    font-size: 12px;
    opacity: 0.8;
    white-space: nowrap;
    min-height: 1.4em;
}
/* Fixed tracks only, so the two rows line up by column whatever the names
   (ui-conventions "Score tables line up by column"). */
.lots-bidrow {
    display: grid;
    grid-template-columns: 6.5em calc(3 * var(--lots-cw) + 2 * var(--lots-gap)) 6.5em;
    align-items: center;
    column-gap: 10px;
    padding: 2px 6px;
    border-radius: 8px;
}
.lots-bidwho {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
    min-width: 0;
}
.lots-bidname {
    max-width: 100%;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.lots-role {
    min-height: 1.35em;
    padding: 0 7px;
    border-radius: 9px;
    font-size: 11px;
    font-weight: 700;
    line-height: 1.35em;
    white-space: nowrap;
    opacity: 0.75;
}
.lots-role:empty { padding: 0; }
.lots-role.lots-elder { background: rgba(255, 209, 102, 0.22); opacity: 1; }
.lots-bidrow .lots-slots { justify-content: flex-start; }
.lots-bidtotal {
    text-align: left;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.lots-bidrow.lots-won { background: rgba(255, 209, 102, 0.16); }
.lots-bidrow.lots-won .lots-bidtotal { color: var(--accent); }
.lots-board .card.lots-facedown { outline: 2px dashed rgba(255, 255, 255, 0.75); outline-offset: 1px; }
@media (max-width: 560px) {
    .lots-bidrow { grid-template-columns: 4.6em calc(3 * var(--lots-cw) + 2 * var(--lots-gap)) 5.4em; column-gap: 6px; }
    .lots-bidname, .lots-bidtotal { font-size: 13px; }
}
/* --- the discards ---------------------------------------------------------- */
.panel.lots-discards {
    --lots-dw: 26px;
    --lots-dh: calc(var(--lots-dw) * 89 / 64);
    max-width: 520px;
    margin-inline: auto;
    padding: 10px 12px;
}
.lots-discgrid {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
/* Fixed tracks, so every suit row's cards start at the same place. */
.lots-discrow {
    display: grid;
    grid-template-columns: 3.6em 1.4em minmax(0, 1fr);
    align-items: center;
    column-gap: 6px;
    min-height: calc(var(--lots-dh) + 2px);
}
.lots-discsuit { text-align: right; font-weight: 800; font-size: 16px; white-space: nowrap; }
.lots-discjokers .lots-discsuit { font-size: 12px; font-weight: 700; opacity: 0.85; }
.lots-discvalue { text-align: center; font-weight: 700; font-variant-numeric: tabular-nums; opacity: 0.75; }
.lots-disccards {
    display: flex;
    flex-wrap: nowrap;
    gap: 2px;
    min-width: 0;
}
.lots-discards .card.mini { --cw: var(--lots-dw); }
.lots-discnone { font-size: 12px; opacity: 0.6; }
@media (max-width: 560px) {
    .panel.lots-discards { --lots-dw: 20px; padding: 8px 6px; }
    /* Thirteen cards of one suit overlap rather than wrap, so the row keeps its height. */
    .lots-disccards .card + .card { margin-left: -6px; }
    .lots-discrow { grid-template-columns: 3em 1.2em minmax(0, 1fr); column-gap: 4px; }
}
/* --- the hand panel ------------------------------------------------------- */
/* Thirteen cards in one row above the phone breakpoint, and in two rows on a
   phone: each card overlaps the one before it (by a fifth, and an eighth on a
   phone), which leaves every corner index in view. */
.lots-handpanel #hand-cards { column-gap: 0; }
.lots-handpanel #hand-cards .card + .card { margin-left: calc(var(--cw) * -0.2); }
@media (max-width: 560px) {
    .lots-handpanel #hand-cards .card + .card { margin-left: calc(var(--cw) * -0.125); }
}
.lots-handscore {
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    opacity: 0.8;
    white-space: nowrap;
}
.lots-actions {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: nowrap;
    gap: 8px;
    min-height: 44px;
    margin-bottom: 8px;
    white-space: nowrap;
}
.lots-actions .lots-stopbtn { min-width: 7.5em; }
/* --- the result dialog and the score table -------------------------------- */
.panel.lots-result { text-align: center; }
.panel.lots-result .result-actions { justify-content: center; flex-wrap: wrap; }
.lots-restitle { margin: 0 0 6px; }
.lots-finalhands {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    margin: 4px auto 10px;
}
.lots-finalblock { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.lots-finalname { font-weight: 700; font-size: 14px; }
.lots-finalhand {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: 2px;
}
.lots-finalhand .card.mini { --cw: 28px; }
table.lots-scores {
    margin: 0 auto;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
}
table.lots-scores th,
table.lots-scores td { padding: 3px 12px; white-space: nowrap; }
table.lots-scores .lots-numcol { text-align: right; }
table.lots-scores .lots-textcol { text-align: left; }
table.lots-scores thead th { font-size: 13px; opacity: 0.8; border-bottom: 1px solid var(--panel-border); }
table.lots-scores tbody th { font-weight: 600; }
table.lots-scores tr.lots-totalrow th,
table.lots-scores tr.lots-totalrow td { font-weight: 800; border-top: 1px solid var(--panel-border); }
@media (max-width: 560px) {
    .lots-finalhand .card.mini { --cw: 22px; }
    .lots-finalhand .card + .card { margin-left: -4px; }
    table.lots-scores th,
    table.lots-scores td { padding: 3px 6px; font-size: 13px; }
}
/* ============ Ascension (asc-) ============ */

/* ---- the status and progress lines ---- */
.asc-progress .asc-bit { white-space: nowrap; }
.asc-progress .asc-bit + .asc-bit::before { content: '\00b7'; margin: 0 6px; opacity: 0.7; }
.asc-progress .asc-actname { font-weight: 700; }

/* ---- the pick panel (classes, weapons) ---- */
.asc-pickpanel h3 { text-align: center; }
.asc-picks {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: 10px;
    max-width: 860px;
    margin: 0 auto;
}
.asc-pickbtn {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    padding: 10px 12px;
    border: 1px solid var(--line, #c9c2b6);
    border-radius: 8px;
    background: var(--panel, #fffdf8);
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.asc-pickbtn:hover:not(:disabled) { border-color: var(--accent); }
.asc-pickbtn:disabled { cursor: default; opacity: 0.6; }
.asc-pickbtn.hinted { outline: 3px solid var(--accent); outline-offset: 1px; }
.asc-pickname { font-weight: 700; font-size: 1.05em; }
.asc-picktext { font-size: 0.9em; line-height: 1.3; }
.asc-pickowner { margin-top: 2px; font-size: 0.85em; font-weight: 700; color: var(--accent-ink, #6b4e16); }

/* ---- the board, Acts I and II ----
   The deck, the two zones, and the defeated pile in one centered row. Each
   zone reserves one card's height plus its tag line, so nothing below moves
   as monsters come and go. */
.asc-board {
    --asc-mcw: min(var(--cw), 64px);
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 14px;
    margin: 6px auto 10px;
    flex-wrap: wrap;
}
.asc-pile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    min-width: 56px;
}
.asc-pile > .card { --cw: 46px; }
.asc-pilelabel { font-size: 12px; white-space: nowrap; }
.asc-emptycard {
    width: 46px;
    height: calc(46px * 89 / 64);
    border: 2px dashed var(--line, #c9c2b6);
    border-radius: 6px;
    box-sizing: border-box;
}
.asc-rows { display: flex; flex-direction: column; gap: 6px; }
.asc-zone {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 4px 8px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.035);
}
.asc-zone.asc-blocked { background: rgba(179, 38, 30, 0.07); }
.asc-zonelabel { font-size: 12px; font-weight: 700; white-space: nowrap; text-align: center; }
.asc-zonecards {
    display: flex;
    justify-content: center;
    gap: 8px;
    min-width: calc(var(--asc-mcw) * 2 + 8px);
    min-height: calc(var(--asc-mcw) * 89 / 64 + 18px);
    align-items: flex-start;
}
.asc-zoneempty { align-self: center; font-size: 12px; opacity: 0.7; white-space: nowrap; }
.asc-mslot { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.asc-mslot > .card { --cw: var(--asc-mcw); }
.asc-mtag { font-size: 11px; line-height: 16px; white-space: nowrap; }
.asc-mslot.frozen .asc-mtag { color: #1f5fa8; font-weight: 700; }
/* Frost Armor: the frozen monster is tinted, not turned, so the zone keeps its size. */
.asc-mslot .card.asc-frozen { filter: saturate(0.4) brightness(1.05) hue-rotate(160deg); }
.asc-board .card.asc-target { outline: 3px solid var(--accent); outline-offset: 1px; }

/* ---- the team ---- */
.asc-team {
    display: flex;
    justify-content: center;
    gap: 10px;
    flex-wrap: wrap;
    margin: 0 auto 8px;
}
.asc-hero {
    min-width: 170px;
    padding: 6px 10px;
    border: 1px solid var(--line, #c9c2b6);
    border-radius: 8px;
    background: var(--panel, #fffdf8);
    text-align: center;
}
.asc-hero.turn { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
.asc-hero.fallen { opacity: 0.55; }
.asc-herohead { display: flex; justify-content: center; gap: 8px; align-items: baseline; }
.asc-heroname { font-weight: 700; white-space: nowrap; }
.asc-heroclass { font-size: 0.9em; white-space: nowrap; }
.asc-heroclass.red { color: var(--red, #b3261e); }
.asc-herofacts, .asc-herocolor { font-size: 12px; white-space: nowrap; }
.asc-herofacts .asc-hp { font-weight: 700; }
.asc-herocolor { opacity: 0.75; }
.asc-herohand {
    justify-content: center;
    min-height: calc(46px * 89 / 64);
    margin-top: 4px;
}

/* Wounds in a hand that cannot use them, and Act III cards already played. */
.card.asc-wound { filter: grayscale(0.55) brightness(0.92); }
.card.asc-used { opacity: 0.4; filter: grayscale(0.8); }

/* ---- the hand panel's controls ---- */
.asc-actions {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    min-height: 40px;
    margin-top: 8px;
}
.asc-actions .btn.sel { box-shadow: inset 0 0 0 2px var(--accent); }
.asc-discardrow {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 6px;
    flex-basis: 100%;
}

/* ---- Act III: the grid ----
   Cells are as wide as a card is tall, so a tapped zombie turned sideways
   fits its cell and nothing around it moves. */
.asc-descentboard { --asc-gcw: min(var(--cw), 50px); flex-direction: column; flex-wrap: nowrap; }
.asc-gridrow { display: flex; justify-content: center; align-items: stretch; gap: 14px; }
.asc-gridside {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 6px;
}
.asc-grid {
    display: grid;
    grid-template-columns: repeat(5, calc(var(--asc-gcw) * 89 / 64));
    grid-template-rows: repeat(2, calc(var(--asc-gcw) * 89 / 64));
    gap: 6px;
    padding: 6px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.04);
}
.asc-cell {
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.035);
}
.asc-cell > .card { --cw: var(--asc-gcw); }
.asc-emptycell { width: 60%; height: 60%; border: 2px dashed var(--line, #d8d2c6); border-radius: 6px; }
/* A tapped zombie turns sideways with `rotate`, never `transform`
   (ui-conventions.md), scoped under its cell so a flight ghost carries none. */
.asc-cell .card.asc-tapped { rotate: 90deg; }
.asc-portalmark { border-color: #b3261e; border-radius: 50%; }
.asc-ammo {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
}
/* One row that scrolls rather than wraps, so a growing pool never pushes the
   team and the hand down. Centered with auto margins on the ends, not
   justify-content, which would clip the left overflow (ui-conventions.md). */
.asc-ammocards {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 4px;
    max-width: 100%;
    min-height: calc(46px * 89 / 64 + 4px);
    align-items: center;
}
.asc-ammo { max-width: 100%; }
.asc-ammocards > :first-child { margin-left: auto; }
.asc-ammocards > :last-child { margin-right: auto; }
.asc-ammocards > .card { flex: 0 0 auto; }
.asc-pilelabel.asc-warn { color: #ffb4a8; font-weight: 700; }

/* ---- the log ---- */
#log-entries .asc-logbad { color: #b3261e; }
#log-entries .asc-loggood { color: #2e7d32; }

@media (max-width: 600px) {
    .asc-board { --asc-mcw: min(var(--cw), 52px); gap: 8px; }
    /* The grid, the deck, and the portal side by side down to 360px: smaller
       cells, a tighter gap, and the two piles as minis. */
    .asc-descentboard { --asc-gcw: 36px; gap: 6px; }
    .asc-gridrow { gap: 6px; }
    .asc-descentboard .asc-grid { gap: 4px; padding: 4px; }
    .asc-descentboard .asc-pile { min-width: 40px; }
    .asc-descentboard .asc-pile > .card { --cw: 32px; }
    .asc-descentboard .asc-pile .asc-emptycard { width: 32px; height: calc(32px * 89 / 64); }
    .asc-descentboard .asc-pilelabel { font-size: 11px; }
    .asc-hero { min-width: 0; flex: 1 1 140px; max-width: 200px; padding: 6px; }
    /* Every prompt fits two lines at 360px; reserving them keeps the board still. */
    .status.asc-status { line-height: 1.3; min-height: 2.6em; }
    /* The discard step's three buttons take two rows here; every step reserves them. */
    .asc-actions { min-height: 88px; align-content: flex-start; }
}
/* =========================================================================
   Nein Nimmt!
   Top to bottom inside #nein-board: the top seat's header and its four
   strawman piles (small, read-only), the middle (the three rows beside the
   trick), and the bottom seat's strawman piles (full size, armed like hand
   cards) and header. Then the page's hand panel.
   Every box reserves its tallest shape in terms of a card width and the card
   aspect (89/64, about 1.39), so nothing above the hand moves as the game
   runs: each row always paints its three slots and its take slot, the trick
   its two, each pile one card, and each header its chips and its taken pile.
   The "Unseen cards" panel sits under the hand. The small cards are 46px on a desktop
   and 34px on a phone, on the simplified atlas (read-only cards at or under
   46px, ui-conventions "Small cards use the simplified sprite").
   ========================================================================= */
/* --- the status and progress lines ---------------------------------------- */
.status.nein-status { min-height: 1.45em; }
.progress.nein-progress { white-space: nowrap; }
@media (max-width: 560px) {
    /* A reply's line ("Bot led the 8♠. You have no Spade, so play any card.")
       takes two lines on a phone: reserve them in every phase. */
    .status.nein-status { min-height: 2.9em; }
}
/* --- the board ------------------------------------------------------------ */
.nein-board {
    --nein-scw: 46px;                 /* rows and the top seat's strawmen */
    --nein-sch: calc(var(--nein-scw) * 89 / 64);
    --nein-tcw: 64px;                 /* the trick */
    --nein-tch: calc(var(--nein-tcw) * 89 / 64);
    --nein-pcw: 72px;                 /* the bottom seat's strawmen */
    --nein-pch: calc(var(--nein-pcw) * 89 / 64);
    --nein-gap: 4px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    margin: 0 auto 12px;
    padding: 10px 12px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
    width: fit-content;
    max-width: 100%;
    box-sizing: border-box;
}
@media (max-width: 560px) {
    .nein-board {
        --nein-scw: 34px;
        /* Above 46px, so the trick keeps the full art (ui-conventions, small cards). */
        --nein-tcw: 50px;
        --nein-pcw: 52px;
        --nein-gap: 3px;
        padding: 8px 6px;
        gap: 6px;
    }
}
.nein-board .card { position: relative; }
.nein-board .card.mini { --cw: var(--nein-scw); }
/* --- the seat headers ----------------------------------------------------- */
.nein-seat {
    --nein-pilecw: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    /* One small card tall, so the taken pile filling up moves nothing. */
    min-height: calc(var(--nein-pilecw) * 89 / 64 + 4px);
    white-space: nowrap;
}
.nein-name { font-weight: 700; }
.nein-score {
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    padding: 1px 10px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.1);
    min-width: 2.5em;
    text-align: center;
    box-sizing: border-box;
}
.nein-handcount {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-variant-numeric: tabular-nums;
    opacity: 0.85;
}
.nein-board .card.nein-handback { --cw: 14px; box-shadow: none; }
/* The taken pile: the stack of +1 cards with its count, then the 9s face up.
   The stack is always there (an outline before the first card is taken), so
   only a 9 arriving widens the header, by one small card. */
.nein-taken {
    display: inline-flex;
    align-items: center;
    gap: 3px;
}
.nein-board .card.nein-stack.empty { opacity: 0.6; }
.nein-board .nein-taken .card { --cw: var(--nein-pilecw); }
.nein-board .card.nein-stack { display: inline-flex; align-items: center; justify-content: center; }
.nein-stacknum {
    font-size: 13px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: #fff;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}
/* --- the strawman piles --------------------------------------------------- */
.nein-straw {
    display: flex;
    justify-content: center;
    gap: 10px;
}
/* A flex box, so an armed top (a <button>) and a plain one (a <div>) sit at
   one height: an inline-block button on a line box adds the descender below
   it, measured at 7px, which moved everything under the row. The padding is
   the room the card underneath peeks out into, on every pile, so a pile that
   runs out of hidden cards changes no size. */
.nein-pile { position: relative; display: flex; padding: 0 7px 7px 0; }
.nein-straw-top .nein-pile { --cw: var(--nein-scw); }
.nein-straw-bottom .nein-pile { --cw: var(--nein-pcw); }
.nein-straw-bottom .nein-pile .card { --cw: var(--nein-pcw); }
.nein-pile .card { z-index: 1; }
/* A pile still hiding a card shows that card's back, the real card-back art,
   peeking out down and to the right from under its face-up top. */
.nein-pile .card.nein-under {
    position: absolute;
    left: 7px;
    top: 7px;
    z-index: 0;
}
.nein-board .card.nein-waiting { opacity: 0.85; }
/* A card the follow rule forbids: a filter, never opacity, beside its neighbors. */
.nein-board .card.nein-dim,
.nein-handpanel .card.nein-dim { filter: grayscale(0.55) brightness(0.72); }
/* --- the middle: the rows beside the trick -------------------------------- */
.nein-mid {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
}
.nein-rows {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.nein-row {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 3px 4px;
    border-radius: 8px;
    border: 2px solid transparent;
    background: rgba(255, 255, 255, 0.04);
    color: inherit;
    font: inherit;
    text-align: left;
}
button.nein-row { cursor: pointer; }
.nein-row.nein-pick { border-color: var(--accent); background: rgba(255, 209, 102, 0.12); }
.nein-row.nein-pick:hover { background: rgba(255, 209, 102, 0.22); }
.nein-row.nein-dimrow { opacity: 0.5; }
.nein-rowlabel {
    width: 1.2em;
    font-size: 12px;
    font-weight: 700;
    opacity: 0.7;
    text-align: center;
}
.nein-rowcards {
    display: flex;
    gap: var(--nein-gap);
    min-height: var(--nein-sch);
}
/* The take slot after a row's three: what an armed row would take, or the
   card about to take the row while a trick plays out. One card wide. */
.nein-take {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    width: var(--nein-scw);
    min-height: var(--nein-sch);
    font-size: 13px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.nein-takepts.nein-good { color: #8fe3a2; }
.nein-takepts.nein-cost { color: #ff9aa8; }
.nein-board .card.nein-over { outline: 3px solid #ff7a8a; outline-offset: 1px; }
/* The trick: padded by the stagger, so the shifted cards stay in the column. */
.nein-trick {
    --nein-stagger: 10px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 0 var(--nein-stagger);
}
.nein-trickslot { --cw: var(--nein-tcw); display: flex; min-height: var(--nein-tch); }
.nein-trickslot .card { --cw: var(--nein-tcw); }
.nein-board .card.nein-won { outline: 3px solid var(--accent); outline-offset: 1px; }
.nein-board .card.nein-pending { outline: 3px dashed var(--accent); outline-offset: 2px; }
/* Which card was led: the led card sits a little left and the reply a little
   right (the Le Truc stagger). On the slot, with `translate` rather than
   `transform` (ui-conventions, sideways cards), so a flight or a lift on the
   card itself never undoes it, and an empty reply slot holds it too. */
.nein-trickslot.nein-ledslot { translate: calc(-1 * var(--nein-stagger)) 0; }
.nein-trickslot.nein-replyslot { translate: var(--nein-stagger) 0; }
/* --- the 9s ---------------------------------------------------------------- */
/* Every 9 is ringed and badged with what it scores, wherever it is. */
.card.nein-nine {
    box-shadow: 0 0 0 2px #4fd18b, 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.nein-ninebadge {
    position: absolute;
    right: -5px;
    bottom: -5px;
    padding: 0 4px;
    border-radius: 8px;
    background: #2f9e62;
    color: #fff;
    font-size: 11px;
    font-weight: 800;
    line-height: 15px;
    font-variant-numeric: tabular-nums;
    pointer-events: none;
}
.card.mini .nein-ninebadge { font-size: 9px; line-height: 12px; padding: 0 3px; right: -4px; bottom: -4px; }
/* --- the hand panel ------------------------------------------------------- */
.nein-handpanel .card { position: relative; }
/* --- the unseen cards ---------------------------------------------------- */
/* A fixed grid, every rank of every suit, so nothing in it moves either. */
.panel.nein-unseen { --nein-ucw: 30px; }
.nein-unseenhead {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 8px;
    margin-bottom: 6px;
}
.nein-unseentitle { margin: 0; }
.nein-unseencount {
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    padding: 0 8px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.1);
}
.nein-unseengrid {
    display: grid;
    grid-template-columns: 16px repeat(9, var(--nein-ucw));
    gap: 4px;
    justify-content: center;
    align-items: center;
}
.nein-unseen .card { --cw: var(--nein-ucw); position: relative; }
.nein-unseenrank,
.nein-unseensuit {
    font-size: 12px;
    font-weight: 700;
    text-align: center;
    opacity: 0.75;
}
.nein-unseensuit { font-size: 14px; opacity: 1; }
.nein-unseen .card.nein-seen { opacity: 0.25; }
/* --- the log --------------------------------------------------------------- */
#log-entries .logline.nein-logtake { font-weight: 600; }
/* --- the result dialog and the score table -------------------------------- */
.panel.nein-result { text-align: center; }
.panel.nein-result .result-actions { justify-content: center; flex-wrap: wrap; }
table.nein-scores {
    margin: 6px auto 4px;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
}
table.nein-scores th,
table.nein-scores td { padding: 3px 10px; white-space: nowrap; }
table.nein-scores .nein-numcol { text-align: right; }
table.nein-scores .nein-textcol { text-align: left; }
table.nein-scores thead th { font-size: 13px; opacity: 0.8; border-bottom: 1px solid var(--panel-border); }
table.nein-scores tbody th { font-weight: 600; }
table.nein-scores tr.nein-totalrow th,
table.nein-scores tr.nein-totalrow td { font-weight: 800; border-top: 1px solid var(--panel-border); }
@media (max-width: 560px) {
    .nein-seat { --nein-pilecw: 24px; gap: 8px; }
    .nein-trick { --nein-stagger: 7px; }
    .panel.nein-unseen { --nein-ucw: 26px; }
    .nein-unseengrid { gap: 3px; }
    .nein-mid { gap: 8px; }
    .nein-straw { gap: 6px; }
    .nein-take { min-width: 2em; font-size: 12px; }
    table.nein-scores th,
    table.nein-scores td { padding: 3px 6px; font-size: 13px; }
}
/* =========================================================================
   Hedges (hedges)
   A climbing game for two. Beat the combo on the table with a higher one of
   the same kind and length, or a bomb, or pass; the trick's winner takes its
   5s, 10s, and Kings and claims or hedges them. The board is built around
   what a player reads every turn:
     the table as a table: the opponent North, the viewer South, each a
       one-row panel with its cards left, what it has claimed, Passed, its
       point cards (a hedged group under a card laid across it), and its
       total                                           (.hdg-seat)
     the middle row: Heaven's Road as an overlapping column, its top card
       last and fully shown; the play to beat at full size with the trick's
       earlier plays small; the talon and the four cards out of play
                                                       (.hdg-middle)
     a trick that just ended held, ringed and starred, its point cards
       marked, until the next lead                     (.hdg-winning)
   Under the controls, the hand: ONE row on a desktop, overlapping when it
   would wrap, so the board and the whole hand fit a laptop screen. Under the
   hand, the melds (one tap each on the player's turn, .hdg-meldbox) and the
   unseen cards (.hdg-unseen). On the viewer's own turn, a card no play can
   use is darkened (.hdg-dim), never made see-through: the road and the
   desktop hand overlap, and a transparent card shows the one beneath it.
   A point card in the hand, on the road, or in the play to beat carries a
   gold oval with its value at its bottom center ([data-hdg-pts]).
   EVERY CARD SIZE is a custom property on .hdg-board, never a rule on .card or
   a shared id, so each reservation follows it.
   ANTI-JITTER (ui-conventions.md, "Containers must not move"). Every box above
   the hand is reserved at its tallest ordinary shape, and re-derived at the
   phone breakpoint: the seat panels at one height in every state (the marks
   are switched by .hdg-off, which keeps their space, and the point cards sit
   in one row that scrolls), the road at six cards, the label at two lines,
   the play to beat at one row, the earlier plays at one scrolling row, the
   controls at one row, the note at two lines, the melds at two chip rows that
   scroll, the status line likewise. Nothing on the board is ever `hidden` in
   a game.
   SHARED IDS are reached only through a class this game owns: the hand panel
   through .hdg-handpanel, the status and progress lines through .hdg-status
   and .hdg-progress.
   ========================================================================= */

/* --- sizes ------------------------------------------------------------- */
.hdg-board {
    --hdg-playcw: 56px;
    --hdg-earliercw: 28px;
    --hdg-roadcw: 50px;
    --hdg-roadstep: 18px;
    --hdg-pilecw: 30px;
    --hdg-capcw: 24px;
    --hdg-centerw: 300px;
    --hdg-seatw: 640px;
    /* The width the play to beat may take: the center less its padding. */
    --hdg-topw: calc(var(--hdg-centerw) - 20px);
}

/* Switched on and off without moving anything: the space is kept. */
.hdg-off { visibility: hidden; }

/* --- the board: the table as a table ------------------------------------ */
.hdg-board {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}
.hdg-middle {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: flex-start;
    gap: 14px;
    max-width: 100%;
}

/* --- a seat's panel ------------------------------------------------------ */
.hdg-seat {
    box-sizing: border-box;
    width: var(--hdg-seatw);
    max-width: 100%;
    min-width: 0;
    padding: 4px 10px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.hdg-seat.hdg-active { outline-color: var(--accent); }
/* ONE row, as tall as the point-card strip, whatever it holds. */
.hdg-seat-row {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 9px;
    white-space: nowrap;
    min-height: calc(var(--hdg-capcw) * 89 / 64 + 4px);
}
.hdg-toact { flex: none; color: var(--accent); font-size: 11px; }
.hdg-seat-name {
    flex: 0 1 auto;
    min-width: 0;
    max-width: 9em;
    font-weight: 700;
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.hdg-seat-games {
    flex: none;
    font-size: 12px;
    font-weight: 700;
    opacity: 0.85;
    white-space: nowrap;
}
.hdg-seat-total {
    flex: none;
    min-width: 2.2ch;
    text-align: right;
    font-size: 16px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.hdg-count,
.hdg-claimed {
    flex: none;
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.hdg-count { min-width: 5.5ch; }
.hdg-claimed { min-width: 9.5ch; }
.hdg-said {
    flex: none;
    font-size: 11px;
    opacity: 0.85;
    font-style: italic;
    white-space: nowrap;
}
/* The point cards a seat won this hand: the rest of the row, scrolling rather
   than wrapping, newest at the right end (def.ts scrolls it there), and NOT
   centered (a centered scrolling strip clips its left end). */
.hdg-caps {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: center;
    gap: 8px;
    height: calc(var(--hdg-capcw) * 89 / 64 + 4px);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}
.hdg-caps-none { font-size: 12px; opacity: 0.6; white-space: nowrap; }
.hdg-cap {
    position: relative;
    flex: none;
    display: flex;
    flex-wrap: nowrap;
    border-radius: 3px;
}
.hdg-cap > .card + .card { margin-left: calc(-0.35 * var(--hdg-capcw)); }
.hdg-cap .card.hdg-capcard { --cw: var(--hdg-capcw); flex: none; }
/* A hedged group: a card back laid across its lower half, as on the table.
   A plain box, not a turned .card, so nothing animates it. */
.hdg-cap.hdg-hedged .card.hdg-capcard { opacity: 0.85; }
.hdg-cap .card.hdg-hedgecard {
    position: absolute;
    left: -3px;
    right: -3px;
    bottom: 1px;
    width: auto;
    height: calc(var(--hdg-capcw) * 0.62);
    --cw: var(--hdg-capcw);
    border-radius: 3px;
    background-size: 1400% 400%;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

/* --- Heaven's Road ------------------------------------------------------- */
.hdg-road {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    width: calc(var(--hdg-roadcw) + 22px);
}
.hdg-roadtitle {
    font-size: 11px;
    font-weight: 700;
    opacity: 0.85;
    white-space: nowrap;
    margin-bottom: 3px;
}
/* SIX cards reserved: one card and five steps, whatever is left. */
.hdg-roadcol {
    display: flex;
    flex-direction: column;
    align-items: center;
    height: calc(var(--hdg-roadcw) * 89 / 64 + 5 * var(--hdg-roadstep));
}
.hdg-roadcol .card.hdg-roadcard { --cw: var(--hdg-roadcw); flex: none; }
.hdg-roadcol .card.hdg-roadcard + .card.hdg-roadcard {
    margin-top: calc(var(--hdg-roadstep) - var(--hdg-roadcw) * 89 / 64);
}
.hdg-roadcol .card.hdg-roadcard:not(.hdg-roadtop) { box-shadow: 0 2px 2px rgba(0, 0, 0, 0.35); }
.hdg-roadnone { font-size: 12px; opacity: 0.6; margin-top: 20px; }

/* --- the play area ------------------------------------------------------- */
.hdg-center {
    flex: 0 1 var(--hdg-centerw);
    box-sizing: border-box;
    width: var(--hdg-centerw);
    min-width: 0;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
/* The play to beat in words, with who played it: TWO lines reserved. */
.hdg-label {
    height: 2.8em;
    line-height: 1.4;
    font-size: 14px;
    white-space: normal;
    text-align: center;
    overflow: hidden;
}
.hdg-label-who {
    display: inline-block;
    max-width: 10em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: bottom;
    font-weight: 700;
}
/* The play to beat, at full size: one reserved row that overlaps to fit. */
.hdg-top {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    min-height: calc(var(--hdg-playcw) * 89 / 64 + 8px);
}
.hdg-top .hdg-group {
    position: relative;
    display: flex;
    flex-wrap: nowrap;
    border-radius: 8px;
}
/* Cards a third under the next, so each one's corner and point badge show; a
   long play closes up to fit --hdg-topw (n is the play's card count, from
   def.ts). */
.hdg-top .hdg-group > .card + .card {
    margin-left: min(calc(-0.3 * var(--hdg-playcw)),
        calc((var(--hdg-topw) - var(--hdg-n, 2) * var(--hdg-playcw)) / (var(--hdg-n, 2) - 1)));
}
.hdg-top .card.hdg-topcard { --cw: var(--hdg-playcw); flex: none; }
.hdg-top .hdg-group.hdg-winning {
    outline: 3px solid var(--accent);
    outline-offset: 3px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
/* A point card in a finished trick: a gold underline, so not color alone. */
.hdg-center .card.hdg-pointcard { box-shadow: 0 3px 0 0 var(--accent), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45); }
.hdg-star {
    position: absolute;
    top: -11px;
    right: -11px;
    width: 18px;
    height: 18px;
    line-height: 18px;
    border-radius: 50%;
    text-align: center;
    font-size: 12px;
    color: var(--accent-ink);
    background: var(--accent);
}
/* The trick's earlier plays, oldest left: ONE row that scrolls rather than
   wraps, NOT centered (a centered scrolling strip clips its left end). */
.hdg-earlier {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: center;
    gap: 8px;
    height: calc(var(--hdg-earliercw) * 89 / 64 + 8px);
    margin-top: 6px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}
.hdg-earlier .hdg-group { flex: none; display: flex; }
.hdg-earlier .hdg-group.hdg-small > .card + .card { margin-left: calc(-0.45 * var(--hdg-earliercw)); }
.hdg-earlier .card.hdg-earliercard { --cw: var(--hdg-earliercw); flex: none; }

/* --- the talon and the cards out of play --------------------------------- */
.hdg-piles {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    width: calc(var(--hdg-pilecw) + 50px);
    padding-top: 16px;
}
.hdg-talon,
.hdg-buried {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
}
.hdg-piles .card.hdg-taloncard { --cw: var(--hdg-pilecw); flex: none; }
.hdg-piles .card.hdg-taloncard.hdg-empty { opacity: 0.25; }
.hdg-buriedbacks { display: flex; flex-wrap: nowrap; }
.hdg-buriedbacks .card.hdg-buriedcard { --cw: calc(var(--hdg-pilecw) * 0.6); flex: none; }
.hdg-buriedbacks .card.hdg-buriedcard + .card.hdg-buriedcard { margin-left: calc(-0.4 * var(--hdg-pilecw)); }
.hdg-pilecount {
    font-size: 12px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* --- the action box ---------------------------------------------------- */
.hdg-actions { margin-bottom: 6px; }
.hdg-controls {
    --hdg-playw: 17em;
    flex-wrap: nowrap;
    align-items: stretch;
    margin: 4px 0;
    /* One .btn high (38px at 16px), so the row is one height with buttons or none. */
    min-height: 2.4em;
}
.hdg-controls .btn.hdg-play,
.hdg-controls .btn.hdg-claim {
    flex: none;
    box-sizing: border-box;
    width: var(--hdg-playw);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.hdg-controls .btn.hdg-pass,
.hdg-controls .btn.hdg-hedge {
    flex: none;
    box-sizing: border-box;
    width: 8.5em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.hdg-controls .btn.hinted,
.hdg-melds .btn.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
.hdg-note {
    min-height: 2.6em;
    line-height: 1.3;
    font-size: 14px;
    text-align: center;
}

/* --- the melds --------------------------------------------------------- */
/* A compact box: a small title and two rows of chips, the rest scrolling, so a
   hand that grows a meld never moves the hand below. A row is one chip high:
   --hdg-chiph, the desktop .btn.small, and the coarse-pointer button minimum
   (36px) on a touch screen. */
.hdg-meldbox {
    --hdg-chiph: 25px;
    padding: 6px 10px 6px;
    margin: 8px 0;
}
.hdg-meldbox h3.hdg-meldtitle {
    margin: 0 0 4px;
    font-size: 13px;
    line-height: 16px;
}
.hdg-melds {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-content: flex-start;
    gap: 4px 6px;
    height: calc(2 * var(--hdg-chiph) + 4px);
    box-sizing: content-box;
    overflow-y: auto;
    scrollbar-width: thin;
}
@media (pointer: coarse) {
    .hdg-meldbox { --hdg-chiph: 36px; }
}
.hdg-melds .btn.hdg-chip {
    flex: none;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.hdg-melds .btn.hdg-chip.hdg-chip-bomb { border-color: var(--accent); }
.hdg-melds .btn.hdg-chip.hdg-chip-road { border-style: dashed; }
.hdg-melds .btn.hdg-chip.hdg-chip-idle { opacity: 0.6; cursor: default; }
.hdg-meldfam {
    flex: none;
    align-self: center;
    font-size: 12px;
    font-weight: 700;
    opacity: 0.8;
    white-space: nowrap;
}
.hdg-meldhint {
    align-self: center;
    font-size: 13px;
    opacity: 0.75;
    white-space: nowrap;
}

/* Suit glyphs spliced into the game's own text (def.ts appendSuitColored),
   the colors the game log and the card labels use. */
.hdg-melds .red,
.hdg-label .red,
.hdg-note .red,
.hdg-controls .red,
.hdg-pointsline .red,
.status.hdg-status .red { color: var(--red); }
.hdg-melds .red.diamond,
.hdg-label .red.diamond,
.hdg-note .red.diamond,
.hdg-controls .red.diamond,
.hdg-pointsline .red.diamond,
.status.hdg-status .red.diamond { color: var(--red-diamond); }
.hdg-melds .club,
.hdg-label .club,
.hdg-note .club,
.hdg-controls .club,
.hdg-pointsline .club,
.status.hdg-status .club { color: var(--blue-club); }
/* On the primary button's light fill: a dark red, and clubs in the button's own ink. */
.hdg-controls .btn.primary .red { color: #b3261e; }
.hdg-controls .btn.primary .red.diamond { color: #b4471a; }
.hdg-controls .btn.primary .club { color: inherit; }

/* --- the hand and the road's top --------------------------------------- */
/* On the viewer's own turn: a card no allowed play uses (not a button), or,
   with a selection, one that cannot join it (still tappable). */
/* DARKENED, NOT TRANSPARENT. Heaven's Road is an overlapping column and the
   desktop hand overlaps too, so a see-through card showed the card under it
   through itself (play-test, 2026-11-01: "the first card in Heaven's Road
   became transparent"). */
.hdg-handpanel .card.hdg-dim,
.hdg-roadcol .card.hdg-dim { filter: brightness(0.5) saturate(0.55); }
/* A card drawn since the viewer's last play: a soft gold edge and a dot at its
   top, so not color alone (the tooltip says "Just drawn"). */
.hdg-handpanel .card.hdg-new {
    position: relative;
    box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.75), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.hdg-handpanel .card.hdg-new::after {
    content: '';
    position: absolute;
    top: -5px;
    left: 50%;
    width: 9px;
    height: 9px;
    margin-left: -4.5px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.55);
    pointer-events: none;
}
/* The cards a hovered meld would use: a dashed ring, so not color alone. */
.hdg-handpanel .card.hdg-preview,
.hdg-roadcol .card.hdg-preview {
    outline: 2px dashed var(--accent);
    outline-offset: 2px;
}

/* THE HAND ON A DESKTOP: ONE ROW. Past the cards that fit with the usual 8px
   gap, the cards overlap, by the step that makes the row exactly the tray's
   width: (100% - n * card) / (n - 1), where n is the card count def.ts puts in
   --hdg-n and a percentage margin is a share of the tray's width. A wrapped
   second row was 111px that pushed the hand off a laptop screen. Every card's
   corner index and its point badge stay in view down to a step of half a card,
   which a 20-card hand at 768px still clears. Phones keep the wrapping hand. */
@media (min-width: 721px) {
    .hdg-handpanel #hand-cards { flex-wrap: nowrap; gap: 0; }
    .hdg-handpanel #hand-cards > .card + .card {
        margin-left: min(8px, calc((100% - var(--hdg-n, 2) * var(--cw)) / (var(--hdg-n, 2) - 1)));
    }
}

/* --- the point badge --------------------------------------------------- */
/* A gold oval with the card's value (5 or 10) at its bottom center. Keyed on the
   attribute, not on a container, so a flight ghost (a clone in <body>) carries
   it too. Sized from the card, so it reads on the road's smaller cards. */
/* EVERY card in an overlapping row is positioned, not only the badged ones. A
   positioned card, like one darkened by a filter or lifted by a transform, is
   painted after the plain cards beside it, so a lone badged or darkened card
   covered the corner of the card that should overlap it (the K covering the
   A after it). With all of them positioned they paint in order again. A
   flight ghost is position: fixed, which is positioned too. */
.hdg-handpanel #hand-cards > .card,
.hdg-roadcol .card,
.hdg-top .card { position: relative; }
.card[data-hdg-pts]::before {
    content: attr(data-hdg-pts);
    position: absolute;
    left: 50%;
    bottom: 5%;
    transform: translateX(-50%);
    min-width: 1.75em;
    padding: 0 0.3em;
    box-sizing: border-box;
    border-radius: 999px;
    background: linear-gradient(#ffe39a, #f0b429);
    border: 1px solid rgba(90, 60, 0, 0.55);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
    color: #3a2c00;
    font-size: max(9px, calc(var(--cw) * 0.19));
    font-weight: 800;
    line-height: 1.3;
    text-align: center;
    font-variant-numeric: tabular-nums;
    pointer-events: none;
}

/* --- the unseen cards -------------------------------------------------- */
/* Below the hand: one row per suit, the ranks 5 up to 2, the Jokers last. An
   unseen card shows its face (the simplified atlas: these are 26px), a seen one
   an empty cell of the same size, so the grid stays a map. */
.hdg-unseen { --hdg-ucw: 26px; padding: 6px 10px 8px; margin-bottom: 8px; overflow-x: auto; }
.hdg-unseen-head {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: baseline;
    gap: 4px 10px;
    margin-bottom: 5px;
}
.hdg-unseen h3.hdg-unseen-title { margin: 0; font-size: 13px; line-height: 16px; }
.hdg-unseen-count { font-size: 12px; opacity: 0.85; }
.hdg-unseen-grid {
    display: grid;
    grid-template-columns: 1.4em repeat(12, var(--hdg-ucw));
    gap: 2px;
    align-items: center;
    width: max-content;
    margin-inline: auto;
}
.hdg-unseen-grid .card.mini { --cw: var(--hdg-ucw); }
.hdg-unseen-rank { font-size: 11px; text-align: center; opacity: 0.6; font-variant-numeric: tabular-nums; }
.hdg-unseen-suit { font-size: 14px; text-align: center; }
.hdg-unseen-suit.red { color: var(--red); }
.hdg-unseen-suit.red.diamond { color: var(--red-diamond); }
.hdg-unseen-suit.club { color: var(--blue-club); }
.hdg-unseen-grid .card.hdg-unseen-seen {
    background-image: none;
    background-color: rgba(255, 255, 255, 0.05);
    box-shadow: none;
}
.hdg-unseen-blank { width: var(--hdg-ucw); }

/* --- status and progress ----------------------------------------------- */
.status.hdg-status { line-height: 1.35; min-height: 2.7em; }
.status.hdg-status.hdg-turn { color: var(--accent); font-weight: 700; }
.progress.hdg-progress { min-height: 1.4em; white-space: nowrap; }

/* --- the summary ------------------------------------------------------- */
.hdg-pointsline { margin: 0 0 6px; text-align: center; font-size: 14px; }
.hdg-result {
    border-collapse: collapse;
    margin: 0 auto 10px;
    font-size: 14px;
}
.hdg-result th,
.hdg-result td {
    padding: 4px 10px;
    border-bottom: 1px solid var(--panel-border);
    white-space: nowrap;
}
.hdg-result th { font-size: 12px; font-weight: 600; opacity: 0.85; }
.hdg-result .hdg-name { text-align: left; }
.hdg-result .hdg-num { text-align: right; font-variant-numeric: tabular-nums; }
.hdg-result tr.hdg-best td { color: var(--accent); font-weight: 700; }

/* --- phones ------------------------------------------------------------ */
/* The middle row stays one row: the road and the piles narrow, the center
   takes what is left. Every reservation above is re-derived here. */
@media (max-width: 720px) {
    .hdg-board {
        --hdg-centerw: 100%;
        /* The center is what the road and the piles leave of the screen. */
        --hdg-topw: calc(100vw - 160px);
        --hdg-roadcw: 40px;
        --hdg-roadstep: 15px;
        --hdg-pilecw: 24px;
        --hdg-playcw: 48px;
        gap: 8px;
    }
    .hdg-middle { gap: 6px; width: 100%; }
    .hdg-center { flex: 1 1 0; padding: 6px; }
    .hdg-road { width: calc(var(--hdg-roadcw) + 8px); }
    .hdg-piles { width: calc(var(--hdg-pilecw) + 28px); }
    .hdg-roadtitle { font-size: 10px; white-space: normal; text-align: center; line-height: 1.1; height: 2.2em; }
    .hdg-claimed { min-width: 0; }
    .hdg-seat-total { font-size: 15px; margin-left: auto; }
    /* The point cards take a second row of their own here. */
    .hdg-seat-row { flex-wrap: wrap; row-gap: 3px; }
    .hdg-caps { order: 10; flex: 1 0 100%; }
    .hdg-seat-name { max-width: 7em; }
    .hdg-unseen { --hdg-ucw: 22px; }
    .hdg-controls { --hdg-playw: min(17em, 100% - 8.5em - 8px); }
    .hdg-note { min-height: 3.9em; }
    .status.hdg-status { min-height: 4.05em; }
}
@media (max-width: 400px) {
    .hdg-board {
        --hdg-playcw: 42px;
        --hdg-earliercw: 24px;
        --hdg-roadcw: 36px;
        --hdg-roadstep: 13px;
        --hdg-capcw: 20px;
    }
    .hdg-unseen { --hdg-ucw: 20px; }
    .hdg-unseen-grid { gap: 1px; }
}
/* =========================================================================
   Zaraba Twenty-Two (zaraba)
   A three-player trick taker where the leader plays two cards, one of them
   goes to a shared stock chart, and the chart prices every suit won.
     the stock chart above the table: one column per suit, what each card won
       of it is worth, its sum, and its cards (.zar-chart, .zar-chartcol)
     the table as a table: West and East either side of the trick, this player
       South, the trick a cross (.zar-board, .zar-trick, .pos-*)
     the other seats' hands and piles as counts, this player's pile as a count
       per suit (.zar-seat-stats, .zar-minewon)
     a finished trick held in the cross until the next lead (.zar-winning)
   ANTI-JITTER. Every chart column is reserved two rows of cards tall, every
   trick slot two cards wide, the seat panels one fixed height, the action box
   one button, and the status line two lines. The hand box is reserved by the
   shared runtime (handspace.ts).
   Shared ids are scoped through classes this game owns (.zar-status on the
   status line, .zar-handpanel on the hand panel).
   ========================================================================= */
/* --- the scoreboard ----------------------------------------------------- */
.progress.zar-scoreboard { min-height: 2.6em; align-items: center; }
/* --- suit glyphs in prose ---------------------------------------------- */
.zar-suited .red { color: var(--red); }
.zar-suited .red.diamond { color: var(--red-diamond); }
.zar-suited .club { color: var(--blue-club); }
/* --- the status line ---------------------------------------------------- */
.status.zar-status { min-height: 2.6em; }
.status.zar-status.zar-turn { color: var(--accent); font-weight: 700; }
.zar-hintnote { opacity: 0.85; font-weight: 400; }
/* --- the stock chart --------------------------------------------------- */
.zar-chart {
    --zar-chartcw: 30px;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 6px;
    max-width: 640px;
    margin: 0 auto 10px;
}
.zar-chartcol {
    box-sizing: border-box;
    min-width: 0;
    padding: 4px 6px 6px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.zar-charthead {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 6px;
    min-height: 20px;
    white-space: nowrap;
}
.zar-chartsuit { font-size: 18px; font-weight: 700; }
.zar-chartsuit.red { color: var(--red); }
.zar-chartsuit.red.diamond { color: var(--red-diamond); }
.zar-chartsuit.club { color: var(--blue-club); }
/* What each card won of the suit is worth: the number that scores. */
.zar-charteach {
    font-size: 14px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    color: #7fd18a;
}
.zar-chartcol.empty .zar-charteach { color: inherit; opacity: 0.6; font-weight: 600; }
.zar-chartcol.bust { border-color: #b0473c; }
.zar-chartcol.bust .zar-charteach { color: #ff8a7a; }
/* The sum, against the limit of 21. */
.zar-chartsum {
    font-size: 12px;
    opacity: 0.75;
    font-variant-numeric: tabular-nums;
}
.zar-chartcol.bust .zar-chartsum { text-decoration: line-through; opacity: 0.9; }
.zar-chartcards {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 3px;
    margin-top: 4px;
    min-height: calc(2 * var(--zar-chartcw) * 1.39 + 3px);
    align-content: flex-start;
}
.zar-chartcards .card { --cw: var(--zar-chartcw); }
/* --- the board: the table as a table ------------------------------------ */
.zar-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-areas:
        "left trick right"
        ".    down  .";
    align-items: center;
    gap: 10px 16px;
    margin-bottom: 6px;
}
.zar-table { display: contents; }
.zar-seat.pos-left { grid-area: left; justify-self: end; }
.zar-seat.pos-right { grid-area: right; justify-self: start; }
.zar-seat.pos-down { grid-area: down; justify-self: center; }
.zar-trick { grid-area: trick; justify-self: center; }
/* --- a seat's panel ------------------------------------------------------ */
.zar-seat {
    box-sizing: border-box;
    width: 150px;
    max-width: 100%;
    min-width: 0;
    padding: 6px 9px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.zar-seat.zar-active { outline-color: var(--accent); }
.zar-seat-head {
    display: flex;
    align-items: baseline;
    gap: 5px;
    margin-bottom: 4px;
    min-height: 18px;
}
.zar-toact { color: var(--accent); font-size: 11px; }
.zar-seat-name {
    flex: 1 1 auto;
    min-width: 0;
    font-weight: 700;
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.zar-seat-stats {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 26px;
    font-size: 13px;
    white-space: nowrap;
}
.zar-handcount { display: inline-flex; align-items: center; gap: 4px; }
.zar-handcount .card.zar-backicon { --cw: 17px; flex: none; }
.zar-statnum, .zar-woncount { font-variant-numeric: tabular-nums; font-weight: 700; }
.zar-woncount { opacity: 0.85; }
/* This round's points so far, for the viewer's own pile. */
.zar-pts {
    font-size: 13px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    color: #7fd18a;
}
.zar-pts.neg { color: #ff8a7a; }
/* --- the viewer's own panel, and the cards won of each suit ------------- */
.zar-seat.zar-mine { width: 236px; }
.zar-minewon {
    display: flex;
    justify-content: center;
    gap: 6px;
    min-height: 26px;
    align-items: center;
}
.zar-wonchip {
    display: inline-flex;
    align-items: baseline;
    gap: 3px;
    padding: 1px 7px;
    border-radius: 999px;
    border: 1px solid var(--panel-border);
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.zar-wonchip .red { color: var(--red); }
.zar-wonchip .red.diamond { color: var(--red-diamond); }
.zar-wonchip .club { color: var(--blue-club); }
.zar-wonchip.none { opacity: 0.45; }
.zar-wonchip.zero { opacity: 0.7; }
.zar-wonchip.bust { border-color: #b0473c; }
/* --- the trick: a cross ------------------------------------------------- */
.zar-trick {
    --zar-playcw: 56px;
    display: grid;
    grid-template-columns: auto auto;
    grid-template-areas:
        "left right"
        "down down";
    align-items: center;
    justify-items: center;
    gap: 6px 12px;
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.zar-play {
    display: flex;
    justify-content: center;
    gap: 4px;
    /* Two cards wide, whoever holds the slot: the leader's two lead cards
       never push the cross wider. */
    min-width: calc(2 * var(--zar-playcw) + 4px);
}
.zar-play.pos-left { grid-area: left; }
.zar-play.pos-right { grid-area: right; }
.zar-play.pos-down { grid-area: down; }
.zar-play .card { --cw: var(--zar-playcw); }
.zar-play .card.empty { border-style: dashed; opacity: 0.35; }
.zar-play.onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
.zar-play .card.zar-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
/* --- the action box ---------------------------------------------------- */
.zar-actions { display: block; height: 40px; margin-bottom: 10px; }
.zar-actionrow {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 8px;
    height: 40px;
}
/* --- the hand --------------------------------------------------------- */
/* One flex group per suit, a wider gap between groups than inside one. */
/* Rows stack from the top of the reserved box, so the first row stays put as
   the hand empties instead of the rows spreading out to fill it. */
.panel.zar-handpanel #hand-cards { column-gap: 16px; row-gap: 8px; align-content: flex-start; }
.zar-suitgroup { display: flex; flex-wrap: nowrap; gap: 6px; }
.zar-mute { opacity: 0.4; }
/* Picked to lead: marked IN PLACE (no transform, which the FLIP animator
   would read as a move). */
.card.zar-picked {
    outline: 3px dashed #ffd479;
    outline-offset: 2px;
    opacity: 0.6;
}
/* --- the summary ------------------------------------------------------ */
/* A real table, so every column lines up (ui-conventions.md). */
.zar-result {
    border-collapse: collapse;
    margin: 6px auto 0;
    font-size: 13px;
}
.zar-result th,
.zar-result td {
    padding: 3px 10px;
    border-bottom: 1px solid var(--panel-border);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.zar-result .zar-rlabel { text-align: left; font-size: 12px; }
.zar-result .zar-rlabel .red { color: var(--red); }
.zar-result .zar-rlabel .red.diamond { color: var(--red-diamond); }
.zar-result .zar-rlabel .club { color: var(--blue-club); }
.zar-result .zar-reach { opacity: 0.85; }
.zar-result .zar-reach.bust { color: #ff8a7a; opacity: 1; }
.zar-result .zar-rnum { text-align: right; }
.zar-result th.zar-rnum { font-size: 12px; }
.zar-result tr.zar-rpoints td { font-weight: 700; }
.zar-result tr.zar-rtotal td { font-size: 16px; font-weight: 800; }
.zar-result + .zar-result { margin-top: 10px; }
.zar-log-win { color: var(--accent); font-weight: 700; }
/* --- phones ----------------------------------------------------------- */
/* The board restacks: the other two seats in a row ACROSS THE TOP, still West
   then East from left to right, the cross under them, this player under that. */
@media (max-width: 720px) {
    /* Four columns a quarter of a phone wide hold two cards a row, so a fifth
       stock card of a suit added a row and walked the table down. Two by two,
       a column holds six in one row, reserved at that one row. */
    .zar-chart { --zar-chartcw: 26px; gap: 4px; grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .zar-chartcol { padding: 3px 4px 5px; }
    .zar-chartcards { min-height: calc(var(--zar-chartcw) * 1.39); margin-top: 3px; }
    .zar-charthead { gap: 5px; }
    .zar-chartsuit { font-size: 16px; }
    .zar-charteach { font-size: 13px; }
    /* The prompts run to two lines here and the score chips to two rows once a
       total grows: reserve both, in px (2.6em measured short of two lines). */
    .status.zar-status { min-height: 50px; }
    .progress.zar-scoreboard { min-height: 64px; align-items: flex-start; align-content: flex-start; }
    .zar-board {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        grid-template-areas:
            "left  right"
            "trick trick"
            "down  down";
        gap: 8px 6px;
        align-items: start;
    }
    .zar-seat.pos-left,
    .zar-seat.pos-right { width: auto; justify-self: stretch; padding: 5px 6px 6px; }
    .zar-trick { --zar-playcw: 44px; padding: 8px 10px; }
}
/* The narrowest phones: six stock cards of a suit still fit one row. */
@media (max-width: 380px) {
    .zar-chart { --zar-chartcw: 24px; }
    .zar-chartcards { gap: 2px; }
}
/* =========================================================================
   Chonkers
   Top to bottom inside .chon-board: the top seat's name line, a grid of 7
   columns (the top seat's bid, the table card, the bottom seat's bid), and
   the bottom seat's name line; then the hand panel. Every cell is painted in
   every phase (a card, a back, or an empty outline) and reserves its card's
   height in terms of the card width and the card aspect (89/64), so nothing
   above the hand moves as the game runs. Table cards are full art (the
   player bids on them); bids are small read-only cards on the simplified
   atlas (ui-conventions, "Small cards use the simplified sprite").
   ========================================================================= */

/* --- the status line ------------------------------------------------------ */
.status.chon-status { min-height: 1.45em; }
@media (max-width: 560px) {
    /* "Your turn. Tap a card, then the table card to bid it on." wraps on a
       phone: reserve two lines in every phase. */
    .status.chon-status { min-height: 2.9em; }
}

/* --- the board ------------------------------------------------------------ */
.chon-board {
    --chon-cw: 60px;
    --chon-bw: 46px;
    --chon-gap: 10px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    margin: 0 auto 10px;
    padding: 8px 14px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
    width: fit-content;
    max-width: 100%;
    box-sizing: border-box;
}
@media (max-width: 560px) {
    .chon-board {
        --chon-cw: clamp(36px, calc((100vw - 70px) / 7.4), 46px);
        --chon-bw: clamp(30px, calc(var(--chon-cw) - 8px), 36px);
        --chon-gap: 4px;
        padding: 8px 6px;
        gap: 6px;
    }
}
.chon-board .card { position: relative; }
.chon-board .card.chon-bid { --cw: var(--chon-bw); }
.chon-board .card.chon-tablecard { --cw: var(--chon-cw); }

/* --- the name lines ------------------------------------------------------- */
/* Every seat line is the height it has at the end, with its hand's name in a
   tag beside the player's name, so the tag appearing moves nothing below it. */
.chon-seat {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    height: 30px;
    white-space: nowrap;
}
.chon-name { font-weight: 700; }
/* Whose turn it is: a dot before the name (a shape, not a word). */
.chon-name.chon-turn::before {
    content: '';
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: 6px;
    border-radius: 50%;
    background: var(--accent);
    vertical-align: middle;
}
.chon-handname {
    font-weight: 600;
    line-height: 22px;
    padding: 0 10px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.1);
    white-space: nowrap;
}
.chon-handname.chon-winner { background: var(--accent); color: #1b1b1b; }
.chon-handname.chon-winner .red,
.chon-handname.chon-winner .club { color: inherit; }

/* --- the grid ------------------------------------------------------------- */
.chon-grid {
    display: grid;
    grid-template-columns: repeat(7, var(--chon-cw));
    column-gap: var(--chon-gap);
    justify-content: center;
}
.chon-col {
    display: grid;
    grid-template-rows: auto auto auto;
    justify-items: center;
    row-gap: 6px;
}
@media (max-width: 560px) {
    .chon-col { row-gap: 5px; }
}
.chon-cell {
    display: flex;
    align-items: center;
    justify-content: center;
}
.chon-celltop,
.chon-cellbottom { min-height: calc(var(--chon-bw) * 89 / 64); }
.chon-celltable { min-height: calc(var(--chon-cw) * 89 / 64); }

/* An open table card, while a hand card is picked: the cards it can go on. */
.chon-grid.chon-picking .card.chon-tablecard.chon-open { outline: 2px dashed var(--accent); outline-offset: 2px; }
.chon-board .card.chon-tablecard.sel { outline: 3px solid var(--accent); outline-offset: 2px; transform: translateY(-3px); }
/* The empty bid cell under an open table card, while a hand card is picked. */
.chon-board .card.empty.chon-target { border-color: var(--accent); background: rgba(255, 210, 90, 0.12); }

/* The other seat's newest bid since your last one. */
.chon-board .card.chon-bid.chon-last { box-shadow: 0 0 0 2px var(--accent); }
/* A bid that has just gone down (a back, so no card flies to it). */
.chon-board .card.chon-fresh { animation: chon-appear 360ms ease-out both; }
@keyframes chon-appear {
    from { opacity: 0; scale: 0.6; }
    to { opacity: 1; scale: 1; }
}

/* --- the end ------------------------------------------------------------- */
/* The winning bid of a column. */
.chon-board .card.chon-bid.chon-won { box-shadow: 0 0 0 3px var(--accent); }
/* A tied column: nobody takes the table card. */
.chon-board .card.chon-tablecard.chon-void { opacity: 0.45; }
.chon-board .card.chon-tablecard.chon-void::after {
    content: '';
    position: absolute;
    left: 8%;
    right: 8%;
    top: 50%;
    border-top: 3px solid rgba(20, 20, 20, 0.8);
    rotate: -35deg;
}
/* Which way a taken table card went: an arrow at the edge facing the taker. */
.chon-arrow {
    position: absolute;
    left: 50%;
    translate: -50% 0;
    padding: 0 5px;
    border-radius: 8px;
    background: var(--accent);
    color: #1b1b1b;
    font-size: 11px;
    line-height: 15px;
    pointer-events: none;
}
.chon-arrow.chon-arrowup { top: -8px; }
.chon-arrow.chon-arrowdown { bottom: -8px; }
/* The bottom seat's best five, on the board and in the hand. */
.chon-board .card.chon-tablecard.chon-best,
#hand-cards .card.chon-handcard.chon-best { box-shadow: 0 0 0 3px #7fd17f; }
#hand-cards .card.chon-handcard.chon-unused { opacity: 0.5; }

/* The reveal: each column turns over in turn, down the row (def.ts sets the
   delay inline; a negative one picks a reveal up mid-way after a repaint).
   `scale` and `opacity`, never `transform`, so a hover or a flight cannot
   cancel it. */
.chon-reveal { animation: chon-flip 420ms ease-out both; }
@keyframes chon-flip {
    from { opacity: 0; scale: 0.1 1; }
    to { opacity: 1; scale: 1 1; }
}

/* --- the result table ----------------------------------------------------- */
/* Inline (a tutorial, a review, or the summary set aside with View board) the
   status line above the board already says who won, so the banner shows only
   in the lifted dialog, which covers that line. */
#result-panel.chon-result:not(.result-overlay) .winner-banner { display: none; }
.chon-scores {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px 28px;
    margin: 2px auto 0;
}
.chon-handres {
    width: 290px;
    display: grid;
    grid-template-columns: 4.5em 1fr;
    grid-template-rows: auto auto;
    column-gap: 8px;
    row-gap: 2px;
    align-items: center;
    text-align: left;
}
.chon-resname {
    grid-row: 1 / 3;
    font-weight: 700;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.chon-handres.chon-winrow .chon-resname { color: var(--accent); }
.chon-bestcards { display: flex; gap: 3px; }
.chon-bestcards .card.chon-resultcard { --cw: 30px; }
.chon-bestname { font-size: 13px; opacity: 0.85; }
#result-panel.chon-result .result-actions { margin-top: 12px; }
@media (max-width: 560px) {
    .chon-handres { width: 250px; grid-template-columns: 3.8em 1fr; }
    .chon-bestcards .card.chon-resultcard { --cw: 26px; }
}

/* --- the log --------------------------------------------------------------- */
#log-entries .logline.chon-logover { font-weight: 700; }
/* =========================================================================
   Chickadee (chickadee)
   A shedding and climbing game for two. Beat the meld on the table with a
   higher one of the same kind and length, or a bomb, or pass; after every
   trick both players draw an available card from their own pyramid. The
   board is built around what a player reads every turn:
     the table as a table: the opponent North, the viewer South, each a panel
       with its name, rounds won, cards left, and Passed, beside its pyramid of
       four overlapping rows, the available cards ringed       (.chk-seat)
     the opponent's known cards, the ones it drew, in its panel (.chk-known)
     the middle: the play to beat in words and at full size, the trick's
       earlier plays small beside it, newest nearest            (.chk-center)
     a trick that just ended held, ringed and starred, through the draws until
       the next lead                                            (.chk-winning)
   On the viewer's own turn, a card no allowed play uses is faded (.chk-dim).
   The unseen cards, the pack as a grid: beside the opponent and the trick on
   a wide screen, under the controls on a phone (.chk-unseen).
   EVERY CARD SIZE is a custom property on .chk-board or the hand panel, never
   a rule on .card or a shared id, so each reservation follows it.
   THE PYRAMID'S CARDS are placed by --chk-col (half-card steps from the left)
   and --chk-row, which def.ts sets on each card, under the .chk-pyr rule. A
   flight's clone lands in <body>, out of that rule's reach, so it takes no
   position from it (ui-conventions.md, "A sideways card").
   ANTI-JITTER (ui-conventions.md, "Containers must not move"). The seat panels
   are one height in every state (the marks are switched by .chk-off, which
   keeps their space, and a pyramid keeps its box when it empties), the label
   is one line, the trick row one card high, the earlier plays one row that
   scrolls, the controls one row, the note one line (two on a phone), and the
   status line its tallest. Nothing on the board is ever `hidden` in a game.
   SHARED IDS are reached only through a class this game owns: the hand panel
   through .chk-handpanel, the status and progress lines through .chk-status
   and .chk-progress.
   ========================================================================= */

/* --- sizes ------------------------------------------------------------- */
.chk-board {
    --chk-oppcw: 36px;
    --chk-mycw: 46px;
    /* A row of a pyramid shows this share of the card above it. */
    --chk-step: 0.42;
    --chk-gap: 4px;
    --chk-playcw: 50px;
    --chk-earliercw: 30px;
    --chk-infow: 120px;
    --chk-knowncw: 24px;
    --chk-seatw: 380px;
    --chk-actionsw: 16em;
}

/* Switched on and off without moving anything: the space is kept. */
.chk-off { visibility: hidden; }

/* --- the board: the table as a table ------------------------------------ */
/* A wide screen: the opponent, the trick, and the viewer's seat in one column,
   the unseen cards and then the controls beside them. */
.chk-board {
    display: grid;
    grid-template-columns: auto auto;
    grid-template-areas: "opp unseen" "center unseen" "mine actions";
    justify-content: center;
    justify-items: center;
    align-items: center;
    gap: 6px 12px;
    margin-bottom: 6px;
}
.chk-board > .chk-opp { grid-area: opp; }
.chk-board > .chk-center { grid-area: center; }
.chk-board > .chk-mine { grid-area: mine; }
.chk-board > .chk-actions { grid-area: actions; width: var(--chk-actionsw); }
.chk-board > .chk-unseen { grid-area: unseen; }

/* --- a seat: its panel and its pyramid ----------------------------------- */
.chk-seat {
    box-sizing: border-box;
    width: var(--chk-seatw);
    max-width: 100%;
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 14px;
    padding: 5px 10px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.chk-seat.chk-active { outline-color: var(--accent); }
.chk-info {
    flex: none;
    width: var(--chk-infow);
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
}
.chk-seat-head {
    display: flex;
    align-items: baseline;
    gap: 6px;
    max-width: 100%;
    white-space: nowrap;
}
.chk-toact { flex: none; color: var(--accent); font-size: 11px; }
.chk-seat-name {
    min-width: 0;
    font-weight: 700;
    font-size: 14px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Rounds won: a filled ring for each, a hollow one for each still to win. */
.chk-pips {
    font-size: 15px;
    line-height: 1;
    letter-spacing: 3px;
    color: var(--accent);
    white-space: nowrap;
}
.chk-pipword {
    margin-right: 4px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0;
    color: var(--text, inherit);
    opacity: 0.85;
    vertical-align: 1px;
}
/* The count and Passed share a line. */
.chk-countline {
    display: flex;
    align-items: baseline;
    gap: 8px;
    white-space: nowrap;
}
.chk-count {
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.chk-said {
    font-size: 12px;
    font-style: italic;
    opacity: 0.85;
    white-space: nowrap;
}

/* The opponent's known cards: ONE row, one card high whether it holds any or
   none, its cards overlapping just enough to fit the panel's width
   (def.ts sets --chk-known-n, how many). */
.chk-known {
    display: flex;
    flex-wrap: nowrap;
    width: 100%;
    height: calc(var(--chk-knowncw) * 89 / 64);
}
.chk-known > .card.chk-knowncard { --cw: var(--chk-knowncw); flex: none; }
.chk-known > .card + .card {
    margin-left: min(2px, calc((100% - var(--chk-known-n) * var(--chk-knowncw)) / (var(--chk-known-n) - 1)));
    box-shadow: -1px 0 2px rgba(0, 0, 0, 0.45);
}

/* The pyramid: a fixed box four cards wide and four rows deep, whatever is left. */
.chk-opp .chk-pyr { --chk-pyrcw: var(--chk-oppcw); }
.chk-mine .chk-pyr { --chk-pyrcw: var(--chk-mycw); }
.chk-pyr {
    --cw: var(--chk-pyrcw);
    position: relative;
    flex: none;
    width: calc(4 * var(--cw) + 3 * var(--chk-gap));
    height: calc(var(--cw) * 89 / 64 * (1 + 3 * var(--chk-step)));
}
.chk-pyr > .card {
    position: absolute;
    left: calc(var(--chk-col) * (var(--cw) + var(--chk-gap)) / 2);
    top: calc(var(--chk-row) * var(--cw) * 89 / 64 * var(--chk-step));
    z-index: var(--chk-row);
}
.chk-pyr > .card { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); }
/* No lift on hover: a card that rose away from the pointer near its lower
   edge dropped back under it, again and again, and a tap there could miss.
   It brightens instead. */
.chk-pyr > .card.clickable:hover { transform: none; filter: brightness(1.12); }
/* An available card: a gold ring, so not color alone (its tooltip says it). */
.chk-pyr > .card.chk-avail {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}
.chk-pyr > .card.chk-avail.clickable { box-shadow: 0 0 10px rgba(255, 209, 102, 0.6); }
.chk-pyr > .card.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}

/* --- the middle: the trick ----------------------------------------------- */
.chk-center {
    box-sizing: border-box;
    width: var(--chk-seatw);
    max-width: 100%;
    padding: 4px 10px 6px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
/* Who played what, in words: ONE line reserved. */
.chk-label {
    height: 1.4em;
    line-height: 1.4;
    font-size: 14px;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.chk-label-who { font-weight: 700; }
/* The trick's row: the earlier plays, the play to beat in the middle, and a pad
   as wide as the earlier strip, so the play to beat stays centered. */
.chk-trickrow {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 10px;
    height: calc(var(--chk-playcw) * 89 / 64 + 10px);
}
.chk-trickpad { flex: 1 1 0; min-width: 0; }
/* The earlier plays, newest nearest the play to beat: a reversed row, so it
   starts at its right end and its overflow scrolls away to the left. */
.chk-earlier {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: row-reverse;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: center;
    gap: 6px;
    height: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}
.chk-earlier .chk-group { flex: none; display: flex; }
.chk-earlier .chk-group > .card + .card { margin-left: calc(-0.45 * var(--chk-earliercw)); }
.chk-earlier .card.chk-earliercard { --cw: var(--chk-earliercw); flex: none; }
.chk-top {
    flex: none;
    display: flex;
    justify-content: center;
    align-items: center;
}
.chk-top .chk-group {
    position: relative;
    display: flex;
    flex-wrap: nowrap;
    border-radius: 8px;
}
.chk-top .chk-group > .card + .card { margin-left: calc(-0.42 * var(--chk-playcw)); }
.chk-top .card.chk-topcard { --cw: var(--chk-playcw); flex: none; }
.chk-top .chk-group.chk-winning {
    outline: 3px solid var(--accent);
    outline-offset: 3px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}
.chk-star {
    position: absolute;
    top: -11px;
    right: -11px;
    width: 18px;
    height: 18px;
    line-height: 18px;
    border-radius: 50%;
    text-align: center;
    font-size: 12px;
    color: var(--accent-ink);
    background: var(--accent);
}

/* --- the action box ---------------------------------------------------- */
/* Beside the viewer's seat on a wide screen: Play over Pass, then the note. */
.chk-actions { box-sizing: border-box; }
.chk-controls {
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 6px;
    margin: 2px 0;
    /* Two .btn high, so the box is one height with buttons or none. */
    min-height: calc(4.8em + 6px);
}
.chk-controls .btn.chk-play,
.chk-controls .btn.chk-pass {
    flex: none;
    box-sizing: border-box;
    width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.chk-controls .btn.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
.chk-note {
    min-height: 3.9em;
    line-height: 1.3;
    font-size: 14px;
    text-align: center;
}

/* Suit glyphs spliced into the game's own text (def.ts appendSuitColored),
   the colors the game log and the card labels use. */
.chk-label .red,
.chk-note .red,
.chk-controls .red,
.status.chk-status .red { color: var(--red); }
.chk-label .red.diamond,
.chk-note .red.diamond,
.chk-controls .red.diamond,
.status.chk-status .red.diamond { color: var(--red-diamond); }
.chk-label .club,
.chk-note .club,
.chk-controls .club,
.status.chk-status .club { color: var(--blue-club); }
/* On the primary button's light fill: a dark red, and clubs in the button's own ink. */
.chk-controls .btn.primary .red { color: #b3261e; }
.chk-controls .btn.primary .red.diamond { color: #b4471a; }
.chk-controls .btn.primary .club { color: inherit; }

/* --- the hand ------------------------------------------------------------ */
/* One row on a wide screen: the tray is the container, and its cards are no
   wider than it allows for --chk-n of them (def.ts sets it: the hand's size,
   at least 12). */
.chk-handpanel { --chk-handcw: min(var(--cw), 64px); }
.chk-handpanel #hand-cards { container-type: inline-size; flex-wrap: nowrap; gap: 6px; }
.chk-handpanel #hand-cards .card {
    --cw: min(var(--chk-handcw), calc((100cqw - (var(--chk-n, 12) - 1) * 6px) / var(--chk-n, 12)));
    flex: none;
}
/* On the viewer's own turn: a card no allowed play uses (not a button), or,
   with a selection, one that cannot join it (still tappable). */
.chk-handpanel .card.chk-dim { opacity: 0.4; }
/* A card drawn since the viewer's last play: a soft gold edge and a dot at its
   top, so not color alone (the tooltip says "Just drawn"). */
.chk-handpanel .card.chk-new {
    position: relative;
    box-shadow: 0 0 0 2px rgba(255, 209, 102, 0.75), 0.05em 0.15em 0.35em rgba(0, 0, 0, 0.45);
}
.chk-handpanel .card.chk-new::after {
    content: '';
    position: absolute;
    top: -5px;
    left: 50%;
    width: 9px;
    height: 9px;
    margin-left: -4.5px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.55);
    pointer-events: none;
}

/* --- the unseen cards (def.ts renderUnseen) ----------------------------- */
/* A 4 x 13 grid of the pack: unseen cards face up in the simplified atlas
   (they are small), the rest as faint blanks. One size in every state. */
.chk-unseen {
    --chk-unseencw: 22px;
    box-sizing: border-box;
    padding: 5px 8px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
}
.chk-unseenhead {
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 4px;
    white-space: nowrap;
}
.chk-unseentitle { margin: 0; font-size: 13px; line-height: 16px; }
.chk-unseencount {
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    opacity: 0.85;
}
.chk-unseengrid {
    display: grid;
    grid-template-columns: 12px repeat(13, var(--chk-unseencw));
    gap: 2px;
    justify-content: center;
    align-items: center;
}
.chk-unseengrid .card { --cw: var(--chk-unseencw); }
.chk-unseenrank,
.chk-unseensuit {
    text-align: center;
    font-size: 10px;
    line-height: 12px;
    font-weight: 700;
    opacity: 0.8;
}
.chk-unseensuit { font-size: 12px; opacity: 1; }
.chk-unseensuit.red { color: var(--red); }
.chk-unseensuit.red.diamond { color: var(--red-diamond); }
.chk-unseensuit.club { color: var(--blue-club); }
.chk-unseengrid .card.chk-unseenknown {
    background: none;
    border: 1px dashed rgba(255, 255, 255, 0.18);
    box-shadow: none;
}

/* --- status and progress ----------------------------------------------- */
/* One line on a wide screen and two on a phone: the longest status ("Beat a
   run of 5 to the 10 with a higher one or a bomb, or pass.") fits them. */
.status.chk-status { line-height: 1.35; min-height: 1.35em; }
.status.chk-status.chk-turn { color: var(--accent); font-weight: 700; }
.progress.chk-progress { min-height: 1.4em; white-space: nowrap; }

/* --- the summary ------------------------------------------------------- */
.chk-result {
    border-collapse: collapse;
    margin: 0 auto 10px;
    font-size: 14px;
}
.chk-result th,
.chk-result td {
    padding: 4px 12px;
    border-bottom: 1px solid var(--panel-border);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.chk-result th { font-size: 12px; font-weight: 600; opacity: 0.85; }
.chk-result .chk-name { text-align: left; }
.chk-result .chk-num { text-align: right; }
.chk-result td.chk-best { color: var(--accent); font-weight: 700; }
.chk-result tr.chk-totalrow td { font-weight: 700; }

/* --- phones ------------------------------------------------------------ */
/* The seat panels stay one row, narrower; the hand wraps at a fixed size.
   Every reservation above is re-derived here. */
/* A narrow screen: one column, the controls in a row under the viewer's seat,
   then the unseen cards. */
@media (max-width: 720px) {
    .chk-board {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "opp" "center" "mine" "actions" "unseen";
        justify-items: stretch;
    }
    .chk-board > .chk-opp,
    .chk-board > .chk-center,
    .chk-board > .chk-mine,
    .chk-board > .chk-unseen { justify-self: center; }
    .chk-board > .chk-actions { width: auto; }
    .chk-controls { flex-direction: row; justify-content: center; min-height: 2.4em; }
    .chk-controls .btn.chk-play { width: min(15em, 100% - 7em - 8px); }
    .chk-controls .btn.chk-pass { width: 7em; }
    .chk-note { min-height: 2.6em; }
    .status.chk-status { min-height: 2.7em; }
}
@media (max-width: 560px) {
    .chk-board {
        --chk-oppcw: 32px;
        --chk-mycw: 40px;
        --chk-playcw: 44px;
        --chk-earliercw: 26px;
        --chk-infow: 96px;
        --chk-knowncw: 20px;
        --chk-seatw: 100%;
        gap: 5px;
    }
    /* Thirteen columns in a phone's width. */
    .chk-unseen { --chk-unseencw: 20px; padding: 4px 6px 5px; }
    .chk-seat { gap: 10px; padding: 4px 8px; }
    .chk-label { font-size: 13px; }
    /* Eight cards a row at 390px: 8 x 40px and 7 gaps of 2px fit the 336px tray. */
    .chk-handpanel #hand-cards { flex-wrap: wrap; gap: 4px 2px; }
    .chk-handpanel #hand-cards .card { --cw: 40px; }
}
/* 360px is best effort: a little tighter everywhere, so a 14-card hand still
   fits a 360x740 screen. */
@media (max-width: 380px) {
    .chk-board {
        --chk-oppcw: 30px;
        --chk-mycw: 38px;
        --chk-playcw: 40px;
        --chk-infow: 88px;
        gap: 3px;
    }
    .chk-unseen { --chk-unseencw: 18px; }
    .chk-seat { padding: 3px 6px; }
}
/* =========================================================================
   Filch
   .flc-main is a size container; .flc-layout inside it puts the hand panel
   BESIDE the board when the column is 760px wide or more, and below it
   otherwise. The card width (--flc-cw) is the largest that fits both the
   column's width and the viewport's height, up to 80px, so everything the
   player plays with stays on screen (ui-conventions).
   Top to bottom inside .flc-board: the top seat's name line, a grid of 5
   region columns and a discard column (rows: the top seat's stack, its
   total, THE LINE, the bottom seat's total, the bottom seat's stack, the
   region's name), and the bottom seat's name line; then the hand panel,
   whose tray carries the Discard button beside a card that has no other use.
   Every cell is placed explicitly (.flc-c<column> .flc-r<row>), so a decided
   region's tint can span its column behind the cells. Every stack is painted
   in every phase and reserves the height of 4 cards (a region holds 5 at
   most, both sides together; a side with all 5 tightens its overlap to fit
   the same box), so nothing above the hand moves as the game runs. Both
   stacks splay away from the line.
   Cards in a stack are placed with top/bottom offsets, never `transform`,
   so a hover lift or a flight cannot move them out of place.
   ========================================================================= */

/* --- the status line ------------------------------------------------------ */
.status.flc-status { min-height: 1.45em; }
@media (max-width: 700px) {
    /* "Tap a spot on your side to play the 4, or a lit card of Bot’s to filch
       it." wraps on a phone: reserve two lines in every phase. */
    .status.flc-status { min-height: 2.9em; }
}
@media (max-width: 380px) {
    /* At 360px the longest lines (a last action that can play or filch, or
       play or move) take three. */
    .status.flc-status { min-height: 4.35em; }
}

/* --- the board ------------------------------------------------------------ */
.flc-main { container-type: inline-size; }
.flc-layout { display: block; }
@container (min-width: 760px) {
    .flc-layout {
        display: grid;
        grid-template-columns: auto var(--flc-handw, 200px);
        column-gap: 16px;
        justify-content: center;
        align-items: start;
    }
    /* Height left for the stacks: the viewport minus the header, the status
       line, the board's own rows, and a margin. The hand is beside. */
    .flc-layout > .flc-board { --flc-vrest: 284px; --flc-side: 216px; }
    /* A fixed column, so hiding the hand at the end moves nothing. */
    .flc-layout > .panel.flc-handpanel { width: var(--flc-handw, 200px); box-sizing: border-box; margin: 0; }
}
.flc-board {
    --flc-pad: 14px;
    --flc-gap: 12px;
    /* The discard piles: small read-only cards, on the simplified atlas. */
    --flc-dw: 34px;
    --flc-pmax: 17px;
    /* Column mode: the header, the status line, the board's own rows, and the hand panel below. */
    --flc-vrest: 460px;
    --flc-side: 0px;
    /* Two stacks of 4 splayed cards are 4.94 card widths tall. */
    --flc-cw: clamp(40px, min(
        calc((100cqw - var(--flc-side) - 2 * var(--flc-pad) - 2px - var(--flc-dw) - 5 * var(--flc-gap)) / 5),
        calc((100svh - var(--flc-vrest)) / 4.94)), 80px);
    --flc-ch: calc(var(--flc-cw) * 89 / 64);
    /* How much of a covered card shows above the card on top of it: its index. */
    --flc-off: calc(var(--flc-cw) * 0.36);
    --flc-gap: 12px;
    --flc-stackh: calc(var(--flc-ch) + 3 * var(--flc-off));
    --flc-dh: calc(var(--flc-dw) * 89 / 64);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    margin: 0 auto 10px;
    padding: 8px var(--flc-pad);
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
    border: 1px solid var(--panel-border);
    width: fit-content;
    max-width: 100%;
    box-sizing: border-box;
}
@media (max-width: 560px) {
    .flc-board {
        --flc-pad: 6px;
        --flc-dw: 28px;
        --flc-pmax: 15px;
        --flc-gap: 6px;
        padding: 6px var(--flc-pad);
    }
}

/* --- the name lines ------------------------------------------------------- */
.flc-seat {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    min-height: 24px;
    white-space: nowrap;
    font-size: 14px;
}
.flc-name { font-weight: 700; }
/* Whose turn it is: a dot before the name (a shape, not a word). */
.flc-name.flc-turn::before {
    content: '';
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: 6px;
    border-radius: 50%;
    background: var(--accent);
    vertical-align: middle;
}
.flc-name.flc-winner { color: var(--accent); }
.flc-tocome {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-size: 12px;
    opacity: 0.85;
}
.flc-tocome-label { margin-right: 3px; }
.flc-rank {
    min-width: 13px;
    padding: 0 2px;
    border-radius: 3px;
    background: rgba(255, 255, 255, 0.1);
    text-align: center;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}
.flc-rank.flc-rank-jk { padding: 0 3px; }
.flc-won { font-weight: 600; opacity: 0.9; }

/* --- the grid ------------------------------------------------------------- */
.flc-grid {
    display: grid;
    grid-template-columns: repeat(5, var(--flc-cw)) var(--flc-dw);
    column-gap: var(--flc-gap);
    justify-content: center;
    align-items: center;
}
.flc-c1 { grid-column: 1; }
.flc-c2 { grid-column: 2; }
.flc-c3 { grid-column: 3; }
.flc-c4 { grid-column: 4; }
.flc-c5 { grid-column: 5; }
.flc-c6 { grid-column: 6; }
.flc-r1 { grid-row: 1; }
.flc-r2 { grid-row: 2; }
.flc-r3 { grid-row: 3; }
.flc-r4 { grid-row: 4; }
.flc-r5 { grid-row: 5; }
.flc-r6 { grid-row: 6; }
/* A decided region: its winner's tint behind the whole column. */
.flc-settle {
    grid-row: 1 / 7;
    align-self: stretch;
    margin: -4px -5px;
    border-radius: 8px;
    pointer-events: none;
}
.flc-settle.flc-settle-bottom { background: rgba(255, 210, 90, 0.16); box-shadow: inset 0 0 0 1px rgba(255, 210, 90, 0.45); }
.flc-settle.flc-settle-top { background: rgba(242, 165, 155, 0.16); box-shadow: inset 0 0 0 1px rgba(242, 165, 155, 0.45); }
.flc-stack {
    position: relative;
    width: var(--flc-cw);
    height: var(--flc-stackh);
    /* The overlap: the reserved one, tighter when a side holds all 5 cards. */
    --flc-step: min(var(--flc-off), calc((var(--flc-stackh) - var(--flc-ch)) / max(1, var(--flc-n, 1) - 1)));
}
.flc-stack > .card,
.flc-stack > .flc-spot {
    position: absolute;
    left: 0;
    margin: 0;
    --cw: var(--flc-cw);
    width: var(--flc-cw);
    height: var(--flc-ch);
}
/* The top seat's stack sits on the line and splays up the screen, the newest
   card (the one that can be filched) furthest from the line. */
.flc-stacktop > .flc-at-0 { bottom: 0; }
.flc-stacktop > .flc-at-1 { bottom: calc(var(--flc-step) * 1); }
.flc-stacktop > .flc-at-2 { bottom: calc(var(--flc-step) * 2); }
.flc-stacktop > .flc-at-3 { bottom: calc(var(--flc-step) * 3); }
.flc-stacktop > .flc-at-4 { bottom: calc(var(--flc-step) * 4); }
/* The bottom seat's stack hangs from the line and grows down the screen. */
.flc-stackbottom > .flc-at-0 { top: 0; }
.flc-stackbottom > .flc-at-1 { top: calc(var(--flc-step) * 1); }
.flc-stackbottom > .flc-at-2 { top: calc(var(--flc-step) * 2); }
.flc-stackbottom > .flc-at-3 { top: calc(var(--flc-step) * 3); }
.flc-stackbottom > .flc-at-4 { top: calc(var(--flc-step) * 4); }
/* Newer cards lie on older ones. */
.flc-stack > .flc-z-1,
.flc-pile > .flc-z-1 { z-index: 1; }
.flc-stack > .flc-z-2,
.flc-pile > .flc-z-2 { z-index: 2; }
.flc-stack > .flc-z-3,
.flc-pile > .flc-z-3 { z-index: 3; }
.flc-stack > .flc-z-4,
.flc-pile > .flc-z-4 { z-index: 4; }
.flc-pile > .flc-z-5 { z-index: 5; }
.flc-pile > .flc-z-6 { z-index: 6; }
.flc-pile > .flc-z-7 { z-index: 7; }
.flc-pile > .flc-z-8 { z-index: 8; }
.flc-pile > .flc-z-9 { z-index: 9; }
.flc-stack > .flc-spot { z-index: 5; }
/* Beneath the cards: a ghost never hides the card now on top, and a spot
   never hides a card the Ace could move. Only the strip past the top card shows. */
.flc-stack { isolation: isolate; }
.flc-stack > .flc-spot.flc-ghost,
.flc-stack > .flc-spot.flc-under { z-index: -1; }
/* An empty spot: a lit place to play or put a filched card, or a region not yet opened. */
.flc-spot {
    box-sizing: border-box;
    border-radius: 6px;
    border: 2px dashed rgba(255, 255, 255, 0.18);
    background: transparent;
    padding: 0;
}
.flc-spot.flc-notyet { border-color: rgba(255, 255, 255, 0.12); }
.flc-spot.flc-target {
    border-color: var(--accent);
    background: rgba(255, 210, 90, 0.14);
    cursor: pointer;
}
.flc-spot.flc-target:hover,
.flc-spot.flc-target:focus-visible { background: rgba(255, 210, 90, 0.3); }
.flc-spot.hinted { box-shadow: 0 0 0 3px var(--accent); }
/* A card of the other seat's that the picked card can filch. */
.flc-board .card.flc-steal { outline: 2px dashed var(--accent); outline-offset: 2px; }
.flc-board .card.flc-steal.sel { outline: 3px solid var(--accent); }
/* A filched card: it scores for its new side and can never be filched again. */
.flc-board .card.flc-filched { box-shadow: 0 0 0 2px #39b4cf; }
/* A card the other seat has just filched, until the viewed seat acts again. */
.flc-board .card.flc-justtaken {
    box-shadow: 0 0 0 3px #39b4cf, 0 0 12px 3px rgba(57, 180, 207, 0.85);
    animation: flc-taken 1.2s ease-out 2;
}
@keyframes flc-taken {
    0% { box-shadow: 0 0 0 3px #39b4cf, 0 0 22px 8px rgba(57, 180, 207, 1); }
    100% { box-shadow: 0 0 0 3px #39b4cf, 0 0 12px 3px rgba(57, 180, 207, 0.85); }
}
/* The Ace-move variant: a top card the picked Ace can move, and one the other seat just moved. */
.flc-board .card.flc-lift { outline: 2px dashed #c9a3ff; outline-offset: 2px; }
.flc-board .card.flc-lift.sel { outline: 3px solid #c9a3ff; }
.flc-board .card.flc-justmoved { box-shadow: 0 0 0 3px #b48cf2, 0 0 12px 3px rgba(180, 140, 242, 0.85); }
/* Where it was taken from: its outline and its rank. */
.flc-spot.flc-ghost {
    border-color: #39b4cf;
    background: rgba(14, 50, 58, 0.92);
    display: flex;
    justify-content: center;
    /* The rank sits in the strip that shows past the card on top: the far end from the line. */
    align-items: flex-end;
    padding: 0 0 2px;
}
.flc-stacktop > .flc-spot.flc-ghost { align-items: flex-start; padding: 2px 0 0; }
/* With nothing under it the whole outline shows, so the rank goes in the middle. */
.flc-stack > .flc-spot.flc-ghost.flc-at-0 { align-items: center; padding: 0; }
.flc-ghostrank { font-size: 18px; line-height: 1; font-weight: 700; color: #8fe0f0; }
.flc-spot.flc-ghost.flc-ghost-shift { border-color: #b48cf2; background: rgba(40, 26, 62, 0.92); }
.flc-ghost-shift .flc-ghostrank { color: #d6c0ff; }
/* What a Joker is worth right now, on the strip a covered Joker still shows:
   the top right below the line, the bottom right above it. */
.flc-jokerval {
    position: absolute;
    top: 3px;
    right: 3px;
    min-width: 18px;
    height: 18px;
    padding: 0 3px;
    box-sizing: border-box;
    border-radius: 9px;
    background: #1d1d1d;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    font-variant-numeric: tabular-nums;
    pointer-events: none;
}

.flc-stacktop .flc-jokerval { top: auto; bottom: 3px; }
/* On a lit spot: what the picked Joker would be worth there, centered. */
.flc-spot .flc-jokerval.flc-spotval { top: 50%; right: auto; left: 50%; translate: -50% -50%; }

/* --- the discard piles ------------------------------------------------------ */
.flc-pile {
    position: relative;
    width: var(--flc-dw);
    height: var(--flc-stackh);
    /* Both piles splay DOWN, so each covered card shows its rank (the
       simplified art prints it at the top). */
    --flc-pstep: min(var(--flc-pmax), calc((var(--flc-stackh) - var(--flc-dh)) / max(1, var(--flc-n, 1) - 1)));
}
.flc-pile > .card {
    position: absolute;
    left: 0;
    margin: 0;
    --cw: var(--flc-dw);
    width: var(--flc-dw);
    height: var(--flc-dh);
}
.flc-pile > .flc-at-0 { top: 0; }
.flc-pile > .flc-at-1 { top: calc(var(--flc-pstep) * 1); }
.flc-pile > .flc-at-2 { top: calc(var(--flc-pstep) * 2); }
.flc-pile > .flc-at-3 { top: calc(var(--flc-pstep) * 3); }
.flc-pile > .flc-at-4 { top: calc(var(--flc-pstep) * 4); }
.flc-pile > .flc-at-5 { top: calc(var(--flc-pstep) * 5); }
.flc-pile > .flc-at-6 { top: calc(var(--flc-pstep) * 6); }
.flc-pile > .flc-at-7 { top: calc(var(--flc-pstep) * 7); }
.flc-pile > .flc-at-8 { top: calc(var(--flc-pstep) * 8); }
.flc-pile > .flc-at-9 { top: calc(var(--flc-pstep) * 9); }
.flc-pile > .flc-at-10 { top: calc(var(--flc-pstep) * 10); }
/* A card the other seat has just discarded (to filch, or with no use). */
.flc-board .card.flc-justdiscarded { box-shadow: 0 0 0 2px #39b4cf, 0 0 8px 2px rgba(57, 180, 207, 0.7); }

/* --- the totals, the line, and the region names --------------------------- */
.flc-total {
    min-height: 18px;
    font-size: 13px;
    font-weight: 600;
    text-align: center;
    font-variant-numeric: tabular-nums;
    opacity: 0.8;
}
.flc-total.flc-leads { color: var(--accent); opacity: 1; }
.flc-line {
    grid-column: 1 / -1;
    height: 2px;
    margin: 1px 0;
    border-radius: 1px;
    background: rgba(255, 255, 255, 0.55);
}
.flc-label {
    margin-top: 3px;
    min-height: 16px;
    font-size: 12px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    text-align: center;
    white-space: nowrap;
    opacity: 0.7;
}
.flc-label.flc-label-notyet { opacity: 0.35; }
.flc-label.flc-won-bottom,
.flc-label.flc-won-top { opacity: 1; font-weight: 700; }
.flc-label.flc-won-bottom { color: var(--accent); }
/* The top seat's regions in a color of their own, so the two never read alike. */
.flc-label.flc-won-top { color: #f2a59b; }
.flc-label.flc-byhigh::after { content: '*'; }
/* Decided while the game runs: a check before the name. */
.flc-label.flc-label-settled::before { content: '\2713\00a0'; }


/* --- the hand and the Discard row ----------------------------------------- */
.flc-handnote {
    align-self: center;
    font-size: 13px;
    opacity: 0.7;
}
.flc-handpanel #hand-cards .btn.flc-discard { align-self: center; margin-left: 10px; }

/* --- the result table ----------------------------------------------------- */
.flc-scores {
    border-collapse: collapse;
    margin: 6px auto 10px;
}
.flc-scores th,
.flc-scores td { padding: 4px 9px; vertical-align: middle; }
.flc-scores .flc-textcol { text-align: left; }
.flc-scores .flc-numcol { text-align: right; font-variant-numeric: tabular-nums; }
.flc-scores thead th { font-size: 13px; opacity: 0.75; font-weight: 600; }
.flc-scores thead .flc-spancol { text-align: center; padding-bottom: 0; }
.flc-scores tbody tr + tr { border-top: 1px solid rgba(255, 255, 255, 0.12); }
.flc-scores .flc-winrow th { color: var(--accent); }
.flc-scores .flc-cellwon { font-weight: 700; color: var(--accent); }
.flc-scores .flc-woncol { font-weight: 700; }
.flc-resultnote { font-size: 13px; opacity: 0.8; margin: 0 0 8px; text-align: center; }
@media (max-width: 560px) {
    .flc-scores th,
    .flc-scores td { padding: 4px 6px; }
    .flc-scores thead th { font-size: 11px; }
}

/* --- the log --------------------------------------------------------------- */
#log-entries .logline.flc-logover { font-weight: 700; }
/* =========================================================================
   Briscola Chiamata (briscolachiamata)
   A five-player trick taker: bid a rank, call a card, and the card's holder
   is your secret partner.
     the table as a table: the other four seats round the trick clockwise from
       the viewer's left, this player South, and the trick a cross with each
       card on its seat's side              (.bch-board, .bch-trick, .pos-*)
     one center cell holding two layers, the trick and the bid or call picker,
       so the cell is the larger of the two all match long  (.bch-center, .bch-off)
     a three-line panel per seat: name, total and dealer chip, the bid or the
       role and card points                 (.bch-seat)
     the contract box: the high bid, then the called card and the partner
                                            (.bch-deal)
     the unseen cards: a grid of suits by rank whose head is the ranks and
       their points                         (.bch-unseen)
   CONTAINERS MUST NOT MOVE. Every track of the desktop board is fixed or a
   fixed share of a fixed width, the center is a fixed width, every panel is
   three lines, the picker is three fixed rows, and the contract box is a fixed
   size. #hand-cards gets no min-height: src/shared/ui/handspace.ts reserves it.
   SHARED IDS only through a class this game owns: .bch-handpanel on the hand
   panel, .bch-status and .bch-progress added by the renderers, .bch-summary on
   the result panel.
   ========================================================================= */

.bch-vh {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* --- status and progress ------------------------------------------------ */
.status.bch-status { min-height: 2.6em; }
.status.bch-status.bch-turn { color: var(--accent); }
.bch-statusmain { display: block; }
.bch-hintnote { display: block; font-size: 14px; font-weight: 400; opacity: 0.85; }
.progress.bch-progress { min-height: 20px; white-space: nowrap; }
.bch-suited .red { color: var(--red); }
.bch-suited .red.diamond { color: var(--red-diamond); }
.bch-suited .club { color: var(--blue-club); }

/* --- the board ----------------------------------------------------------- */
/* ONE GRID. #bch-table and .bch-foot are `display: contents`, so the panels,
   the contract box, and the aids are cells of it.
     trick  = 3 * 56px + 2 * 6px = 180px
     picker = 5 * --bch-segw + 4 * --bch-seggap = 5 * 38 + 4 * 4 = 206px
     --bch-centerw = 206px + 4px padding + 6px slack = 216px */
.bch-board {
    --bch-playcw: 56px;
    --bch-trickgap: 6px;
    --bch-segw: 38px;
    --bch-segh: 32px;
    --bch-seggap: 4px;
    --bch-placeh: 34px;
    --bch-notefs: 12px;
    --bch-centerw: 216px;
    --bch-namefs: 13px;
    --bch-totalfs: 15px;
    --bch-linefs: 11px;
    --bch-pad: 6px 9px 7px;
    --bch-calledcw: 40px;
    --bch-dealfs: 12px;
    --bch-dealw: 176px;
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--bch-centerw) minmax(0, 1fr);
    grid-template-areas: "nw center ne" "w center e" "deal mine aids";
    column-gap: 12px;
    row-gap: 8px;
    align-items: center;
    margin-bottom: 10px;
}
.bch-table,
.bch-foot { display: contents; }
.bch-seat.pos-w { grid-area: w; justify-self: end; }
.bch-seat.pos-nw { grid-area: nw; justify-self: end; }
.bch-seat.pos-e { grid-area: e; justify-self: start; }
.bch-seat.pos-ne { grid-area: ne; justify-self: start; }
.bch-seat.bch-mine { grid-area: mine; justify-self: center; }
.bch-deal { grid-area: deal; justify-self: end; }
.bch-aids { grid-area: aids; justify-self: start; align-self: start; }

/* --- the center: two layers in one cell ---------------------------------- */
.bch-center {
    grid-area: center;
    display: grid;
    box-sizing: border-box;
    width: var(--bch-centerw);
    justify-self: center;
    justify-items: center;
    align-items: center;
}
.bch-center > * { grid-area: 1 / 1; }
.bch-center > .bch-off { visibility: hidden; }

/* --- the trick: a cross --------------------------------------------------- */
.bch-trick {
    display: grid;
    grid-template-columns: repeat(3, var(--bch-playcw));
    grid-template-areas: "nw . ne" "w . e" ". s .";
    gap: var(--bch-trickgap);
    justify-content: center;
    justify-items: center;
    align-items: center;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
.bch-play.pos-s { grid-area: s; }
.bch-play.pos-w { grid-area: w; }
.bch-play.pos-e { grid-area: e; }
.bch-play.pos-nw { grid-area: nw; }
.bch-play.pos-ne { grid-area: ne; }
.bch-play .card { --cw: var(--bch-playcw); }
/* The trick's card points, in the cross's middle cell. */
.bch-trickpts {
    grid-area: 2 / 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    line-height: 1.1;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.bch-trickpts-num { font-size: 20px; font-weight: 800; }
.bch-trickpts-unit { font-size: 11px; opacity: 0.75; }
.bch-play .card.empty { opacity: 0.35; }
.bch-play.onturn .card.empty { border-color: var(--accent); opacity: 0.9; }
.bch-play .card.bch-winning {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}

/* --- the pickers ----------------------------------------------------------- */
/* Three fixed rows, whatever they hold: two lines of choices, the confirm row,
   and two lines of note. The same height for ranks, the points stepper, the
   call options, or nothing. */
.bch-picker {
    display: grid;
    grid-template-rows:
        calc(2 * var(--bch-segh) + var(--bch-seggap))
        var(--bch-placeh)
        calc(var(--bch-notefs) * 1.3 * 2);
    row-gap: 6px;
    justify-self: stretch;
    box-sizing: border-box;
    width: 100%;
    padding: 0 2px;
}
.bch-choices {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-content: flex-start;
    gap: var(--bch-seggap);
    overflow: hidden;
    /* The clip box reaches 6px past the choices, so a tutorial's .coach-mark
       ring (3px outline, 2px offset) on an edge choice is drawn whole. The
       negative margin hands the padding back, so nothing moves. */
    padding: 6px;
    margin: -6px;
}
.bch-picker .seg.bch-rank,
.bch-picker .seg.bch-callopt,
.bch-picker .seg.bch-step {
    flex: 0 0 var(--bch-segw);
    width: var(--bch-segw);
    height: var(--bch-segh);
    min-height: 0;
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font: inherit;
    font-size: 14px;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
    background: transparent;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    cursor: pointer;
    white-space: nowrap;
}
.bch-picker .seg.bch-callopt { flex-basis: calc(var(--bch-segw) + 8px); width: calc(var(--bch-segw) + 8px); }
.bch-picker .seg.bch-callopt.bch-own { border-style: dashed; }
.bch-picker .seg.selected {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-ink);
    font-weight: 700;
}
.bch-picker .seg.selected .red,
.bch-picker .seg.selected .club { color: var(--accent-ink); }
.bch-picker .seg:disabled { opacity: 0.3; cursor: default; }
.bch-picker .seg.bch-rank:disabled { text-decoration: line-through; }
.bch-stepper {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--bch-seggap);
    height: var(--bch-segh);
}
.bch-stepvalue {
    min-width: 2.4em;
    text-align: center;
    font-size: 18px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}
.bch-confirmrow { display: flex; justify-content: center; align-items: center; gap: 8px; }
.bch-picker .btn.bch-place,
.bch-picker .btn.bch-pass {
    box-sizing: border-box;
    height: var(--bch-placeh);
    padding: 0 14px;
    white-space: nowrap;
}
.bch-pickernote {
    font-size: var(--bch-notefs);
    line-height: 1.3;
    text-align: center;
    overflow: hidden;
    opacity: 0.9;
}

/* --- a seat's panel: three lines at every size ---------------------------- */
.bch-seat {
    position: relative;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 2px;
    width: 176px;
    max-width: 100%;
    min-width: 0;
    padding: var(--bch-pad);
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
}
.bch-seat.bch-active { outline-color: var(--accent); }
.bch-seat-head { line-height: 1.3; min-height: calc(var(--bch-namefs) * 1.3); font-size: var(--bch-namefs); }
.bch-seat-score { line-height: 1.2; min-height: calc(var(--bch-totalfs) * 1.2); font-size: var(--bch-totalfs); }
.bch-seat-line { min-height: calc(var(--bch-linefs) * 1.6 + 2px); font-size: var(--bch-linefs); }
.bch-seat-head,
.bch-seat-score,
.bch-seat-line {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
}
.bch-seat-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-weight: 700; }
.bch-name-short { display: none; }
.bch-toact { visibility: hidden; color: var(--accent); }
.bch-seat.bch-active .bch-toact { visibility: visible; }
.bch-seat-total { font-weight: 800; font-variant-numeric: tabular-nums; }
.bch-dealer {
    visibility: hidden;
    font-size: calc(var(--bch-totalfs) * 0.7);
    font-weight: 700;
    line-height: 1.2;
    padding: 0 3px;
    border: 1px solid var(--accent);
    border-radius: 4px;
    color: var(--accent);
}
.bch-seat.bch-isdealer .bch-dealer { visibility: visible; }
/* The bid or role tag: a shape as well as a color. */
.bch-tag {
    line-height: 1.6;
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    padding: 0 5px;
    font-variant-numeric: tabular-nums;
}
.bch-tag-wait { border-style: dashed; opacity: 0.75; }
.bch-tag-passed { opacity: 0.6; text-decoration: line-through; }
.bch-tag-bid { border-color: rgba(255, 255, 255, 0.45); }
.bch-tag-bidding { border-color: var(--accent); color: var(--accent); }
.bch-tag-bidder,
.bch-tag-alone { background: rgba(255, 209, 102, 0.22); border-color: var(--accent); font-weight: 700; }
.bch-tag-partner { background: rgba(255, 209, 102, 0.12); border-color: var(--accent); border-style: dashed; font-weight: 700; }
.bch-pts { font-variant-numeric: tabular-nums; opacity: 0.9; }

/* --- the contract box ----------------------------------------------------- */
/* A fixed size: a label line, the called card, and two lines. Every line is
   nowrap and ellipsized, so a long name cannot grow the box. */
.bch-deal {
    position: relative;
    box-sizing: border-box;
    width: var(--bch-dealw);
    height: calc(var(--bch-dealfs) * 1.3 * 3 + var(--bch-calledcw) * 1.39 + 18px);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    gap: 2px;
    padding: 4px 6px;
    font-size: var(--bch-dealfs);
    line-height: 1.3;
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.16);
    overflow: hidden;
}
.bch-deal-line {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-align: center;
    min-height: calc(var(--bch-dealfs) * 1.3);
}
.bch-deal-label { opacity: 0.75; }
.bch-deal-main { font-weight: 700; }
/* The partners' card points against the bid, once the partners are known. */
.bch-deal-score { font-size: calc(var(--bch-dealfs) * 1.15); font-weight: 800; font-variant-numeric: tabular-nums; }
.bch-deal-score.bch-made { color: #8fd6a4; }
.bch-calledslot { display: flex; justify-content: center; }
.bch-deal .card.bch-calledcard { --cw: var(--bch-calledcw); }
.bch-deal .card.empty.bch-calledcard { opacity: 0.3; }

/* --- the hand ------------------------------------------------------------ */
.bch-handpanel .bch-handcap { font-weight: 400; opacity: 0.85; }
/* The called card in its holder's own hand: marked, so the partner sees it. */
.bch-handpanel .card.bch-iscalled { box-shadow: 0 0 0 2px var(--accent), 0 0 10px rgba(255, 209, 102, 0.5); }

/* --- the unseen cards ------------------------------------------------------ */
/* A real table of suits by rank, so every column lines up. Its outer size is
   the same all deal long: every cell is a fixed width, seen or not. */
.bch-unseen {
    box-sizing: border-box;
    margin: 0;
    padding: 6px 8px;
    font-size: 12px;
}
.bch-unseenhead {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: 20px;
    margin-bottom: 2px;
}
.bch-unseentitle { margin: 0; font-size: 13px; white-space: nowrap; }
.bch-unseenclose { position: absolute; right: 0; }
.bch-unseengrid { border-collapse: collapse; margin: 0 auto; }
.bch-unseengrid th,
.bch-unseengrid td {
    width: 18px;
    padding: 0 1px;
    line-height: 1.35;
    text-align: center;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.bch-unseengrid .bch-ug-label { width: auto; padding-right: 5px; text-align: left; font-weight: 400; opacity: 0.8; }
.bch-unseengrid thead th { font-weight: 700; }
.bch-unseengrid .bch-ug-pts td { font-size: 10px; opacity: 0.7; border-bottom: 1px solid var(--panel-border); }
.bch-unseengrid .bch-ug-seen { opacity: 0.3; }
.bch-unseengrid .bch-ug-out { font-weight: 700; }
.bch-unseengrid .red { color: var(--red); }
.bch-unseengrid .red.diamond { color: var(--red-diamond); }
.bch-unseengrid .club { color: var(--blue-club); }
.bch-unseengrid tr.bch-trumprow { background: rgba(255, 209, 102, 0.12); }
.bch-unseenfoot { margin-top: 3px; text-align: center; font-size: 11px; opacity: 0.8; white-space: nowrap; }
.bch-unseenbtn { white-space: nowrap; }

/* --- the reference tables (Rules summary and the Reference dialog) -------- */
.bch-ranktable,
.bch-scoretable { border-collapse: collapse; margin: 6px auto 10px; }
.bch-ranktable th,
.bch-ranktable td,
.bch-scoretable th,
.bch-scoretable td {
    text-align: left;
    padding: 3px 8px;
    border-bottom: 1px solid var(--panel-border);
    font-size: 13px;
    white-space: nowrap;
}
.bch-scoretable th,
.bch-scoretable td { padding: 3px 6px; }
.bch-ranktable .bch-refnum,
.bch-scoretable .bch-refnum { text-align: right; font-variant-numeric: tabular-nums; }

/* --- the summary ------------------------------------------------------ */
.bch-dealnote { display: block; }
.bch-result { border-collapse: collapse; margin: 6px auto 0; font-size: 14px; }
.bch-result th,
.bch-result td {
    padding: 3px 8px;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    border-bottom: 1px solid var(--panel-border);
}
/* The same table is each finished deal's body in the grouped log, where nothing
   under the heading is bold (ui-conventions). */
.bch-result th { font-size: 12px; font-weight: 400; opacity: 0.8; }
.bch-summary .bch-result th { font-weight: 600; }
.bch-result .bch-name,
.bch-result .bch-role { text-align: left; }
.bch-result .bch-num { text-align: right; }
.bch-summary .bch-result td.bch-total { font-weight: 800; }
.bch-result tr.bch-siderow td.bch-name { color: var(--accent); }
#log-entries .bch-result { font-size: 12px; margin-top: 4px; }
#log-entries .bch-result th,
#log-entries .bch-result td { padding: 2px 4px; }
#log-entries .bch-result th { font-size: 11px; }
.bch-log-win { color: var(--accent); font-weight: 700; }
#log-entries .bch-log-key { color: var(--accent); }

/* --- a finished trick in the log ------------------------------------------ */
#log-entries .bch-logtrick { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
.bch-logtrick-cap {
    font-size: 12px;
    min-width: 0;
    flex: 1 1 auto;
    opacity: 0.85;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.bch-logcross {
    --bch-lcw: 19px;
    flex: 0 0 auto;
    display: grid;
    grid-template-columns: repeat(3, var(--bch-lcw));
    grid-template-rows: repeat(3, calc(var(--bch-lcw) * 1.39));
    grid-template-areas: "nw . ne" "w . e" ". s .";
    align-items: center;
    justify-items: center;
    gap: 1px 4px;
    padding: 0 9px;
}
.bch-logcross .card { --cw: var(--bch-lcw); }
.bch-logcross-cell { position: relative; line-height: 0; }
.bch-logcross-cell.pos-s { grid-area: s; }
.bch-logcross-cell.pos-w { grid-area: w; }
.bch-logcross-cell.pos-e { grid-area: e; }
.bch-logcross-cell.pos-nw { grid-area: nw; }
.bch-logcross-cell.pos-ne { grid-area: ne; }
.bch-logcross-led,
.bch-logcross-won { position: absolute; font-size: 10px; line-height: 1; pointer-events: none; }
.bch-logcross-led { top: -1px; left: -8px; color: var(--ink); opacity: 0.85; }
.bch-logcross-won { bottom: -1px; right: -8px; color: var(--accent); }

@media (pointer: coarse) {
    .bch-board { --bch-segh: 36px; --bch-placeh: 38px; }
}

/* --- 950px and below: the unseen cards are a sheet ----------------------- */
/* The toggle sits in the aids cell; the sheet floats (app-float) once open. */
@media (max-width: 950px) {
    .bch-aids { align-self: center; }
}

/* --- phones: one row across the top ------------------------------------- */
/* Every reservation is re-derived here. The board is a flex column: the other
   seats in one row in play order, the center at its fixed width, then the
   foot (the contract box, this player's panel, and the unseen toggle).
     picker = 5 * 40px + 4 * 4px = 216px; trick 3 * 44px + 2 * 4px = 140px
     --bch-centerw = 216px + 4px + 6px = 226px */
@media (max-width: 720px) {
    .bch-board {
        --bch-centerw: 226px;
        --bch-segw: 40px;
        --bch-segh: 34px;
        --bch-placeh: 38px;
        --bch-notefs: 11px;
        --bch-trickgap: 4px;
        --bch-playcw: 44px;
        --bch-namefs: 10px;
        --bch-totalfs: 14px;
        --bch-linefs: 10px;
        --bch-pad: 3px 4px;
        --bch-calledcw: 30px;
        --bch-dealfs: 11px;
        --bch-dealw: 150px;
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
    }
    .bch-table { display: flex; flex-wrap: nowrap; justify-content: center; gap: 4px; }
    .bch-table .bch-seat { flex: 1 1 0; width: auto; }
    .bch-center { margin-inline: auto; }
    .bch-foot { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 8px; }
    .bch-deal { flex: 0 0 var(--bch-dealw); }
    .bch-seat.bch-mine { flex: 0 0 120px; width: 120px; }
    .bch-aids { flex: 0 0 auto; }
    .bch-trick { padding: 6px 8px; }
    /* Compact panels: the short name, no marker. The full name is clipped
       rather than removed, so it is still read aloud. */
    .bch-name-full {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }
    .bch-name-short { display: inline; }
    .bch-toact { display: none; }
    .bch-tag { padding: 0 3px; }
}
/* =========================================================================
   Phibo (phibo)
   A two-player climbing and shedding game with a rank tracker: matching a
   rank earns special actions, one of which turns the tracker.
   The board is built around what a player reads every turn:
     both seat panels: rounds won, and the other player's card count
                                                   (.phb-opp, .phb-mine)
     the last combination at full size, and the trick so far  (.phb-center)
     the face-up draw pile and the discard pile                (.phb-piles)
     the rank tracker: the whole ranking low to high, in one row (.phb-tracker)
   On a wide screen the seat panels stack left of the center and the piles sit
   right of it, with the tracker in one row under all three, so the whole hand
   is on screen under them (ui-conventions, "Everything you play with is on
   screen"). On a phone the seat panels sit side by side on top.
   EVERY CARD SIZE is a custom property on .phb-board, never a rule on .card or
   a shared id, so each reservation follows it.
   ANTI-JITTER (ui-conventions.md, "Containers must not move"). Every box above
   the hand is reserved at its tallest ordinary shape: the seat panels at one
   height in every state (the ▶ is switched by .phb-off, which keeps its
   space), the last combination at one row of three, the earlier plays at one
   scrolling row, the control row at one button height in both phases, the
   note at two lines, and the status line likewise.
   SHARED IDS are reached only through a class this game owns: the hand panel
   through .phb-handpanel, the status and progress lines through .phb-status
   and .phb-progress. No rule here reserves the hand tray's height.
   ========================================================================= */
/* --- sizes ------------------------------------------------------------- */
.phb-board {
    --phb-playcw: 60px;
    --phb-earliercw: 28px;
    --phb-pilecw: 38px;
    --phb-trackcw: 34px;
    --phb-backcw: 22px;
    --phb-gap: 6px;
    --phb-centerw: 260px;
    --phb-seatw: 200px;
}
.phb-off { visibility: hidden; }
/* --- the board --------------------------------------------------------- */
.phb-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto auto minmax(0, 1fr);
    grid-template-areas:
        ".  opp     center  piles   ."
        ".  mine    center  piles   ."
        ".  tracker tracker tracker .";
    align-items: center;
    justify-items: center;
    gap: 8px 16px;
    margin-bottom: 6px;
}
.phb-opp { grid-area: opp; align-self: end; }
.phb-mine { grid-area: mine; align-self: start; }
.phb-center { grid-area: center; }
.phb-piles { grid-area: piles; }
.phb-tracker { grid-area: tracker; }
/* --- a seat's panel ------------------------------------------------------ */
.phb-seat {
    box-sizing: border-box;
    width: var(--phb-seatw);
    max-width: 100%;
    min-width: 0;
    padding: 6px 10px 7px;
    border-radius: 10px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    outline: 2px solid transparent;
    /* Inside the box, so a full-width panel on a phone is never clipped. */
    outline-offset: -2px;
}
.phb-seat.phb-active { outline-color: var(--accent); }
.phb-seat-head {
    display: flex;
    align-items: baseline;
    gap: 6px;
    margin-bottom: 4px;
    white-space: nowrap;
}
.phb-toact { flex: none; color: var(--accent); font-size: 11px; }
.phb-seat-name {
    flex: 1 1 0;
    min-width: 0;
    font-weight: 700;
    font-size: 14px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.phb-score {
    flex: none;
    font-size: 15px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.phb-seat-line {
    display: flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
    min-height: calc(var(--phb-backcw) * 89 / 64);
}
.phb-seat-line .card.phb-back { --cw: var(--phb-backcw); flex: none; }
.phb-count { font-size: 13px; font-variant-numeric: tabular-nums; opacity: 0.9; }
/* The cards both players know the other holds (its Joker, and what it took or
   was given face up): one row that scrolls rather than wraps, so it is NOT
   centered, and the panel keeps one height. */
.phb-known {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-wrap: nowrap;
    gap: 2px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
}
.phb-known .card.phb-knowncard { --cw: var(--phb-backcw); flex: none; }
/* A hand card the other player knows about wears the shared eye. Two classes,
   so it out-specifies an animation ghost's position: fixed (panels.ts). */
.card.phb-badged { position: relative; }
/* --- the center ---------------------------------------------------------- */
.phb-center {
    box-sizing: border-box;
    width: var(--phb-centerw);
    max-width: 100%;
    padding: 8px 10px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.16);
}
/* The last combination, at full size: one reserved row of three, and its tag. */
.phb-last {
    position: relative;
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    min-height: calc(var(--phb-playcw) * 89 / 64 + 6px);
}
.phb-last .phb-group { display: flex; flex-wrap: nowrap; gap: var(--phb-gap); }
.phb-last .card.phb-lastcard { --cw: var(--phb-playcw); flex: none; }
.phb-tag {
    position: absolute;
    top: 0;
    right: 0;
    padding: 1px 6px;
    border-radius: 8px;
    font-size: 11px;
    font-weight: 700;
    background: var(--accent);
    color: #1b1b1b;
    white-space: nowrap;
}
.phb-empty { font-size: 13px; opacity: 0.7; white-space: nowrap; }
/* The trick's earlier combinations, oldest left: ONE row that scrolls rather
   than wraps, NOT centered (a centered scrolling strip clips its left end). */
.phb-earlier {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-items: center;
    gap: 8px;
    height: calc(var(--phb-earliercw) * 89 / 64 + 8px);
    margin-top: 6px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}
.phb-earlier .phb-group { flex: none; display: flex; gap: 2px; }
.phb-earlier .card.phb-earliercard { --cw: var(--phb-earliercw); flex: none; }
/* A Joker's badge says what it counts as; "10" is two characters, so the box
   grows to its text rather than staying square. */
.card.phb-jokercard .ability-badge { width: auto; min-width: clamp(14px, calc(var(--cw) * 0.26), 22px); padding: 0 3px; }
/* --- the piles ----------------------------------------------------------- */
.phb-piles {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 8px;
}
.phb-pile {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    width: calc(var(--phb-pilecw) + 40px);
}
.phb-pile .card.phb-pilecard { --cw: var(--phb-pilecw); flex: none; }
.phb-pilecount {
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    opacity: 0.85;
}
/* --- the rank tracker -------------------------------------------------- */
/* The whole ranking low to high in one row: the low 2 as diamonds, 1 to 10,
   the high 2. Green underline marks a rank that can be matched (the rules
   sheet's own colors); the bomb's two ranks carry a dashed outline. */
.phb-tracker {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 4px 10px;
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.12);
    white-space: nowrap;
}
.phb-rankword { font-size: 11px; opacity: 0.75; }
.phb-ranks { display: flex; flex-wrap: nowrap; align-items: stretch; gap: 3px; }
.phb-ranks .card.phb-trackcard { --cw: var(--phb-trackcw); flex: none; border-bottom: 3px solid transparent; }
.phb-ranks .card.phb-trackcard.phb-can { border-bottom-color: #3fae5a; }
.phb-ranks .card.phb-bombrank { outline: 2px dashed var(--accent); outline-offset: 1px; }
/* Owing a special action: the lowest card is the Rotate control. */
.phb-ranks .card.phb-rotate { outline: 3px solid var(--accent); outline-offset: 1px; cursor: pointer; }
.phb-ranks .card.phb-rotate.hinted { animation: hintpulse 0.9s ease-in-out infinite alternate; }
/* Each rank over its unseen boxes: one per card of it in the pack (3, or 2
   for the Joker's 1), the cards not seen this round filled, the rest faded.
   Always present (.phb-off hides them), so the row is one height. */
.phb-rankcol { display: inline-flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 2px; }
.phb-rankcol .phb-ranknum { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; }
.phb-pips { display: flex; gap: 1px; width: 100%; max-width: 22px; height: 5px; }
.phb-pip { flex: 1 1 0; min-width: 0; border-radius: 1px; background: #f2efe9; }
.phb-pip.phb-seen { background: rgba(242, 239, 233, 0.2); }
.phb-ranknum {
    min-width: 1.35em;
    text-align: center;
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1.5;
    border-bottom: 3px solid transparent;
}
.phb-ranknum.phb-can { border-bottom-color: #3fae5a; }
/* --- the action box ---------------------------------------------------- */
.phb-actions { margin-bottom: 6px; }
.phb-controls {
    flex-wrap: nowrap;
    justify-content: center;
    align-items: stretch;
    margin: 4px 0;
    height: 36px;
}
.phb-controls .btn {
    flex: none;
    box-sizing: border-box;
    height: 36px;
    width: 7.5em;
    white-space: nowrap;
    line-height: 20px;
}
.phb-controls.phb-acting .btn { width: 5.6em; }
.phb-controls .btn.hinted {
    animation: hintpulse 0.9s ease-in-out infinite alternate;
    outline: 3px solid var(--accent);
}
.phb-note {
    min-height: 38px;
    line-height: 19px;
    font-size: 14px;
    text-align: center;
}
/* --- the hand ---------------------------------------------------------- */
.phb-handpanel .card.phb-dim { opacity: 0.45; }
/* ONE ROW ON A DESKTOP (ui-conventions, "A wide hand stays one row"). The tray
   is a size container; with n cards (--phb-n, set by the renderer) each card
   takes the site size, or shrinks to share the row, down to a floor; past the
   floor the cards overlap, every index corner (top left) still showing. A
   phone keeps the shared wrapping tray. */
@media (min-width: 721px) {
    .phb-handpanel { --phb-handcw: var(--cw); --phb-handfloor: 44px; }
    .phb-handpanel #hand-cards {
        container-type: inline-size;
        flex-wrap: nowrap;
        gap: 0;
    }
    .phb-handpanel #hand-cards .card {
        --cw: clamp(var(--phb-handfloor), calc((100cqw - (var(--phb-n) - 1) * 6px) / var(--phb-n)), var(--phb-handcw));
        margin-left: min(6px, calc((100cqw - var(--phb-n) * var(--cw)) / (var(--phb-n) - 1)));
    }
    .phb-handpanel #hand-cards .card:first-child { margin-left: 0; }
    .phb-handpanel #hand-cards .card:not(.card-fly) { position: relative; z-index: 0; }
}
/* --- status and progress ----------------------------------------------- */
.status.phb-status { line-height: 1.35; min-height: 2.7em; }
.status.phb-status.phb-turn { color: var(--accent); font-weight: 700; }
.progress.phb-progress { min-height: 1.4em; white-space: nowrap; }
/* --- the summary ------------------------------------------------------- */
.phb-result {
    border-collapse: collapse;
    margin: 0 auto 10px;
    font-size: 14px;
}
.phb-result th,
.phb-result td {
    padding: 4px 10px;
    border-bottom: 1px solid var(--panel-border);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}
.phb-result th { font-size: 12px; font-weight: 600; opacity: 0.85; }
.phb-result .phb-name { text-align: left; }
.phb-result .phb-num { text-align: right; }
.phb-result tr.phb-best td { color: var(--accent); font-weight: 700; }
/* --- phones ------------------------------------------------------------ */
@media (max-width: 720px) {
    .phb-board {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        grid-template-areas:
            "opp     mine"
            "center  piles"
            "tracker tracker";
        gap: 6px;
        --phb-seatw: 100%;
        --phb-centerw: 250px;
        --phb-earliercw: 22px;
    }
    .phb-center { justify-self: end; }
    .phb-piles { justify-self: start; flex-direction: row; }
    .phb-opp, .phb-mine { align-self: stretch; }
    /* The panel is half the width: the count's words and the known cards need the back's room. */
    .phb-seat-line .card.phb-back { display: none; }
}
@media (max-width: 400px) {
    .phb-board {
        --phb-playcw: 52px;
        --phb-pilecw: 30px;
        --phb-trackcw: 28px;
        --phb-centerw: 200px;
        --phb-earliercw: 20px;
    }
    .phb-pile { width: calc(var(--phb-pilecw) + 26px); }
    .phb-piles { flex-direction: column; gap: 4px; }
    .phb-ranknum { min-width: 1.1em; font-size: 12px; }
    .phb-tracker { gap: 3px; padding: 4px 4px; }
    .phb-controls .btn { width: 6.8em; }
    .phb-controls.phb-acting .btn { width: 5em; }
}
