/* style.css — TRICKTAKERs. THIS GAME ONLY.
 *
 * The header bar, AI badge, tool buttons, the 📜 / 🤖 panels, the start screen, the dialog
 * shell (.overlay / .panel / .primary-btn), the house fonts and the table felt all come from
 * ../core/deckades.css, which index.html links FIRST. Nothing here may restyle any of those —
 * see the uniformity contract in /card-game-UI, enforced by a grep over this file:
 *
 *   grep -n "#header-bar\|\.deckades-logo\|\.start-card\|#ai-indicator" games/Tricktakers/style.css
 *       -> must return nothing
 *
 * What is left is Tricktakers' own surface: the beige table, the three logos baked into the
 * felt, the cards, the seat plates, the character cards and the round-end sheet.
 *
 * ONE EXCEPTION, taken deliberately on 2026-08-16 and marked as such in :root below: this game
 * re-declares the chrome's PURPLE --dk-* tokens to the King's cloak blue. That is outside the
 * four-token contract above. It changes hues only — every alpha, gradient stop and radius stays
 * the shared value — and it is the one place in this file where "nothing here may restyle the
 * shell" is knowingly not true.
 *
 * The board is JS-positioned: game.js writes left/top/width/height (and font-size) onto every
 * card and plate from the viewport dimensions, so this file styles APPEARANCE only and never
 * the board geometry. Card internals are sized in `em` against the card's own font-size, which
 * game.js sets to the current card width — one number scales a whole card.
 */

/* ── The game's own display face ──────────────────────────────────────────────
   D-DIN, Datto's open release of DIN 1451, under the SIL Open Font License 1.1 — the licence
   and FONTLOG travel with the files in assets/fonts/. NOT the commercial FF DIN / DIN Next,
   which could not be bundled. The Reserved Font Name means these files may be redistributed but
   not modified under the name "D-DIN"; nothing here modifies them.

   Only --dk-font-display is repointed. core/deckades.css is explicit that a game shipping its
   own font repoints THAT token and leaves --dk-font-ui as Fredoka, so buttons, labels and
   panels still match every other game on the platform. */
@font-face {
    font-family: 'D-DIN Condensed';
    src: url('assets/fonts/D-DINCondensed.woff2') format('woff2');
    font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
    font-family: 'D-DIN Condensed';
    src: url('assets/fonts/D-DINCondensed-Bold.woff2') format('woff2');
    font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
    font-family: 'D-DIN';
    src: url('assets/fonts/D-DIN.woff2') format('woff2');
    font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
    font-family: 'D-DIN';
    src: url('assets/fonts/D-DIN-Bold.woff2') format('woff2');
    font-weight: 700; font-style: normal; font-display: swap;
}

:root {
    /* The four skin tokens a game may re-declare. Sampled from the rulebook itself: the
       wordmark's tan, and a beige wash for the table as asked. */
    --dk-accent:      #d9a44e;
    --dk-accent-soft: rgba(217, 164, 78, 0.35);
    /* The printed wordmark is a condensed industrial grotesque, so the display face is the
       nearest libre relative of the type it was drawn from rather than the house garamond. */
    /* The CONDENSED cut is the title voice: headings and button captions. `--dk-font-display`
       is the sanctioned token and core/deckades.css hangs .panel h2 / .start-title off it, so
       repointing it gives every dialog heading the new face for free. Body text takes the
       uncondensed cut through --dk-font-ui just below — a condensed grotesque is a title face,
       not a paragraph one. */
    --dk-font-display: 'D-DIN Condensed', 'D-DIN', 'Cormorant Garamond', Georgia, serif;
    /* A SECOND deliberate exception to the shared-chrome contract (2026-08-18, user's call).
       core/deckades.css says --dk-font-ui never moves, so every game's buttons and labels match.
       Tricktakers moves it anyway: the box, the cards and the rulebook are set in DIN end to
       end, and a game that carried the industrial face on its titles and a rounded geometric
       everywhere else read as two designs rather than one. It is the UNCONDENSED cut here —
       Condensed is the title voice, and at 13px a condensed grotesque loses its counters.
       The house Fredoka stays in the stack behind it, so a failed font file falls back to the
       platform's own face rather than to whatever the browser keeps. */
    --dk-font-ui:      'D-DIN', 'D-DIN Condensed', 'Fredoka', system-ui, sans-serif;
    /* The one voice that is NOT DIN: the printed cards' numerals and the round badge. Those are
       components of the physical game sitting on the table, not chrome around it. */
    --tt-serif:        'Cormorant Garamond', Georgia, serif;

    /* The read screens — the draft and the round-end carousel — are cooler than the felt-side
       panels: they are the only places in the game you STOP and read, and a warm brown ground
       under a character portrait muddied both.
       The blue is the King's CLOAK, measured off assets/characters/1_King.webp over both
       shoulder bands: median #4d6696 across 21k samples. The ground IS that colour taken down,
       not a neutral slate with a blue glow laid over it — as a glow it read as navy and not as
       the cloak at all. */
    --tt-blue-lit:  #4d6696;
    --tt-blue:      #35476a;
    --tt-blue-deep: #1f2a41;
    --tt-beige:     #e8dfcc;

    /* ── A DELIBERATE EXCEPTION to the shared-chrome contract ────────────────
       core/deckades.css says a game may re-declare only --dk-accent,
       --dk-accent-soft, --dk-tint and --dk-font-display, and that everything
       below is "never overridden by a game". Tricktakers overrides the chrome's
       PURPLE anyway, on an explicit decision (2026-08-16): the platform violet
       under a game whose whole palette is the King's blue cloak, tan card stock
       and beige felt read as two designs stacked on each other.
       Only hues change. Every alpha, gradient stop and radius is the shared
       value (one exception, --dk-panel, is explained where it is set), so the
       chrome keeps its shape and only its colour moves — and the purple is
       replaced by the same cloak blue (#4d6696) the read screens use, not by an
       invented one.
       The block has to be COMPLETE. The 2026-09-17 polish pass found the tool
       buttons, the AI badge, the ? popup, the dialog gradient and the start
       card's inks still on the platform violet: every token the shared sheet
       had since promoted from a literal, and none of them re-pointed here. A
       re-tint that stops at the tokens that existed when it was written leaves
       the old world showing through the newer ones — so this block lists every
       colour token the sheet declares, including the ones that keep their value.
       If the platform palette ever moves, THIS BLOCK is what has to move with
       it; nothing here tracks core/deckades.css automatically. */
    --dk-header-bg:   linear-gradient(180deg,
                          rgba(10, 14, 24, 0.92),
                          rgba(10, 14, 24, 0.65) 80%,
                          rgba(10, 14, 24, 0));
    --dk-header-line: rgba(77, 102, 150, 0.22);

    --dk-logo:        rgba(186, 205, 238, 0.45);
    --dk-logo-hover:  rgba(200, 216, 245, 0.92);
    --dk-spade:       rgba(77, 102, 150, 0.65);
    --dk-spade-hover: rgba(124, 158, 214, 1);

    /* The one alpha that is NOT the shared value. The sheet's panels are 0.97 so a hint of
       the table shows through them; here the standings and the Round Card are parked in the
       very corner the move log and the AI panel open over, and 3% of gold-on-black text
       ghosted through both panels as legible words. Opaque, because what is behind these
       panels in this game is text, not felt. */
    --dk-panel:       rgb(24, 33, 51);
    --dk-panel-line:  rgba(77, 102, 150, 0.34);
    --dk-panel-text:  rgba(226, 216, 195, 0.92);
    --dk-panel-dim:   rgba(226, 216, 195, 0.55);
    --dk-chrome-plate:      rgba(10, 14, 24, 0.92);
    --dk-chrome-plate-line: rgba(77, 102, 150, 0.30);

    /* The dialog shell: the violet gradient's two stops, taken to the cloak. */
    --dk-dialog-top:    #223150;
    --dk-dialog-bottom: #121a2c;
    --dk-dialog-line:   rgba(77, 102, 150, 0.45);

    /* The ? popup's edge and close cross, and the scrollbar of a long panel. */
    --dk-popup-line:   rgba(77, 102, 150, 0.5);
    --dk-scroll-thumb: rgba(77, 102, 150, 0.34);
    --dk-popup-close:  rgba(186, 205, 238, 0.85);

    /* Header tool buttons. The ring is the LIT cloak (the spade's hover value) rather than
       the resting one — at 0.45 over the near-black header the resting blue disappeared. */
    --dk-tool-ring:       rgba(124, 158, 214, 0.45);
    --dk-tool-ink:        rgba(186, 205, 238, 0.85);
    --dk-tool-wash:       rgba(124, 158, 214, 0.2);
    --dk-tool-ink-hover:  rgba(215, 228, 250, 1);
    --dk-tool-ring-hover: rgba(150, 180, 230, 1);

    /* The AI badge: its plate and resting ink, and the "ready" state, which is the one of
       the four the sheet draws in lilac. ONNX green, heuristic gold and the pending amber
       are signal colours and keep their platform values. */
    --dk-ai-bg:           rgba(10, 14, 24, 0.65);
    --dk-ai-ink:          rgba(186, 205, 238, 0.7);
    --dk-ai-ready-ink:    rgba(170, 198, 245, 0.95);
    --dk-ai-ready-ring:   rgba(124, 158, 214, 0.45);
    --dk-ai-ready-dot:    rgba(170, 198, 245, 0.95);
    --dk-ai-ready-glow:   0 0 8px rgba(124, 158, 214, 0.65);

    --dk-scrim:       rgba(7, 10, 17, 0.86);
    --dk-field-bg:    #1b2436;
    --dk-field-line:  #3a4f78;

    /* The start card. Its surface is the panel colour and its inks are the game's own paper
       — the sheet's defaults are lilacs picked for a violet card, and on the cloak they read
       as the platform's colour rather than this game's. Secondary buttons take their edge
       from the accent, as the default derives from the platform's. */
    --dk-start-surface:        rgba(18, 25, 40, 0.97);
    --dk-start-ink:            #f4efe2;
    --dk-start-ink-dim:        #c8bfab;
    --dk-start-ink-soft:       #e4dbc6;
    --dk-start-ink-fade:       #948c7a;
    --dk-start-secondary-edge: rgba(217, 164, 78, 0.6);
    /* The wordmark baked into the felt is TWO-TONE — charcoal above, tan below — so the
       centre of the table has to be light enough for the dark half to read at all. At 0.30
       the top of the logo vanished into the scrim and only its gold half showed.
       Cooled towards the King's cloak (2026-08-18): the same beige, pulled a step down the
       blue side, so the felt sits under the draft and the round sheets rather than against
       them. Kept warm ENOUGH to hold the wordmark's charcoal half up — a straight blue tint
       at this alpha took the top of the logo back into the scrim. */
    --dk-tint:        rgba(126, 156, 214, 0.38);   /* the cool wash over the shared felt */

    /* Suit colours, sampled at dpi 72 off the rulebook's own suit swatches (p.6/7). */
    --s-r: #e27c70;
    --s-b: #788ec7;
    --s-g: #65b1ab;
    --s-k: #433e3b;

    /* Character header colours, sampled off the rulebook's section banners (pp.12-18). */
    --c-king:       #5077bb;
    --c-gambler:    #e7ba4f;
    --c-resistance: #f39139;
    --c-adventurer: #63a05c;
    --c-hermit:     #6064ad;
    --c-collector:  #6fc4cb;
    --c-berserker:  #ee7992;
    --c-ruler:      #948e89;

    /* The rulebook's own neutrals: a "dark white" and a warmer parchment. */
    --paper:      #f4efe2;
    --paper-dim:  #ded7c6;
    --ink:        #2f2a24;
    --dark-white: #ededee;
    --gold:       #d9a44e;
    --crown-black:#8d8578;

    /* Card geometry — JS writes these from updateCardDims() on load and on resize. */
    --card-w: 76px;
    --card-h: 118px;
    --card-radius: 7px;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
    height: 100%;
    touch-action: manipulation;
    overscroll-behavior: none;
    -webkit-tap-highlight-color: transparent;
}

/* Cover art behind the START SCREEN only — the one place a per-game identity is wanted. The
   table itself is the shared felt (#game below), so the two are layered rather than competing.
   Tricktakers ships no cover photo, so the "art" is its own palette: a warm beige field with
   the character-header hues bled into the corners, which is the rulebook's own look. */
body {
    background: #1a1611;
    color: var(--paper);
    overflow: hidden;
    font-family: var(--dk-font-ui);
    -webkit-user-select: none; user-select: none;
}
/* Tricktakers ships no box-art photo, so the "cover" is the thing the game is actually about:
   the five basic characters, laid out as a gallery. The shared #start-screen already puts an
   0.86 scrim over whatever is here, which is why this layer carries no scrim of its own —
   stacking a second one takes the whole screen to black, which is what it did at first. */
body::before {
    content: '';
    position: fixed; inset: 0;
    /* Five cards on a 20%-of-width grid, each 292:500. At 66% of the height alone they were
       594px tall and 347px wide on a 1440x900 desktop — wider than their 288px columns, so
       the gallery overlapped itself into a jumble of banners. The cap on WIDTH keeps every
       card inside its own column (h ≤ 32vw → w ≤ 18.7vw) with a gutter between them. */
    --tt-gallery-h: min(66%, 32vw);
    background:
        url('assets/characters/king-front.webp')       10% 44% / auto var(--tt-gallery-h) no-repeat,
        url('assets/characters/gambler-front.webp')    30% 56% / auto var(--tt-gallery-h) no-repeat,
        url('assets/characters/resistance-front.webp') 50% 44% / auto var(--tt-gallery-h) no-repeat,
        url('assets/characters/hermit-front.webp')     70% 56% / auto var(--tt-gallery-h) no-repeat,
        url('assets/characters/berserker-front.webp')  90% 44% / auto var(--tt-gallery-h) no-repeat,
        radial-gradient(ellipse 34% 46% at  8% 30%, rgba(80, 119, 187, 0.55), transparent 70%),
        radial-gradient(ellipse 34% 46% at 30% 74%, rgba(231, 186, 79, 0.50), transparent 70%),
        radial-gradient(ellipse 34% 46% at 52% 26%, rgba(243, 145, 57, 0.50), transparent 70%),
        radial-gradient(ellipse 34% 46% at 74% 74%, rgba(96, 100, 173, 0.55), transparent 70%),
        radial-gradient(ellipse 34% 46% at 94% 30%, rgba(238, 121, 146, 0.50), transparent 70%),
        radial-gradient(ellipse 70% 60% at 50% 40%, #e6d7b6, transparent 72%),
        linear-gradient(160deg, #c8b894, #8e7f63 58%, #4f4538);
    transform: scale(1.12);
    transform-origin: center;
    /* The shared start scrim is 0.86, so anything subtle back here is simply not there. */
    filter: saturate(1.35) brightness(1.12);
    pointer-events: none;
    z-index: -2;
}
/* The wordmark on the start card. core/deckades.css styles .start-title but NOT .start-logo,
   so an <img> here renders at its intrinsic 736x199 and burst straight out of the 620px card.
   Same shape as games/Decks and games/Toy: cap the width, let the height follow. */
.start-logo {
    display: block;
    width: min(340px, 70%);
    height: auto;
    margin: 0 auto 20px;
    filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.45));
}

/* The advanced-characters toggle is added by start.js, so it is the game's to style. Shaped
   to sit under the seat-count field without competing with it. */
.start-check {
    /* .start-field is a COLUMN (label above control); this one is a row, so the direction has
       to be re-declared or the box stacks and the label centres under the tick. */
    display: flex; flex-direction: row;
    align-items: center; justify-content: flex-start;
    gap: 9px;
    margin-top: 12px;
    font-size: 14px;
}
.start-check input { width: 17px; height: 17px; accent-color: var(--dk-accent); cursor: pointer; }
.start-check label { cursor: pointer; }
.start-check.disabled { opacity: 0.45; }
.start-check.disabled input, .start-check.disabled label { cursor: not-allowed; }
/* A checkbox that only means anything while the one above it is ticked sits UNDER it, not
   beside it in the same rhythm as the seat count. .start-col-left's own 14px gap plus
   .start-check's 12px put 26px between the two — as much air as separates unrelated
   fields. This closes it to a hair. */
.start-check.start-subcheck { margin-top: -6px; }

/* A vignette only — deep enough to keep the gallery off the card's edges, light enough that
   the characters still read through the shared scrim. */
body::after {
    content: '';
    position: fixed; inset: 0;
    background: radial-gradient(ellipse 80% 70% at 50% 48%, rgba(10, 8, 5, 0.10), rgba(8, 6, 4, 0.66));
    pointer-events: none;
    z-index: -1;
}

/* ── The table ─────────────────────────────────────────────────────────────── */

/* The shared felt photo (games/table.avif) under a dark scrim, tinted beige with --dk-tint.
   Not a per-game gradient — the felt is what makes the site read as one product, and the
   scrim keeps the cards the brightest thing on screen. */
#game {
    position: fixed; inset: 0;
    z-index: 10;
    display: none;
    background: var(--dk-table);
    touch-action: none;
    overflow: hidden;
}
#game.on { display: block; }

/* Logos baked INTO the felt, the same technique games/Haggis uses for its crest: low opacity,
   pointer-events:none, z-index 0 so every positioned child paints over them. They are part of
   the table, not floating UI — the play area reads through them and they never take a click. */
.felt-mark {
    position: absolute;
    pointer-events: none;
    user-select: none;
    z-index: 0;
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
}
.felt-mark.tt {
    left: 50%; top: 50%;
    width: min(46vw, 460px);
    height: calc(min(46vw, 460px) * 0.27);      /* the wordmark is 1881x509 */
    transform: translate(-50%, -50%);
    /* The LIGHT cut of the wordmark, not the one the start card uses. The main artwork is
       charcoal above the waist and tan below (median luminance 65 of 255), so on a dark table
       its top half simply was not there — no amount of opacity fixes dark-on-dark. This cut is
       a uniform parchment (luminance 234 throughout), so every letter carries the same weight
       and the opacity below actually controls how present it is. */
    background-image: url('assets/misc/tricktakers_logo_light.webp');
    opacity: 0.22;
}
.felt-mark.pgc {
    right: 18px; bottom: 14px;
    width: 78px; height: 63px;                   /* 184x148 */
    background-image: url('assets/misc/pgc_logo.webp');
    opacity: 0.26;
}
.felt-mark.joyple {
    left: 18px; bottom: 14px;
    width: 92px; height: 66px;                   /* 286x206 */
    background-image: url('assets/misc/joyple_logo.png');
    opacity: 0.24;
}
@media (max-height: 480px), (max-width: 700px) {
    .felt-mark.pgc { width: 54px; height: 44px; right: 10px; bottom: 8px; }
    .felt-mark.joyple { width: 64px; height: 46px; left: 10px; bottom: 8px; }
    .felt-mark.tt { opacity: 0.10; }
}

/* The cast printed into the table, the way the Sevens board carries its four knights. All
   eight ring the play area — two along the top, two along the bottom, two down each side — so
   the middle stays clear for the wordmark and the trick. Same z-index 0 / pointer-events:none
   contract as the logos above: the felt panel, every card and every plate paint over them, and
   the figures the felt overlaps pick up its scrim and read a shade fainter. That vignette falls
   out of the layering; it is not tuned. */
.felt-cast {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    user-select: none;
}
/* The art is square, so `contain` renders it at min(width, height) — one --cast-size drives
   both. Colour and tint are baked in by scripts/make_tricktakers_table_art.py; the only thing
   left to set here is how far down it is turned. */
.cast-fig {
    --cast-size: min(26vw, 34vh, 420px);
    position: absolute;
    width: var(--cast-size);
    height: var(--cast-size);
    transform: translate(-50%, -50%);
    opacity: 0.15;
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
    transition: opacity 0.45s ease, filter 0.45s ease;
}
/* The figure of whoever is on turn lifts a little out of the wood — a second, ambient turn
   indicator behind the plates. drop-shadow follows the alpha channel, so the glow traces the
   silhouette instead of boxing it; a box-shadow here would light up a square of nothing. */
.cast-fig.lit {
    opacity: 0.20;
    filter: drop-shadow(0 0 9px rgba(243, 200, 120, 0.22));
}
@media (prefers-reduced-motion: reduce) { .cast-fig { transition: none; } }

/* Eight points round an ellipse, in percentages so the ring survives every viewport and seat
   count. The top and bottom pairs sit at 16/84 rather than nearer the edge because a figure is
   half its own height tall above its anchor: any closer and the header eats the top pair. The
   ellipse is symmetric on both axes, so whatever the viewport crops, it crops evenly. */
.cast-fig[data-fig="king"]       { left: 12%; top: 26%; background-image: url('assets/characters/table/1_King.webp'); }
.cast-fig[data-fig="gambler"]    { left: 36%; top: 16%; background-image: url('assets/characters/table/2_Gambler.webp'); }
.cast-fig[data-fig="resistance"] { left: 64%; top: 16%; background-image: url('assets/characters/table/3_Resistance.webp'); }
.cast-fig[data-fig="hermit"]     { left: 88%; top: 26%; background-image: url('assets/characters/table/4_Hermit.webp'); }
.cast-fig[data-fig="berserker"]  { left: 88%; top: 74%; background-image: url('assets/characters/table/5_Berserker.webp'); }
.cast-fig[data-fig="adventurer"] { left: 64%; top: 84%; background-image: url('assets/characters/table/6_Adventurer.webp'); }
.cast-fig[data-fig="collector"]  { left: 36%; top: 84%; background-image: url('assets/characters/table/7_Collector.webp'); }
.cast-fig[data-fig="ruler"]      { left: 12%; top: 74%; background-image: url('assets/characters/table/8_Ruler.webp'); }

/* A narrow screen would overlap the ring into a smear. Drop figures rather than shrink them —
   the four that survive keep their scale and still mark the four corners of the table. */
@media (max-width: 1100px) {
    .cast-fig { --cast-size: min(34vw, 30vh, 340px); }
    .cast-fig[data-fig="gambler"],
    .cast-fig[data-fig="resistance"],
    .cast-fig[data-fig="adventurer"],
    .cast-fig[data-fig="collector"] { display: none; }
}
@media (max-width: 700px), (max-height: 480px) {
    .cast-fig { --cast-size: min(42vw, 26vh, 260px); opacity: 0.12; }
}

/* The play rectangle. Sized and placed by game.js (renderFelt). */
#felt {
    position: absolute;
    z-index: 5;
    border-radius: 18px;
    border: 1px solid rgba(217, 164, 78, 0.42);
    background: rgba(24, 19, 14, 0.30);
    box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.55), 0 8px 28px rgba(0, 0, 0, 0.45);
    pointer-events: none;
    transition: box-shadow 0.3s, border-color 0.3s, background 0.3s;
}
/* The lead suit decides what every card still to be played is allowed to be, so the table's own
   edge carries it. game.js writes --felt-lead; the Revolt rule below outranks it, because a
   Revolt trick is about strength being inverted rather than about which colour was led. */
#felt.lead-suit {
    border-color: var(--felt-lead, rgba(217, 164, 78, 0.42));
    box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.55), inset 0 0 22px -8px var(--felt-lead),
                0 0 22px -6px var(--felt-lead), 0 8px 28px rgba(0, 0, 0, 0.45);
}
/* A Revolt trick is the one moment the whole hierarchy is upside down — the table says so. */
#felt.revolt {
    border-color: rgba(243, 145, 57, 0.85);
    background: rgba(243, 145, 57, 0.10);
    box-shadow: inset 0 0 70px rgba(243, 145, 57, 0.22), 0 0 34px rgba(243, 145, 57, 0.35);
}

#round-badge {
    position: absolute;
    left: 50%; bottom: 8px;
    transform: translateX(-50%);
    font-family: var(--tt-serif);
    font-size: 13px; font-weight: 700;
    letter-spacing: 0.14em; text-transform: uppercase;
    font-variant-numeric: lining-nums;   /* caps and old-style figures do not share a line */
    color: rgba(244, 239, 226, 0.62);
    white-space: nowrap;
    pointer-events: none;
}
#lead-badge {
    position: absolute;
    left: 50%; top: 8px;
    transform: translateX(-50%);
    font-family: var(--tt-serif);
    font-size: 13px; font-weight: 700;
    letter-spacing: 0.1em;
    color: rgba(244, 239, 226, 0.75);
    white-space: nowrap;
    pointer-events: none;
}
#lead-badge .lead-chip {
    display: inline-block;
    width: 0.72em; height: 0.72em;
    border-radius: 2px;
    margin-left: 6px;
    vertical-align: baseline;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);
}
#felt.revolt ~ * #lead-badge, #lead-badge.revolt { color: #ffb877; }

/* ── Cards ─────────────────────────────────────────────────────────────────── */

.card {
    position: absolute;
    width: var(--card-w);
    height: var(--card-h);
    border-radius: var(--card-radius);
    background: var(--paper);
    color: var(--ink);
    border: 0.022em solid rgba(0, 0, 0, 0.35);
    box-shadow: 0 0.04em 0.09em rgba(0, 0, 0, 0.5);
    overflow: hidden;
    z-index: 20;
    touch-action: none;
    transition: filter 0.15s, box-shadow 0.15s;
    /* game.js sets font-size to the card's own width, so everything inside scales in `em`. */
}
.card > * { position: relative; z-index: 2; }

/* Artwork, once assets/cards/<id>.webp exists. addArt() only swaps this in on load, so a
   missing file silently leaves the CSS-drawn card standing. */
.card-art {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    object-fit: cover;
    z-index: 1;
    border-radius: inherit;
}
.card.has-art > .pip,
.card.has-art > .face { display: none; }

.card .pip {
    position: absolute;
    /* The game's own UI face, not the browser's: a corner numeral is exactly what a plain
       grotesque is for, and this was the last place a system font could reach the screen. */
    font-family: var(--dk-font-ui);
    font-size: 0.20em;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -0.02em;
}
.card .pip.tl { top: 0.34em; left: 0.34em; }
.card .pip.br { bottom: 0.34em; right: 0.34em; transform: rotate(180deg); }

.card .face {
    position: absolute; inset: 0;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    gap: 0.03em;
}
.card .face-num {
    font-family: var(--tt-serif);
    font-size: 0.62em; font-weight: 700; line-height: 0.9;
}
.card .face-glyph { font-size: 0.30em; line-height: 1; opacity: 0.9; }
.card .face-word {
    font-family: var(--dk-font-ui);
    font-size: 0.115em; font-weight: 700;
    letter-spacing: 0.14em; text-transform: uppercase;
    opacity: 0.8;
}

/* Number cards: the suit is the whole card, the way the printed deck does it. */
.card.n-r { background: var(--s-r); color: #fff; border-color: rgba(0, 0, 0, 0.3); }
.card.n-b { background: var(--s-b); color: #fff; border-color: rgba(0, 0, 0, 0.3); }
.card.n-g { background: var(--s-g); color: #fff; border-color: rgba(0, 0, 0, 0.3); }
.card.n-k { background: var(--s-k); color: var(--gold); border-color: rgba(0, 0, 0, 0.55); }

/* The three colourless kinds are parchment, so they read as "not a suit" at a glance. */
.card.k-rare { background: linear-gradient(170deg, #f7f2e4, #e3d8bd); color: #6b4a20; }
.card.k-rare .face-glyph { color: #a97a37; }
.card.k-flag { background: linear-gradient(170deg, #fbfaf6, #e8e6df); color: #7a7468; }
.card.k-berserker {
    background: linear-gradient(170deg, #2a2622, #14110e);
    color: var(--gold);
    border-color: rgba(217, 164, 78, 0.5);
}
.card.k-berserker .face-glyph { color: var(--gold); }

.card.back {
    background: repeating-linear-gradient(135deg, #b8ddd2 0 0.09em, #a8d2c6 0.09em 0.18em);
    border-color: rgba(0, 0, 0, 0.35);
}
.card.back::after {
    content: '';
    position: absolute; inset: 0.08em;
    border: 0.02em solid rgba(255, 255, 255, 0.55);
    border-radius: calc(var(--card-radius) - 2px);
}
.card.back .face, .card.back .pip { display: none; }

.card.playable {
    cursor: pointer;
    box-shadow: 0 0 0 0.028em var(--gold), 0 0.05em 0.12em rgba(0, 0, 0, 0.55);
}
.card.playable:hover { filter: brightness(1.1); }
.card.blocked { filter: grayscale(0.5) brightness(0.62); cursor: not-allowed; }
.card.picked { box-shadow: 0 0 0 0.05em #7fe0a0, 0 0.05em 0.14em rgba(0, 0, 0, 0.6); }
.card.won-glow {
    box-shadow: 0 0 0 0.035em #ffd98a, 0 0 0.28em 0.06em rgba(255, 217, 138, 0.6);
    filter: brightness(1.1);
}
.card.lost-dim { filter: brightness(0.5) saturate(0.6); }
.card.flying { z-index: 500; pointer-events: none; }

/* The Revolt Token, riding the card it was placed on. */
.revolt-token {
    position: absolute;
    right: -0.07em; bottom: -0.07em;
    width: 0.34em; height: 0.34em;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, #ffb877, #d1651c 70%);
    border: 0.02em solid rgba(255, 255, 255, 0.7);
    box-shadow: 0 0 0.14em rgba(243, 145, 57, 0.9);
    z-index: 6;
    display: flex; align-items: center; justify-content: center;
    font-size: 0.9em;
}
.revolt-token::after {
    content: '✊';
    font-size: 0.19em;
    line-height: 1;
}
/* With the real punchboard token extracted, the CSS medallion gets out of its way entirely. */
.revolt-token.has-art {
    background: center / contain no-repeat;
    border: 0; box-shadow: 0 0.02em 0.06em rgba(0, 0, 0, 0.6);
    width: 0.38em; height: 0.38em;
}
.revolt-token.has-art::after { content: none; }

/* The Collector's Reserve Token. Same box and the same three homes as the Revolt token —
   beside the character card, on a card for one trick, then back — so the two share
   .play-token and differ only in the fallback medallion. */
.reserve-token {
    position: absolute;
    right: -0.07em; bottom: -0.07em;
    width: 0.34em; height: 0.34em;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, #b8ecef, #2e9aa4 70%);
    border: 0.02em solid rgba(255, 255, 255, 0.7);
    box-shadow: 0 0 0.14em rgba(111, 196, 203, 0.9);
    z-index: 6;
    display: flex; align-items: center; justify-content: center;
    font-size: 0.9em;
}
.reserve-token::after { content: '🔖'; font-size: 0.19em; line-height: 1; }
.reserve-token.has-art {
    background: center / contain no-repeat;
    border: 0; box-shadow: 0 0.02em 0.06em rgba(0, 0, 0, 0.6);
    width: 0.38em; height: 0.38em;
}
.reserve-token.has-art::after { content: none; }

/* Tokens ON a played card sit in the MIDDLE of it, and side by side when more than one lands
   there. A token on a card answers "what is different about THIS card", and the corners of a
   card in an open trick are already overlapped by its neighbours — the centre is the only place
   that reads. Up to three can share one card (Reserve + Revolt + a Lead Token), so it is a flex
   row and not a set of fixed offsets. Home and in-flight keep their own anchoring, and flyToken
   probes this box for its destination, so a flight lands here without being told. */
/* The item an Adventurer used on the card it is lying beside. Absolute in the GAME, not in the
   card: `.card` hides its overflow, so anything parked outside the card's box vanishes. The
   unused-points tag is dropped — it prices an item still in the rack, and this one is spent. */
.play-item {
    position: absolute;
    z-index: 65;
    pointer-events: auto;
    cursor: zoom-in;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(59, 178, 83, 0.55);
}
.play-item .item-value { display: none; }

.card-tokens {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center; justify-content: center;
    gap: 0.06em;
    pointer-events: none;
    z-index: 6;
}
.card-tokens > .play-token {
    position: relative;
    left: auto; right: auto; top: auto; bottom: auto;
    transform: none;
}

/* A token at REST, beside the character card rather than on it — "next to your character
   card" is the printed set-up instruction, and ON the card is what PLAYED looks like. Half
   outside the right edge, the way a punchboard piece leans against a card on a table. */
.seat-charcard .play-token.home {
    left: auto; right: -34%; top: 50%; bottom: auto;
    width: 52%; height: 52%;
    font-size: 30px;                       /* the tokens size themselves in em */
    transform: translate(0, -50%) rotate(6deg);
    filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.7));
    pointer-events: auto;
    cursor: help;
    z-index: 7;
}
/* In flight it is a fixed-position ghost, so every anchoring offset has to be neutralised. */
.play-token.in-flight {
    left: 0; top: 0; right: auto; bottom: auto;
    transform: none;
    font-size: 30px;
    pointer-events: none;
    filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.7));
}

/* ── The Ruler ───────────────────────────────────────────────────────────────
   Its two props both live on the plate: the task card each OPPONENT was dealt sits beside
   that opponent's character card, and the four Lead Tokens sit in a rail beside the Ruler's
   own. Task cards are 210x300 trim, not the 292x500 of a character card. */

.seat-charcard.taskcard {
    aspect-ratio: 210 / 300;
    background-color: #f6f3ec;
    font-family: var(--dk-font-ui);
    font-size: 7px; line-height: 1.15;
    color: #2f2a24;
    text-align: center;
    display: flex; align-items: center; justify-content: center;
    padding: 2px;
}
/* With the print loaded, the pale card ground only ever shows as the sub-pixel rim the
   contain-fit leaves around the art — invisible at plate size, a bright white frame under the
   10x hover zoom. The art carries the card's own ground, so the backing goes with it; the
   string-rendered fallback keeps it, because there the ground IS the card. */
.seat-charcard.taskcard.art { background-color: transparent; }
/* A finished task is turned face DOWN — the printed back is the "Completed +N POINTS" side,
   so the flip is the rulebook's own way of recording it, not a UI invention. */
.seat-charcard.taskcard.done { box-shadow: 0 0 0 1px rgba(120, 235, 190, 0.55), 0 2px 6px rgba(0, 0, 0, 0.6); }
.seat-charcard.taskcard.t-normal:not(.art)    { border-bottom: 3px solid #4fc3ba; }
.seat-charcard.taskcard.t-hard:not(.art)      { border-bottom: 3px solid #ef5f96; }
.seat-charcard.taskcard.t-difficult:not(.art) { border-bottom: 3px solid #8257c8; }

/* The four Lead Tokens: a RACK, one per suit, reusable every trick. It is a plate ELEMENT
   beside the character card rather than something leaning on its edge — four tokens are taller
   than a 34px card, and hanging them off it printed them over the player's name. Each suit
   keeps its slot whether or not the token is in it: an empty slot is what a token in flight
   comes home to, and a rack that reflowed as tokens left would move the landing spot in
   mid-flight. */
.seat-rack {
    flex: 0 0 auto;
    align-self: stretch;
    width: 13px;
    display: flex; flex-direction: column;
    justify-content: space-between;
    padding: 1px 0;
    pointer-events: auto;
    cursor: help;
}
.lead-slot {
    position: relative;
    flex: 1 1 0; min-height: 0;
    display: flex; align-items: center; justify-content: center;
}
/* An empty socket, so a token that is out on a card reads as MISSING rather than as a rack
   that happens to be short. Painted behind the token, which covers it when it is home. */
.lead-slot::before {
    content: '';
    position: absolute;
    width: 78%; aspect-ratio: 1;
    border-radius: 50%;
    border: 1px dashed rgba(244, 239, 226, 0.22);
}
/* The printed tokens all carry the same face under a suit wash, which is indistinguishable at
   12px — so each one gets its suit as a ring. That is the only thing a player needs to read
   off the rack at plate size. */
.lead-slot.s-r .play-token.home { box-shadow: 0 0 0 1.5px var(--s-r), 0 1px 3px rgba(0, 0, 0, 0.7); }
.lead-slot.s-b .play-token.home { box-shadow: 0 0 0 1.5px var(--s-b), 0 1px 3px rgba(0, 0, 0, 0.7); }
.lead-slot.s-g .play-token.home { box-shadow: 0 0 0 1.5px var(--s-g), 0 1px 3px rgba(0, 0, 0, 0.7); }
.lead-slot.s-k .play-token.home { box-shadow: 0 0 0 1.5px #6f6864, 0 1px 3px rgba(0, 0, 0, 0.7); }
.lead-slot .play-token.home { border-radius: 50%; }
/* In the rack a token sits IN its slot rather than hanging off a card's edge, so the shared
   .home anchoring is neutralised here. */
.seat-rack .lead-slot .play-token.home {
    position: relative;
    left: auto; right: auto; top: auto; bottom: auto;
    width: 12px; height: 12px;
    transform: rotate(-4deg);
    font-size: 30px;
}
.seat-plate.compact .seat-rack { width: 11px; }
.seat-plate.compact .seat-rack .lead-slot .play-token.home { width: 10px; height: 10px; }

/* Four suits, four tokens. Same box and the same three homes as the Revolt and Reserve
   tokens — rail, card, rail — so they share .play-token and differ only in the fallback. */
.lead-red-token, .lead-blue-token, .lead-green-token, .lead-black-token {
    position: absolute;
    right: -0.07em; top: -0.07em;
    width: 0.34em; height: 0.34em;
    border-radius: 50%;
    border: 0.02em solid rgba(255, 255, 255, 0.65);
    box-shadow: 0 0 0.12em rgba(0, 0, 0, 0.8);
    z-index: 6;
}
.lead-red-token   { background: var(--s-r); }
.lead-blue-token  { background: var(--s-b); }
.lead-green-token { background: var(--s-g); }
.lead-black-token { background: var(--s-k); }
.lead-red-token.has-art, .lead-blue-token.has-art,
.lead-green-token.has-art, .lead-black-token.has-art {
    background: center / contain no-repeat;
    border: 0; box-shadow: 0 0.02em 0.06em rgba(0, 0, 0, 0.6);
    width: 0.38em; height: 0.38em;
}

/* How close the Ruler is to its outright win: two tricks with no colour card in them.
   `◆ 0/2` cannot explain itself, so the chip carries a title — and the title only ever fires
   because of the two lines at the bottom. `.seat-plate` is pointer-events:none so the felt
   stays clickable through it, which means every child that wants a hover has to opt back IN;
   without them the tooltip was set on an element that could never receive a pointer. */
.ruler-clean {
    font-family: var(--dk-font-ui);
    font-size: 10px; font-weight: 700;
    letter-spacing: 0.04em;
    color: rgba(244, 239, 226, 0.45);
    cursor: help;
    pointer-events: auto;
}
.ruler-clean.on { color: var(--c-ruler); text-shadow: 0 0 8px rgba(148, 142, 137, 0.7); }

/* ── The Adventurer ──────────────────────────────────────────────────────────
   Its equipment is a rack of face-up item thumbnails beside its character card. FACE UP and on
   every seat, because available items are public information (p23) — the opposite of the
   Collector's pile, which is face down by rule. The slot's own colour is not decoration: a
   Crystal is worth 20 on an initial equipment card and 30 on an extra one, so which card an item
   sits on is part of the score and has to be readable at plate size. */
.seat-rack.items {
    width: auto;
    max-width: 46px;
    flex-direction: row; flex-wrap: wrap;
    align-content: flex-start;
    justify-content: flex-start;
    gap: 2px;
    cursor: pointer;
}
.item-slot {
    flex: 0 0 auto;
    width: 20px; aspect-ratio: 44 / 63;
    border-radius: 2px;
    background: #241d16 center / cover no-repeat;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
    font-family: var(--dk-font-ui);
    font-size: 5px; line-height: 1.1;
    color: #d8d0c2;
    overflow: hidden;
    position: relative;
    transition: transform .12s ease;
}
/* The three slot kinds, as a ring. Blue and red are the two printed initial equipment cards;
   gold is a trick that was won and became one. */
.item-slot.s-blue  { box-shadow: 0 0 0 1.5px #5b8fd6, 0 1px 3px rgba(0, 0, 0, 0.6); }
.item-slot.s-red   { box-shadow: 0 0 0 1.5px #d0555f, 0 1px 3px rgba(0, 0, 0, 0.6); }
.item-slot.s-extra { box-shadow: 0 0 0 1.5px rgba(212, 175, 96, 0.75), 0 1px 3px rgba(0, 0, 0, 0.6); }
.item-slot.deck    { box-shadow: 0 0 0 1px rgba(244, 239, 226, 0.28), 0 1px 3px rgba(0, 0, 0, 0.6); }
/* The discard beside it: the same pile, spent. Dimmer than the deck because it is a
   record rather than a resource, and clickable by ANYONE — a used item is public. */
.item-slot.discard {
    box-shadow: 0 0 0 1px rgba(244, 239, 226, 0.16), 0 1px 3px rgba(0, 0, 0, 0.6);
    filter: saturate(0.8) brightness(0.9);
    cursor: pointer;
}
.item-slot.discard:hover { filter: none; box-shadow: 0 0 0 1px var(--gold), 0 1px 3px rgba(0, 0, 0, 0.6); }
/* Its own hover has to beat the rack's blanket `transform: none`, or the one cell in the
   row that DOES something looks as dead as the ones that do not. */
.seat-rack.items:hover .item-slot.discard:hover { transform: translateY(-2px); }
/* An equipment card whose item was used and could not be replaced — the deck ran dry. It is a
   real end state, not a loading gap, so it reads as an empty socket rather than as a card. */
.item-slot.bare {
    background: transparent;
    border: 1px dashed rgba(244, 239, 226, 0.22);
    box-shadow: none;
}
.item-slot.bare.s-blue  { border-color: rgba(91, 143, 214, 0.5); }
.item-slot.bare.s-red   { border-color: rgba(208, 85, 95, 0.5); }
.item-slot.bare.s-extra { border-color: rgba(212, 175, 96, 0.45); }
.seat-rack.items:hover .item-slot { transform: none; }
.item-slot .pile-count {
    position: absolute; right: 0; bottom: 0;
    padding: 0 1px;
    background: rgba(0, 0, 0, 0.7);
    font-size: 6px; font-weight: 700;
}
/* The smallest useful form. Under `tiny` the whole character card is dropped, but the
   Adventurer cannot play without knowing what it is holding — so the rack survives as a row of
   dots that still says how many slots there are and how many of them are initial. */
.seat-rack.items.dots { max-width: 40px; gap: 1px; }
.seat-rack.items.dots .item-slot {
    width: 5px; height: 5px; aspect-ratio: 1;
    border-radius: 50%;
}
/* `width: auto` again: `.seat-plate.compact .seat-rack` above pins every rack to 11px for
   the Ruler's token column, and out-ranked the rule that lets this one wrap — so on a
   five-seat table the equipment stacked in one 11px column, ran out of the plate's bottom
   and lay under the seat's name. */
.seat-plate.compact .seat-rack.items { width: auto; max-width: 36px; gap: 1px; }
.seat-plate.compact .item-slot { width: 15px; }
.seat-plate.has-rack { padding: 3px 5px; }

/* An armed item prints what it would DO on each card it could be played on. */
.card-becomes {
    position: absolute;
    left: 50%; bottom: 6%;
    transform: translateX(-50%);
    padding: 0 0.14em;
    border-radius: 0.1em;
    background: rgba(20, 16, 12, 0.82);
    color: var(--gold);
    font-family: var(--dk-font-ui);
    font-size: 0.26em; font-weight: 700;
    letter-spacing: 0.03em;
    pointer-events: none;
    z-index: 5;
}
/* The Invisibility Potion, in the open trick: face down, with a cool rim so it reads as
   deliberately hidden rather than as a card that failed to load. */
.card.hidden-play { box-shadow: 0 0 0 0.05em rgba(150, 200, 255, 0.55), 0 0.3em 0.7em rgba(0, 0, 0, 0.6); }
/* ...and turned over once everyone has played. A half-turn about Y, so the card visibly FLIPS
   rather than simply becoming a different picture. */
.card.revealing { animation: tt-flip 0.42s cubic-bezier(0.3, 0, 0.2, 1); }
@keyframes tt-flip {
    from { transform: rotate(var(--card-rot, 0deg)) rotateY(90deg) scale(1.06); }
    60%  { transform: rotate(var(--card-rot, 0deg)) rotateY(0deg) scale(1.06); }
    to   { transform: rotate(var(--card-rot, 0deg)); }
}
@media (prefers-reduced-motion: reduce) { .card.revealing { animation: none; } }

/* ── The item modals ─────────────────────────────────────────────────────────
   `.item-panel` picks the initial equipment at set-up; `.equip-panel` shows any seat's
   equipment and doubles as the tray on the Adventurer's own turn. Both inherit .crown-panel's
   max-height and scroll, so a 7-slot rack on a short viewport scrolls rather than clipping. */
.item-panel { width: min(760px, 94vw); }
/* All THREE equipment screens are the Adventurer's own — the set-up pick, the rack/tray, and
   the pile of items already spent — so all three wear the Adventurer's own colour: #3bb253,
   sampled off the NAME BAND of the card render, not the duller rulebook-banner green
   (--c-adventurer) the rest of the chrome carries. The discard viewer borrows the Collector's
   contact-sheet SHELL, which is a shape and not an owner: left alone it wore the Collector's
   parchment and its purple heading, on a screen full of the Adventurer's cards.

   Addressed through the modal ids ON PURPOSE. The elements are `class="crown-panel …-panel"`
   and the two classes have equal specificity, so the shared `.crown-panel` skin — which is
   further down this file — wins on source order alone. An id here settles it wherever the rule
   happens to sit. */
#item-modal .item-panel,
#equip-modal .equip-panel,
#itemdiscard-modal .collect-panel {
    background:
        radial-gradient(110% 80% at 50% 0%, rgba(59, 178, 83, 0.26), transparent 68%),
        linear-gradient(180deg, #17301d, #0f1c12);
    border-color: rgba(59, 178, 83, 0.45);
}
#item-modal .item-panel h2,
#equip-modal .equip-panel h2,
#itemdiscard-modal .collect-panel h2 { color: #9fe2ac; }
#item-modal .item-panel .round-note, #item-modal .item-panel .collect-hint,
#equip-modal .equip-panel .round-note, #equip-modal .equip-panel .collect-hint,
#itemdiscard-modal .collect-panel .round-note,
#itemdiscard-modal .collect-panel .collect-hint {
    color: rgba(206, 236, 211, 0.74);
}
/* And the task hand-out is the Ruler's own screen, in the Ruler's grey — #9e9994, sampled off
   the NAME BAND of its card render, the same sourcing as the equipment screens' green. */
#task-modal .task-panel {
    background:
        radial-gradient(110% 80% at 50% 0%, rgba(158, 153, 148, 0.22), transparent 68%),
        linear-gradient(180deg, #2b2926, #181715);
    border-color: rgba(158, 153, 148, 0.5);
}
#task-modal .task-panel h2 { color: #d8d4cf; }
#task-modal .task-panel .round-note,
#task-modal .task-panel .collect-hint { color: rgba(224, 221, 216, 0.74); }
.equip-panel { width: min(820px, 94vw); }
.item-row {
    display: flex; flex-wrap: wrap;
    align-items: center; justify-content: center;
    gap: 10px;
    padding: 10px 0;
}
.item-row + .item-row { border-top: 1px solid rgba(244, 239, 226, 0.12); }
/* The whole row takes the drop, so the whole row lights up. */
.item-row { border-radius: var(--dk-radius); transition: background .12s ease; }
.item-row.over { background: rgba(217, 164, 78, 0.12); }
.equip-card {
    flex: 0 0 auto;
    border-radius: 4px;
    background: #2a2118 center / cover no-repeat;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
    font-family: var(--dk-font-ui);
    font-size: 10px; color: #d8d0c2;
    display: flex; align-items: center; justify-content: center;
    text-align: center; padding: 4px;
}
.equip-card.s-blue { box-shadow: 0 0 0 2px #5b8fd6, 0 2px 8px rgba(0, 0, 0, 0.6); }
.equip-card.s-red  { box-shadow: 0 0 0 2px #d0555f, 0 2px 8px rgba(0, 0, 0, 0.6); }
/* The equipment card is the drop target. It only lights for its OWN colour — a blue item
   cannot go on the red card, so an accepting highlight there would be a lie. */
.equip-card.over {
    box-shadow: 0 0 0 2px var(--gold), 0 0 26px -4px var(--gold);
    transform: translateY(-3px);
}
.equip-card.filled { outline: 1px solid rgba(120, 235, 190, 0.4); }
.equip-card { transition: transform .12s ease, box-shadow .12s ease; }
/* A picked item sits ON its equipment card, slightly askew like a card put down by hand. It is
   a copy — the row's own card stays in place, dimmed, so the row never reflows. Clicking the
   placed copy puts the item back. */
.equip-wrap { position: relative; flex: 0 0 auto; }
.equip-wrap .equip-placed {
    position: absolute;
    left: 50%; top: 54%;
    transform: translate(-50%, -50%) rotate(-4deg);
    cursor: pointer;
    animation: tt-item-land .18s ease;
}
@keyframes tt-item-land {
    from { opacity: 0; transform: translate(-50%, -64%) rotate(-4deg) scale(1.12); }
    to   { opacity: 1; transform: translate(-50%, -50%) rotate(-4deg); }
}
@media (prefers-reduced-motion: reduce) { .equip-wrap .equip-placed { animation: none; } }
.item-card {
    position: relative;
    flex: 0 0 auto;
    border-radius: 4px;
    background: #f6f3ec center / cover no-repeat;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.55);
    font-family: var(--dk-font-ui);
    font-size: 9px; line-height: 1.2;
    color: #2f2a24;
    display: flex; align-items: center; justify-content: center;
    text-align: center; padding: 4px;
    transition: transform .12s ease, box-shadow .12s ease;
}
/* A finger dragging one of these must not scroll the panel underneath it. Set inline by
   makeDragSource too — this is the belt to that pair of braces. */
.item-card.pick, .task-card.pick { touch-action: none; }
.item-card.pick { cursor: grab; }
.item-card.dragging { opacity: 0.35; cursor: grabbing; }
.item-card.pick:hover { transform: translateY(-3px); }
.item-card.held { box-shadow: 0 0 0 2px var(--gold), 0 4px 14px rgba(0, 0, 0, 0.6); }
.item-card.dim { opacity: 0.42; }
/* The unused value IS the decision, so it is printed on the card rather than left to a tooltip
   — the whole initial-equipment choice is +10 against -30. */
.item-value {
    position: absolute; right: 2px; top: 2px;
    padding: 0 3px;
    border-radius: 3px;
    background: rgba(20, 16, 12, 0.86);
    font-size: 9px; font-weight: 700;
}
.item-value.good { color: #7bd88f; }
.item-value.bad { color: #ef7b7b; }
/* Why an item is greyed out, straight from the engine's own blocker — so a legality rule is
   never restated in the UI. */
.item-why {
    position: absolute; left: 2px; right: 2px; bottom: 2px;
    padding: 2px;
    border-radius: 3px;
    background: rgba(20, 16, 12, 0.86);
    color: #e8ddc6;
    font-size: 7px; line-height: 1.1;
}
.item-empty {
    flex: 0 0 auto;
    border-radius: 4px;
    border: 1px dashed rgba(244, 239, 226, 0.3);
    display: flex; align-items: center; justify-content: center;
    text-align: center; padding: 4px;
    font-family: var(--dk-font-ui);
    font-size: 8px; color: rgba(244, 239, 226, 0.55);
}
#equip-body {
    display: flex; flex-wrap: wrap;
    align-items: flex-start; justify-content: center;
    gap: 10px;
    padding: 8px 0;
}
.equip-slot { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.equip-label {
    font-family: var(--dk-font-ui);
    font-size: 9px; letter-spacing: 0.04em;
    color: rgba(244, 239, 226, 0.62);
}
.equip-slot.s-blue  .equip-label { color: #8fb4e6; }
.equip-slot.s-red   .equip-label { color: #e08a91; }
.equip-slot.s-extra .equip-label { color: rgba(212, 175, 96, 0.85); }
/* Which equipment card an item ended the round on, in the scoring sheet — the two crystals pay
   by slot, so the sheet has to say which one. */
.item-where {
    font-size: 0.82em;
    color: rgba(244, 239, 226, 0.5);
}

/* ── Rule Avoidance, in the hand ──────────────────────────────────────────────
   A card the Ruler may play only by ignoring the must-follow rule. It is legal — and it is
   also about to change suit — so it is marked in the LEAD suit's own colour rather than the
   generic playable glow, and it takes a confirming click. */
.card.avoids::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    border: 0.055em dashed var(--avoid-colour, var(--gold));
    pointer-events: none;
    z-index: 4;
}
.card.avoids.armed {
    box-shadow: 0 0 0 0.05em var(--avoid-colour, var(--gold)),
                0 0 1.1em -0.2em var(--avoid-colour, var(--gold)),
                0 0.3em 0.7em rgba(0, 0, 0, 0.65);
}
.card.avoids.armed::after { border-style: solid; }
/* The card whose optional Lead Token is still unanswered, out on the felt. */
.card.token-pending {
    box-shadow: 0 0 0 0.05em var(--gold), 0 0 1.3em -0.2em var(--gold);
    animation: tt-token-pulse 1.1s ease-in-out infinite;
}
@keyframes tt-token-pulse {
    0%, 100% { filter: brightness(1); }
    50%      { filter: brightness(1.22); }
}
@media (prefers-reduced-motion: reduce) {
    .card.token-pending { animation: none; }
}

/* Punchboard crowns and the lead-player medallion, where the extraction found them. */
.crown-pip {
    width: 14px; height: 14px;
    vertical-align: -2px;
    margin-right: 1px;
    border-radius: 50%;
}
/* Each plate used to carry its character's hue as a 3px LEFT EDGE. Withdrawn 2026-09-17: the
   platform's Hairline Rule has no coloured one-sided edge in it, and the plate already says
   who is the Berserker twice — the printed card thumbnail on .has-card plates, and the chip
   in the character's banner colour on every plate. The edge was a third copy, and on the
   has-card plates it sat behind the thumbnail as a sliver. */

/* The Lead Player token rides the plate's LEFT edge, beside the character card and half
   outside the border — a punchboard piece laid next to the card, not a badge inside a line of
   text. It is the only part of the plate that takes a pointer, for its tooltip. */
.lead-token.on-plate {
    position: absolute;
    left: 0; top: 50%;
    width: 36px; height: 36px;
    transform: translate(-56%, -50%);
    z-index: 3;
    background: center / contain no-repeat;
    filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.75));
    pointer-events: auto;
    cursor: help;
}
.lead-token.on-plate.no-art {
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%;
    background: var(--gold);
    color: #14110c;
    font-size: 17px; font-weight: 700;
}
.seat-plate.compact .crown-pip { width: 12px; height: 12px; }
.seat-plate.compact .lead-token.on-plate { width: 29px; height: 29px; }
.seat-plate.tiny .crown-pip { width: 10px; height: 10px; }
.seat-plate.tiny .lead-token.on-plate { width: 23px; height: 23px; }

/* ── The two piles ─────────────────────────────────────────────────────────── */

/* The draw deck sits in the felt's lower-LEFT corner and the discard mirrors it on the RIGHT.
   Two stacked shadows behind the face fake the thickness; .thin drops one and .empty hides the
   whole thing. They take pointer events ONLY so their title tooltips can appear — nothing in
   either corner is clickable, and every card paints above them at z-index 60+. */
#draw-deck, #discard-deck {
    position: absolute;
    z-index: 40;
    pointer-events: auto;
    cursor: help;
    transition: opacity 0.3s ease;
}
/* ::after is inserted as the LAST child, so without explicit z-indexes the lower "thickness"
   slab paints straight over the deck face. Order it deliberately: slabs behind, face, count. */
#draw-deck::before, #draw-deck::after,
#discard-deck::before, #discard-deck::after {
    content: '';
    position: absolute; inset: 0;
    border-radius: 6px;
    background: #2a2118;
    border: 1px solid rgba(217, 164, 78, 0.28);
}
#draw-deck::before, #discard-deck::before { transform: translate(5px, 5px); opacity: 0.55; z-index: 0; }
#draw-deck::after,  #discard-deck::after  { transform: translate(2px, 2px); opacity: 0.8;  z-index: 1; }
#draw-deck.thin::before, #discard-deck.thin::before { display: none; }
#draw-deck.empty, #discard-deck.empty { opacity: 0; }
#draw-deck-face, #discard-deck-face {
    position: absolute; inset: 0;
    z-index: 2;
    border-radius: 6px;
    background: #efe7d4 center / cover no-repeat;
    border: 1px solid rgba(217, 164, 78, 0.45);
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.5);
}
#draw-deck-count, #discard-deck-count {
    position: absolute;
    z-index: 3;
    left: 50%; bottom: -9px;
    transform: translateX(-50%);
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(18, 14, 10, 0.9);
    border: 1px solid rgba(217, 164, 78, 0.35);
    font-family: var(--dk-font-ui);
    font-size: 11px; font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--gold);
}

/* The discard is the same pile, turned down: spent cards, not the live deck you draw from.
   A brightness filter dims the shared card-back art without needing a second image. */
#discard-deck-face { filter: brightness(0.62) saturate(0.7); }
#discard-deck::before, #discard-deck::after { background: #1d1710; }
#discard-deck-count { color: rgba(217, 164, 78, 0.62); border-color: rgba(217, 164, 78, 0.22); }

/* ── The Gambler's Bidding Card ────────────────────────────────────────────── */

/* The printed card, laid on the felt while the Gambler sets up. Every control on it is a hit
   area over the artwork rather than a drawn widget — the grid and the BET POINTS strip are
   already printed, and drawing a second set of them on top would be saying it twice. */
.bid-card {
    position: absolute;
    z-index: 70;
    border-radius: 8px;
    background: #efe7d4 center / 100% 100% no-repeat;
    box-shadow: 0 18px 46px rgba(0, 0, 0, 0.75);
    border: 1px solid rgba(217, 164, 78, 0.5);
    animation: bid-card-in 0.32s cubic-bezier(0.2, 0.9, 0.3, 1.05) both;
}
.bid-card.hidden { display: none; }
@keyframes bid-card-in {
    from { opacity: 0; transform: translateY(14px) scale(0.94); }
    to   { opacity: 1; transform: none; }
}
/* Without the artwork the card still has to be usable, so the cells draw their own borders. */
.bid-card.no-art { background: linear-gradient(180deg, #e8c98a, #d4a95f); }

#bid-cells { position: absolute; inset: 0; }
.bid-cell {
    position: absolute;
    margin: 0; padding: 0;
    background: transparent;
    border: 0;
    border-radius: 4px;
    color: transparent;                 /* the printed cell already says "0 bid" */
    font: inherit;
    cursor: default;
    transition: background 0.15s, box-shadow 0.15s;
}
.bid-cell.live { cursor: pointer; }
/* The printed 5 bid is a two-player cell. It has to LOOK unavailable, or it reads as a click
   that got swallowed — which is exactly how it was first reported. */
.bid-cell.two-player-only {
    cursor: help;
    background: repeating-linear-gradient(-45deg,
        rgba(40, 30, 18, 0.16) 0 6px, rgba(40, 30, 18, 0) 6px 13px);
}
.bid-cell.live:hover {
    background: rgba(255, 255, 255, 0.34);
    box-shadow: inset 0 0 0 2px var(--gold);
}
.bid-card.no-art .bid-cell {
    color: #2b2114;
    font-family: var(--tt-serif);
    font-weight: 700; font-size: 22px;
    box-shadow: inset 0 0 0 1px rgba(43, 33, 20, 0.5);
}

/* The Bidding Token, once it is on a cell. Sized off the card so it scales with it. */
.bid-token {
    position: absolute;
    width: 34%; aspect-ratio: 1;
    transform: translate(-50%, -50%);
    background: center / contain no-repeat;
    border-radius: 50%;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.6);
    display: flex; align-items: center; justify-content: center;
    font-family: var(--tt-serif);
    font-weight: 700; font-size: 20px;
    color: #2b2114;
    pointer-events: none;
    animation: bid-token-drop 0.34s cubic-bezier(0.2, 0.9, 0.25, 1.1) both;
}
.bid-token.hidden { display: none; }
@keyframes bid-token-drop {
    0%   { opacity: 0; transform: translate(-50%, -50%) scale(2.4); }
    60%  { opacity: 1; transform: translate(-50%, -50%) scale(0.92); }
    100% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

/* The stake, set on the card's own BET POINTS strip — 87.7%..100% of the artwork. */
.bid-bet {
    position: absolute;
    left: 0; right: 0; top: 87.7%; bottom: 0;
    display: flex; align-items: center; justify-content: space-between; gap: 4px;
    padding: 0 4%;
}
.bid-bet.hidden { display: none; }
/* An opaque chip, because the strip it sits on already has "BET POINTS" printed across it —
   transparent text here reads as two labels fighting for the same line. */
#bid-bet-val {
    flex: 0 1 auto;
    padding: 3px 11px;
    border-radius: 999px;
    background: rgba(30, 23, 14, 0.94);
    border: 1px solid var(--gold);
    font-family: var(--tt-serif);
    font-weight: 700; font-size: 14px;
    color: var(--gold);
    white-space: nowrap;
}
.bid-step {
    flex: 0 0 auto;
    width: 26px; height: 26px;
    border-radius: 50%;
    /* A <button> inherits no font: without this the − and + were drawn in the browser default
       next to a chip that is not. The chip itself keeps the serif — it is printed ON the
       Bidding Card — but the two controls beside it are UI. */
    font-family: var(--dk-font-ui);
    background: rgba(43, 33, 20, 0.88);
    border: 1px solid var(--gold);
    color: var(--gold);
    font-size: 17px; font-weight: 700; line-height: 1;
    cursor: pointer;
    touch-action: manipulation;
}
.bid-step:disabled { opacity: 0.32; cursor: default; }
.bid-step:not(:disabled):hover { background: var(--gold); color: #2b2114; }

/* ── Seat plates ───────────────────────────────────────────────────────────── */

.seat-plate {
    position: absolute;
    z-index: 90;
    display: flex; flex-direction: column; gap: 4px;
    padding: 7px 12px;
    border-radius: 10px;
    background: rgba(20, 16, 12, 0.82);
    border: 1px solid rgba(217, 164, 78, 0.22);
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
    pointer-events: none;
    white-space: nowrap;
    transition: border-color 0.2s, box-shadow 0.2s;
}
.seat-plate.active {
    border-color: var(--gold);
    box-shadow: 0 0 16px rgba(217, 164, 78, 0.45), 0 4px 14px rgba(0, 0, 0, 0.45);
}
.seat-plate.has-card { flex-direction: row; align-items: center; gap: 9px; padding: 6px 12px 6px 7px; }
.seat-info { display: flex; flex-direction: column; gap: 4px; min-width: 0; }

/* The seat's drafted character card. Thumbnail on the plate, readable on hover — the plate
   itself is pointer-events:none so the felt stays clickable through it, which means the card
   has to opt back IN to receiving a pointer. */
.seat-charcard {
    position: relative;
    flex: 0 0 auto;
    width: 34px;
    aspect-ratio: 292 / 500;
    border-radius: 4px;
    background: rgba(0, 0, 0, 0.3) center / contain no-repeat;
    border: 1px solid rgba(217, 164, 78, 0.35);
    pointer-events: auto;
    cursor: zoom-in;
    transition: border-color 0.15s;
    display: flex; align-items: center; justify-content: center;
    font-family: var(--tt-serif); font-size: 11px; font-weight: 700;
    color: var(--paper-dim);
}
/* Reading a plate card is the FLOATING zoom (showCharZoom & friends in game.js) — the same
   direct popup the item modals use — so the thumbnail itself stays put and only announces
   that it will answer a hover. */
.seat-charcard:hover { border-color: rgba(217, 164, 78, 0.75); }
/* The Revolt token is played onto the Resistance's character card, where the punchboard token
   goes on the table. The floating zoom shows the clean print, so the token stays put here. */
.seat-charcard .revolt-token {
    left: 50%; top: 46%; right: auto; bottom: auto;
    width: 62%; height: 62%;
    font-size: 34px;                       /* .revolt-token sizes itself in em */
    transform: translate(-50%, -50%) rotate(-9deg);
    transition: opacity 0.18s;
}

/* The Gambler's Bidding Card, beside its character card. Same box — the two printed cards are
   the same 292x500 trim, so nothing needs re-sizing. */
.seat-charcard.bidcard { background-color: #efe7d4; }

/* The Berserker's set-aside pile. Card-back art rather than a character card, so it reads as a
   deck; it dims once both cards have been bought and there is nothing left to spend a crown on.
   Playing-card trim, not character trim — 63x88 rather than 70x120. */
.seat-charcard.berspile { aspect-ratio: 63 / 88; background-color: #2a2118; }
.seat-charcard.berspile.spent { opacity: 0.4; }

/* The Collector's collected cards: a FACE-DOWN pile, the way it sits on a real table — "place
   the cards you have collected face down in front of you" (p26). The count is open and the
   contents are not, so only your OWN pile takes a pointer. Playing-card trim like the
   Berserker's pile. */
/* OUTSIDE the plate, hanging off its edge like a pile of cards on the table beside a player —
   not another chip in the name tag. In the row it shared with the character card, the Reserve
   Token, the task card and the score, the Collector's plate was the most crowded at the table.
   game.js picks the side once the plate has landed (`.pile-left` = whichever has more room),
   and drops the pile to the corner when the Lead Player token already owns that edge. */
.seat-charcard.collectpile {
    position: absolute;
    left: 100%; top: 50%;
    margin-left: 6px;
    transform: translateY(-50%);
    z-index: 3;
    aspect-ratio: 63 / 88;
    background-color: #2a2118;
    background-size: cover;
    cursor: default;
    box-shadow: 2px 2px 0 -1px rgba(20, 16, 10, 0.9), 4px 4px 0 -2px rgba(20, 16, 10, 0.7);
}
.seat-plate.pile-left .seat-charcard.collectpile {
    left: auto; right: 100%;
    margin-left: 0; margin-right: 6px;
}
/* The Lead Player token sits half outside the LEFT edge, so a pile sent there shares the wall
   with it — the pile drops to the bottom corner rather than landing on top of it. */
.seat-plate.pile-left.has-lead .seat-charcard.collectpile {
    top: auto; bottom: 0;
    transform: none;
}
.seat-charcard.collectpile.mine { cursor: pointer; }
.seat-charcard.collectpile.spent {
    background-color: #2a2118;
    box-shadow: none;
    opacity: 0.45;
}
/* No hover zoom (none is wired up in game.js): a stack of BACKS by rule (p26), so enlarging it
   shows a bigger picture of nothing. The count is on it and a click opens the pile for real. */
.seat-charcard.collectpile .pile-count {
    background: rgba(18, 14, 10, 0.94);
    border-color: var(--c-collector);
    color: var(--c-collector);
}
.seat-charcard .pile-count {
    position: absolute;
    right: -4px; bottom: -5px;
    min-width: 13px;
    padding: 0 3px;
    border-radius: 999px;
    background: rgba(18, 14, 10, 0.94);
    border: 1px solid rgba(217, 164, 78, 0.5);
    font-family: var(--dk-font-ui);
    font-size: 9px; font-weight: 700; line-height: 13px;
    text-align: center;
    color: var(--gold);
}
/* The stake, in point chips, on the plate card's BET POINTS strip (87.7%..100% of the art).
   They overlap deliberately — a stack of chips, not a row of icons. */
.point-chips {
    position: absolute;
    left: 5%; right: 5%; top: 86%; bottom: 1%;
}
.point-chip {
    position: absolute;
    top: 50%;
    height: 96%; aspect-ratio: 1;
    transform: translateY(-50%);
    background: center / contain no-repeat;
    border-radius: 50%;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
    display: flex; align-items: center; justify-content: center;
    font-size: 5px; font-weight: 700; color: #2b2114;
}
.point-chip.p50 { background-color: #d9a44e; }
.point-chip.p10 { background-color: #cfc7b4; }

.bid-pin {
    position: absolute;
    width: 34%; aspect-ratio: 1;
    transform: translate(-50%, -50%);
    background: center / contain no-repeat;
    border-radius: 50%;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
    display: flex; align-items: center; justify-content: center;
    font-size: 7px; font-weight: 700; color: #2b2114;
}
/* Live only during the Revolt trick itself; afterwards the token stays put but stops shouting. */
.seat-charcard .revolt-token.spent { opacity: 0.55; filter: saturate(0.5); }
.seat-plate.compact .seat-charcard { width: 28px; }

.seat-name {
    /* The one plate line that is NOT a printed component: a player's name is chrome, so it
       takes the UI face while the trick token and the character thumbnail keep the serif. */
    font-family: var(--dk-font-ui);
    font-size: 16px; font-weight: 700;
    color: var(--paper);
    display: flex; align-items: center; gap: 6px;
}
.seat-char {
    font-size: 11px; font-weight: 700;
    letter-spacing: 0.08em; text-transform: uppercase;
    padding: 1px 7px; border-radius: 999px;
    color: #14110c;
}
.seat-char.c-king       { background: var(--c-king); color: #fff; }
.seat-char.c-gambler    { background: var(--c-gambler); }
.seat-char.c-resistance { background: var(--c-resistance); }
.seat-char.c-hermit     { background: var(--c-hermit); color: #fff; }
.seat-char.c-collector  { background: var(--c-collector); }
.seat-char.c-berserker  { background: var(--c-berserker); }
.seat-char.c-ruler      { background: var(--c-ruler); }
.seat-char.c-adventurer { background: var(--c-adventurer); }
.seat-char.c-none       { background: rgba(255, 255, 255, 0.14); color: var(--paper-dim); }

.seat-stats {
    display: flex; align-items: center; gap: 8px;
    font-size: 13px; color: var(--paper-dim);
    font-variant-numeric: tabular-nums;
}
/* Tricks are counted in CARDS, so the counter is a little face-down card with the number
   printed on it rather than the words "Tricks 2". The number is black because it is printed
   ON the back art, not floated over the plate. */
.seat-tricks {
    display: inline-flex; align-items: center; justify-content: center;
    width: 15px; height: 21px;
    border-radius: 3px;
    background: var(--paper-dim) center / cover no-repeat;
    border: 1px solid rgba(0, 0, 0, 0.45);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
    font-family: var(--tt-serif);
    font-size: 12px; font-weight: 700; line-height: 1;
    /* Cormorant's default figures are OLD-STYLE, and an old-style zero is an "o" — the
       counter every plate opens the round with read as a letter. Lining figures, and the
       same call on the badge below so "1/3" is not a lower-case ligature either. */
    font-variant-numeric: lining-nums;
    color: #14110c;
    text-shadow: 0 0 3px rgba(255, 255, 255, 0.85), 0 0 6px rgba(255, 255, 255, 0.7);
    cursor: help;
    pointer-events: auto;
}
.seat-tricks.none { opacity: 0.42; }
.seat-plate.compact .seat-tricks { width: 13px; height: 18px; font-size: 10.5px; }
.seat-plate.tiny .seat-tricks { width: 11px; height: 16px; font-size: 9px; }
.seat-points { font-weight: 700; color: var(--gold); }
.seat-bid {
    padding: 0 6px; border-radius: 999px;
    background: rgba(231, 186, 79, 0.18);
    color: #f0cf88; font-weight: 700;
}
.crown-run { letter-spacing: 1px; }
.crown-g { color: var(--gold); }
.crown-b { color: var(--crown-black); }

.seat-plate.compact { padding: 5px 9px; gap: 2px; }
.seat-plate.compact .seat-name { font-size: 13.5px; }
.seat-plate.compact .seat-char { font-size: 9.5px; padding: 1px 5px; }
.seat-plate.compact .seat-stats { font-size: 11.5px; gap: 6px; }
.seat-plate.tiny { padding: 3px 7px; }
.seat-plate.tiny .seat-name { font-size: 11px; }
.seat-plate.tiny .seat-char { font-size: 8px; padding: 0 4px; }
.seat-plate.tiny .seat-stats { font-size: 10px; gap: 4px; }

/* ── Standings panel ───────────────────────────────────────────────────────── */

#rank-panel {
    position: fixed;
    top: 62px; right: 14px;
    z-index: 200;
    min-width: 196px;
    padding: 8px 10px 9px;
    border-radius: 10px;
    background: rgba(20, 16, 12, 0.86);
    border: 1px solid rgba(217, 164, 78, 0.22);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
    pointer-events: none;
    font-family: var(--dk-font-ui);
    display: none;
}
.rank-title {
    font-family: var(--dk-font-display);
    font-size: 12px; font-weight: 700;
    letter-spacing: 0.16em; text-transform: uppercase;
    color: var(--gold);
    margin-bottom: 5px;
}
#rank-panel table { border-collapse: collapse; width: 100%; }
#rank-panel th {
    font-size: 10px; font-weight: 700;
    letter-spacing: 0.08em; text-transform: uppercase;
    color: rgba(244, 239, 226, 0.45);
    padding: 1px 5px; text-align: left;
}
#rank-panel td { padding: 2px 5px; font-size: 13px; color: var(--paper-dim); }
#rank-panel .rank-num { text-align: right; font-variant-numeric: tabular-nums; }
#rank-panel .rank-pts { color: var(--gold); font-weight: 700; }
#rank-panel tbody tr + tr td { border-top: 1px solid rgba(217, 164, 78, 0.12); }
#rank-panel tr.me td { background: rgba(217, 164, 78, 0.12); }
#rank-panel tr.me .rank-name { color: #f0cf88; font-weight: 700; }

/* ── Action bar (bids, bets, revolt, exchange) ─────────────────────────────── */

#action-bar {
    position: fixed;
    z-index: 150;
    transform: translate(-50%, 0);
    display: none;
    align-items: center; justify-content: center;
    gap: 8px; flex-wrap: wrap;
    max-width: 92vw;
}
#action-bar.on { display: flex; }
.act-btn {
    min-height: 44px; min-width: 44px;
    padding: 8px 16px;
    border-radius: 10px;
    border: 1px solid rgba(217, 164, 78, 0.55);
    background: rgba(24, 19, 14, 0.9);
    color: var(--paper);
    font-family: var(--dk-font-display);
    font-size: 16px; font-weight: 700; letter-spacing: 0.02em;
    cursor: pointer; touch-action: none;
    transition: background 0.15s, border-color 0.15s, filter 0.15s;
}
.act-btn:hover:not(:disabled) { background: rgba(217, 164, 78, 0.24); }
.act-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.act-btn.primary {
    background: linear-gradient(180deg, #e9bd6a, var(--gold));
    color: #14110c;
    border-color: #f0cd8a;
    box-shadow: 0 2px 12px rgba(217, 164, 78, 0.30);
}
.act-btn.primary:hover { filter: brightness(1.12); }
/* The shared dialog button takes the same caption face as the action bar, so the two kinds of
   button in the game do not disagree. Scoped INSIDE a panel — the same scoping Dalmuti uses for
   its own display font — so the start screen and every other use of .primary-btn on the platform
   are left exactly as core/deckades.css draws them. */
.panel .primary-btn,
.crown-panel .primary-btn,
#select-modal .primary-btn {
    font-family: var(--dk-font-display);
    letter-spacing: 0.02em;
}
.act-btn.revolt {
    background: rgba(243, 145, 57, 0.16);
    border-color: var(--c-resistance);
    color: #ffb877;
    letter-spacing: 0.1em;
}
.act-btn.revolt.on {
    background: var(--c-resistance);
    color: #1a1005;
    box-shadow: 0 0 18px rgba(243, 145, 57, 0.6);
}

/* ── Character draft ───────────────────────────────────────────────────────── */

/* core/deckades.css hides an .overlay with display:none, which cannot be transitioned. This
   does NOT restyle the shared rule — it adds a fade on top of it, scoped to this game's own
   modal: JS drops .hidden first (so the element lays out), then adds .shown a frame later.
   Closing runs it backwards and only re-adds .hidden once the fade has finished. */
#select-modal {
    opacity: 0;
    transition: opacity 0.3s ease;
}
#select-modal.shown { opacity: 1; }

/* The draft is a decision ABOUT the hand you are holding, so the hand has to be readable while
   you make it. The panel moves to the TOP of the scrim and a read-only copy of the hand hangs
   underneath it — the real fan is behind the scrim, and copying it is cheaper and far safer
   than punching a hole in the shared overlay to reveal the live one.
   `:not(.hidden)` is load-bearing: `#select-modal` outranks `.overlay.hidden`, so declaring
   `display` unconditionally here would stop the modal ever hiding again. */
#select-modal:not(.hidden) {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    row-gap: 12px;
}
/* The flex line now owns the height, and the panel is the part that gives way: the hand strip
   must never be the thing that gets pushed off the bottom. */
#select-modal .select-panel {
    flex: 0 1 auto;
    min-height: 0;
    max-height: none;
}
.select-hand {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    pointer-events: none;           /* read-only, belt and braces — nothing binds a click either */
}
.select-hand:empty { display: none; }
.select-hand-row { display: flex; justify-content: center; gap: 6px; }
/* .card is absolutely positioned on the table; in this strip it is a flex item. It must stay
   POSITIONED — the face and pips inside it are `position:absolute; inset:0`, so a static card
   hands them to the nearest positioned ancestor, which is the fixed full-screen overlay. */
.select-hand .card {
    position: relative;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.6);
}
.select-hand-label {
    font-size: 12px; font-weight: 700;
    letter-spacing: 0.09em; text-transform: uppercase;
    color: rgba(244, 239, 226, 0.5);
}
#select-modal .select-panel {
    transform: scale(0.94) translateY(10px);
    transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.3, 1.05);
}
#select-modal.shown .select-panel { transform: none; }
@media (prefers-reduced-motion: reduce) {
    #select-modal, #select-modal .select-panel { transition: none; }
}

/* ── Character cards ───────────────────────────────────────────────────────── */

/* The draft's own box. Deliberately NOT .panel: five printed character cards need ~1000px and
   the shared shell is min(440px, 94vw), which may not be re-declared here. Same visual
   language, its own width. */
/* The draft and the round-end carousel are THE SAME KIND OF SCREEN — the two places in the
   game where the table stops and everyone reads — so they share a ground: the King's cloak
   blue, taken down, under beige rather than the felt-side panels' warm gold. Eight character
   cards in eight colours need a neutral behind them; the brown pushed the reds and oranges
   forward and left the blues sitting in a hole. */
/* A COLUMN whose grid is the only part that scrolls. The panel used to scroll as a whole, and
   once the confirm button moved inside it that put the button inside the scroll: with the
   advanced roster the six cards wrap to two rows, the panel overflows, and the button sat
   ~200px below the frame's bottom edge. Worse, every rebuild reset scrollTop, so the distance
   between the cards and the button changed on its own each time a seat picked. Only #select-grid
   scrolls now, so the title above it and the button below it are always where you left them. */
.select-panel {
    position: relative;
    display: flex;
    flex-direction: column;
    width: min(1440px, 97vw);
    max-height: calc(100dvh - 16px);
    overflow: hidden;
    padding: 18px 20px 20px;
    border-radius: 16px;
    background:
        radial-gradient(120% 80% at 50% 0%, rgba(77, 102, 150, 0.42), transparent 66%),
        linear-gradient(180deg, var(--tt-blue) 0%, var(--tt-blue-deep) 100%);
    border: 1px solid rgba(198, 178, 142, 0.30);
    box-shadow: 0 12px 44px rgba(0, 0, 0, 0.7);
    text-align: center;
    color: var(--tt-beige);
    font-family: var(--dk-font-ui);
}
.select-panel h2 {
    font-family: var(--dk-font-display);
    font-weight: 700; font-size: 24px;
    color: var(--tt-beige);
    letter-spacing: 0.04em;
    margin-bottom: 4px;
}
/* The two lines under the title: the choosing order, and whose turn it is. Beige at two
   strengths rather than gold — on this ground gold read as a third accent competing with the
   cards' own colours. */
#select-order { color: rgba(226, 216, 195, 0.72); }
.select-status { color: rgba(226, 216, 195, 0.62); }
.select-status.mine { color: var(--tt-beige); }

/* The vertical padding is what a hovered card scales INTO — .select-panel scrolls, so a card
   that grows past the grid's own box gets clipped by the scroll container. */
.char-grid {
    display: flex; flex-wrap: wrap;
    justify-content: center; gap: 14px;
    margin: 6px 0 4px;
    padding: 26px 8px 30px;
}
/* The one scrolling part of the draft (see .select-panel). `min-height: 0` because a flex item
   defaults to min-content and would refuse to shrink below its two rows of cards, which is how
   an overflowing child pushes a flex column past its own max-height. */
#select-grid {
    flex: 0 1 auto;
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
}
.char-card {
    position: relative;
    width: 250px;
    border-radius: 12px;
    overflow: hidden;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.16);
    text-align: left;
    cursor: pointer;
    touch-action: manipulation;
    transition: transform 0.18s cubic-bezier(0.2, 0.8, 0.3, 1.2), box-shadow 0.18s,
                border-color 0.15s, opacity 0.15s;
    color: var(--paper);
    font-family: var(--dk-font-ui);
    padding: 0;
}
/* Zoom on hover — the printed ability text is 6 pt on a 70 mm card, so at grid size it is
   decoration. This is what makes it readable without opening anything. Taken cards zoom too:
   knowing what an opponent just took matters as much as reading a free one. */
/* z-index has to clear everything else inside the panel — in particular the status line
   below the grid, which a zoomed card overhangs and must completely cover. */
/* Framed in its own colour. The printed art fills the card, so the frame is the only place
   the character's hue can carry at a glance — and it is what the seat chip echoes later. */
.char-card.c-king       { border-color: var(--c-king); }
.char-card.c-gambler    { border-color: var(--c-gambler); }
.char-card.c-resistance { border-color: var(--c-resistance); }
.char-card.c-hermit     { border-color: var(--c-hermit); }
.char-card.c-collector  { border-color: var(--c-collector); }
.char-card.c-berserker  { border-color: var(--c-berserker); }
.char-card.c-ruler      { border-color: var(--c-ruler); }
.char-card.c-adventurer { border-color: var(--c-adventurer); }
.char-card:hover {
    transform: translateY(-6px) scale(1.3);
    z-index: 40;
    box-shadow: 0 18px 44px rgba(0, 0, 0, 0.75);
}
.char-card.c-king:hover       { box-shadow: 0 18px 44px rgba(0, 0, 0, 0.75), 0 0 26px rgba(80, 119, 187, 0.55); }
.char-card.c-gambler:hover    { box-shadow: 0 18px 44px rgba(0, 0, 0, 0.75), 0 0 26px rgba(231, 186, 79, 0.55); }
.char-card.c-resistance:hover { box-shadow: 0 18px 44px rgba(0, 0, 0, 0.75), 0 0 26px rgba(243, 145, 57, 0.55); }
.char-card.c-hermit:hover     { box-shadow: 0 18px 44px rgba(0, 0, 0, 0.75), 0 0 26px rgba(96, 100, 173, 0.55); }
.char-card.c-collector:hover  { box-shadow: 0 18px 44px rgba(0, 0, 0, 0.75), 0 0 26px rgba(111, 196, 203, 0.55); }
.char-card.c-berserker:hover  { box-shadow: 0 18px 44px rgba(0, 0, 0, 0.75), 0 0 26px rgba(238, 121, 146, 0.55); }
.char-card.c-ruler:hover      { box-shadow: 0 18px 44px rgba(0, 0, 0, 0.75), 0 0 26px rgba(148, 142, 137, 0.55); }
.char-card.c-adventurer:hover { box-shadow: 0 18px 44px rgba(0, 0, 0, 0.75), 0 0 26px rgba(99, 160, 92, 0.55); }
/* The lift and the shadow are the whole hover affordance — a gold frame on the zoomed card
   reads as "chosen", which is what .char-card.pick below actually means. */
/* …and with a character sheet to float (game.js adds .detail when one is deployed), the lift
   IS the whole affordance: scaling the printed card up was only ever a way to read 6 pt type,
   and the sheet does that properly. Growing it as well would just push the sheet's anchor
   around and cover the neighbours for nothing. */
.char-card.detail:hover { transform: translateY(-6px); }
/* Taken or banned. Carried as a class because these keep firing mouse events — the sheet must
   still open on a card an opponent has taken — so they are aria-disabled, not :disabled. */
.char-card:disabled, .char-card.taken { opacity: 0.5; cursor: default; }
.char-card:disabled:hover, .char-card.taken:hover { opacity: 0.92; }
/* Free, but another seat is choosing right now. */
.char-card.idle { cursor: default; }
.char-card.pick { border-color: var(--gold); box-shadow: 0 0 20px var(--dk-accent-soft); }
.char-head {
    display: flex; align-items: baseline; justify-content: space-between;
    padding: 6px 10px;
    font-family: var(--dk-font-display);
    font-weight: 700; font-size: 17px;
    letter-spacing: 0.04em;
    color: #fff;
}
.char-head .char-code { font-size: 12px; opacity: 0.75; }
.char-card.c-king       .char-head { background: var(--c-king); }
.char-card.c-gambler    .char-head { background: var(--c-gambler); color: #2b2110; }
.char-card.c-resistance .char-head { background: var(--c-resistance); color: #2b1608; }
.char-card.c-hermit     .char-head { background: var(--c-hermit); }
.char-card.c-collector  .char-head { background: var(--c-collector); color: #113034; }
.char-card.c-berserker  .char-head { background: var(--c-berserker); color: #33121b; }
.char-card.c-ruler      .char-head { background: var(--c-ruler); color: #2b2724; }
.char-card.c-adventurer .char-head { background: var(--c-adventurer); color: #10240e; }
.char-art {
    height: 96px;
    background: rgba(0, 0, 0, 0.25) center / cover no-repeat;
    display: flex; align-items: center; justify-content: center;
    font-size: 44px;
}
/* With the printed card extracted, the whole card IS the art: 70 x 120 mm trim, shown whole
   (contain, never cover) so the scoring ladder along its bottom edge is never cropped off. */
.char-card.art .char-art {
    height: auto;
    aspect-ratio: 292 / 500;
    background: rgba(0, 0, 0, 0.2) center / contain no-repeat;
}
.char-wants {
    padding: 5px 9px 8px;
    font-size: 10.5px; line-height: 1.35;
    color: rgba(244, 239, 226, 0.75);
}
.char-wants b { color: var(--gold); font-weight: 700; }
.char-motto {
    padding: 5px 10px 0;
    font-size: 10.5px; font-style: italic;
    color: rgba(244, 239, 226, 0.6);
    min-height: 26px;
}
.char-body { padding: 2px 10px 8px; font-size: 11px; line-height: 1.4; color: rgba(244, 239, 226, 0.85); }
.char-ladder {
    display: flex; flex-wrap: wrap; gap: 3px;
    padding: 0 10px 9px;
}
.char-rung {
    font-size: 10px; font-weight: 700;
    padding: 1px 5px; border-radius: 4px;
    background: rgba(0, 0, 0, 0.35);
    color: rgba(244, 239, 226, 0.8);
    font-variant-numeric: tabular-nums;
}
.char-rung.good { color: #8ce0a4; }
.char-rung.bad { color: #f0907a; }
.char-rung.win { color: #ffd98a; background: rgba(217, 164, 78, 0.22); }
/* ── "Selected by …" stamp ─────────────────────────────────────────────────── */

.char-taken {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    background: rgba(10, 8, 6, 0.62);
    text-align: center; padding: 8px;
}
.char-taken.fresh { animation: veil-in 0.34s ease-out both; }

/* A rubber stamp, not a label: rotated, ring-bordered, ink-coloured, and slammed on. */
.char-stamp {
    display: inline-block;
    transform: rotate(-13deg);
    padding: 9px 14px;
    border: 3px solid var(--gold);
    border-radius: 7px;
    font-family: var(--tt-serif);
    font-weight: 700;
    font-size: 17px; line-height: 1.15;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--gold);
    background: rgba(24, 17, 9, 0.6);
    box-shadow: 0 3px 14px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(255, 231, 178, 0.28);
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
}
.char-taken.fresh .char-stamp { animation: stamp-in 0.46s cubic-bezier(0.2, 0.9, 0.25, 1) both; }

@keyframes veil-in { from { opacity: 0; } to { opacity: 1; } }
/* Overshoot slightly past 1 on the way down, then settle — reads as impact. */
@keyframes stamp-in {
      0% { opacity: 0; transform: rotate(-13deg) scale(2.8); }
     50% { opacity: 1; transform: rotate(-13deg) scale(0.9); }
     72% { transform: rotate(-13deg) scale(1.08); }
    100% { opacity: 1; transform: rotate(-13deg) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
    .char-taken.fresh, .char-taken.fresh .char-stamp { animation: none; }
}

/* Whose turn it is, under the grid. Explicitly positioned at z-index 0 so it can never paint
   over a hovered card that overhangs it — a zoomed card must fully cover this line. */
.select-status {
    position: relative;
    z-index: 0;
    min-height: 20px;
    font-size: 14px; font-weight: 700;
    letter-spacing: 0.03em;
    /* the colour is set with the rest of the panel's beige, up with .select-panel */
}
/* Same for the title and order line above the grid. */
#select-title, #select-order { position: relative; z-index: 0; }

/* Confirm. The draft ARMS a card and this is what spends it, so it sits below the status line
   where the answer to "am I choosing right now" already is. Disabled until a card is armed —
   the caption is the prompt in that state — and gone entirely on somebody else's turn, because
   a permanently dead button on four screens out of five is just noise. */
/* A SIBLING of the panel, not a footer inside it. The panel scrolls — eight character cards at
   250px each — and a sticky button inside a scroll box lies over whatever is behind it, so on a
   short screen it covered the very cards it was asking about and they could not be clicked.
   Out here the flex column gives it its own line under the panel and over the hand strip. */
#select-confirm {
    flex: 0 0 auto;
    /* As wide as its caption, not as wide as the panel: at 1440px a full-bleed bar under five
       character cards reads as a page footer rather than as the button that spends your pick.
       Inside the panel now, so it is centred by hand — .select-panel is a plain block. */
    display: block;
    width: fit-content;
    min-width: 220px;
    max-width: 94vw;
    margin: 10px auto 2px;
    padding: 0 32px;
    box-shadow: 0 8px 22px rgba(10, 14, 22, 0.6);
}
#select-confirm.hidden { display: none; }
/* Dimmed as a SOLID, never with opacity: it sits on the dark scrim between the panel and the
   hand, and a translucent bar there reads as a rendering fault. Blue-grey rather than the warm
   brown it started as — it belongs to the cold ground everything else on this screen uses. */
#select-confirm:disabled {
    background: rgba(31, 42, 65, 0.97);
    border-color: rgba(198, 178, 142, 0.32);
    color: rgba(226, 216, 195, 0.55);
    cursor: default;
}

/* ── The confirmation gate ─────────────────────────────────────────────────── */

/* "Waiting for other players to confirm…", under the Continue button of every screen that
   moves the match on. It reserves NO height when empty: these dialogs are already tall enough
   to scroll on a phone, and a blank line under the button in solo play would be a bug. */
.table-wait {
    display: none;
    margin-top: 8px;
    font-size: 13px; font-weight: 700;
    letter-spacing: 0.03em;
    color: var(--gold);
    opacity: 0.85;
    animation: table-wait-pulse 1.8s ease-in-out infinite;
}
.table-wait.on { display: block; }
@keyframes table-wait-pulse {
    0%, 100% { opacity: 0.85; }
    50%      { opacity: 0.45; }
}
@media (prefers-reduced-motion: reduce) {
    .table-wait { animation: none; }
}

/* ── The Round Card, on the table ────────────────────────── */

/* A component, not a dialog: it sits beside the standings for the whole match with the crowns
   that are still to be won printed on it, and at the end of a round the earned ones physically
   fly off it onto their winners' seat plates. Positioned by renderRoundCard() in game.js, whose
   rectangle is folded into the same obstacle set as the standings. */
.round-card {
    position: absolute;
    z-index: 120;
    display: none;
    border-radius: 6px;
    background: #efe7d4 center / 100% 100% no-repeat;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.6);
    border: 1px solid rgba(217, 164, 78, 0.35);
    pointer-events: auto;
    cursor: help;
    /* The card is printed sideways — the round numbers run up its right edge. A quarter-turn
       CLOCKWISE stands them upright and lays the rounds out left-to-right as 1, 2, 3. The crown
       spots are positioned in fractions of the element, so they turn with it and need no
       re-measuring; renderRoundCard() converts between the layout box and this visual one. */
    transform: rotate(90deg);
    transform-origin: center;
}
.round-card.no-art { background: linear-gradient(180deg, #e9e2d2, #cfc7b4); }

/* One printed crown spot. Positioned from CROWN_SPOTS in game.js — the same numbers the flight
   animation starts from, so the crown always leaves the spot it is drawn on. */
.crown-pip-fly {
    position: absolute;
    aspect-ratio: 1;
    transform: translate(-50%, -50%);
    background: center / contain no-repeat;
    border-radius: 50%;
    transition: opacity 0.25s, box-shadow 0.25s;
}
/* The card itself is turned a quarter-turn CLOCKWISE to stand its printed round numbers up, and
   its children turn with it — which left every crown lying on its side. Counter-rotating the
   pips puts the crowns back upright without disturbing the spots they are measured onto. Scoped
   to the card: the flying ghost is re-parented outside it and must not inherit this. */
.round-card .crown-pip-fly { transform: translate(-50%, -50%) rotate(-90deg); }
.crown-pip-fly.gold  { background-color: #d9a44e; }
.crown-pip-fly.black { background-color: #6b6459; }
/* The row for the round being played, so the card says WHICH crowns are in contention. */
.crown-pip-fly.now:not(.taken) { box-shadow: 0 0 0 2px rgba(217, 164, 78, 0.85); }
/* Once its crown has flown the spot must read as EMPTY. Fading the token out is not enough —
   the crown is PRINTED on the card underneath it, so the spot has to be covered by a socket
   in the card's own stone colour rather than made transparent. */
.crown-pip-fly.taken {
    background-image: none !important;
    background-color: #ded7c6;
    box-shadow: inset 0 0 0 2px rgba(90, 80, 62, 0.45), inset 0 3px 7px rgba(90, 80, 62, 0.35);
    opacity: 1;
    filter: none;
}
.crown-pip-fly.in-flight { transform: none; box-shadow: 0 8px 22px rgba(0, 0, 0, 0.7); }
/* The plate a crown is on its way to. */
.seat-plate.crown-lit {
    border-color: var(--gold);
    box-shadow: 0 0 26px rgba(217, 164, 78, 0.6), 0 4px 14px rgba(0, 0, 0, 0.45);
}

/* ── The Collector ─────────────────────────────────────── */

/* The pile viewer. A contact sheet, closed by a click anywhere — there is nothing to interact
   with inside it, so a stray click that did nothing would read as the dialog being stuck. */
/* Every panel that fades in on .shown. #task-modal was missing from this list, so its
   .shown class did nothing at all and the panel cut in hard. */
#collect-modal, #combo-modal, #task-modal,
#item-modal, #equip-modal,
#char-modal { opacity: 0; transition: opacity 0.3s ease; }
#collect-modal.shown, #combo-modal.shown, #task-modal.shown,
#item-modal.shown, #equip-modal.shown,
#char-modal.shown { opacity: 1; }

/* A line the sheet SHOWS but does not add in. SHIROHATA and the Gambler's set-up payment
   are both already in the running score by the time the round is scored, so summing them
   here would make the sheet disagree with the round table's own gain column. */
.round-table tr.sheet-info td { opacity: 0.6; font-style: italic; }
/* ── The Ruler's task hand-out ────────────────────────────────────────────────
   The whole deck on top in TWO FIXED rows, one slot per opponent below, and the Ruler's own
   hand under the panel. The printed six read as 3 + 3; the full sixteen read as 8 + 8, which is
   the only reason that panel is wider.
   Rows rather than a wrap, because the shape has to survive a hand-out: a dealt card stays
   exactly where it was and is disabled, so nothing moves out from under the pointer. */
.task-panel { width: min(880px, 94vw); }
/* Addressed through the modal id ON PURPOSE — see the item screens above: the shared
   `.crown-panel` skin sits further down this file and beats a bare `.task-panel` on source
   order alone, so its 22 px padding would simply ignore the tightening. */
#task-modal .task-panel { padding: 16px 24px 18px; }
.task-panel.wide { width: min(1180px, 96vw); }
/* The panel sits HIGH with the hand strip under it — the same column the character draft uses.
   `.overlay`'s grid centring would put the panel in the middle of the screen and hang the strip
   off the bottom edge. `:not(.hidden)` is load-bearing: an id outranks `.overlay.hidden`, so
   declaring `display` unconditionally here would stop the modal ever hiding again. */
#task-modal:not(.hidden) {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    row-gap: 10px;
}
/* The flex line owns the height and the PANEL is the part that gives way: the hand strip must
   never be the thing pushed off the bottom. The panel keeps .crown-panel's own overflow-y, which
   is exactly what the fit pass in renderTaskModal() measures its correction against. */
#task-modal .task-panel {
    flex: 0 1 auto;
    min-height: 0;
    max-height: none;
}
.task-deck {
    display: flex; flex-direction: column;
    align-items: center;
    gap: 8px;
    margin: 8px 0 4px;
    min-height: 40px;
}
.task-row { display: flex; justify-content: center; gap: 8px; }
.task-slots {
    display: flex; flex-wrap: wrap;
    justify-content: center; align-items: stretch;
    gap: 10px;
    margin: 10px 0 4px;
    padding-top: 10px;
    border-top: 1px solid rgba(217, 164, 78, 0.22);
}
.task-slot {
    display: flex; flex-direction: column; align-items: center;
    gap: 6px;
    padding: 8px 10px 10px;
    border-radius: var(--dk-radius);
    border: 1px solid rgba(217, 164, 78, 0.18);
    background: rgba(0, 0, 0, 0.22);
    cursor: pointer;
    transition: border-color 0.15s, background 0.15s, transform 0.12s;
}
.task-slot:hover { transform: translateY(-2px); }
/* An empty slot lights up only while a card is actually in hand — otherwise "click here" is
   an instruction the click cannot carry out. */
.task-slot.open {
    border-color: var(--gold);
    background: rgba(217, 164, 78, 0.12);
    box-shadow: 0 0 16px -4px var(--gold);
}
.task-slot.filled { border-color: rgba(120, 235, 190, 0.35); }
.task-who {
    display: flex; flex-direction: column; align-items: center; gap: 3px;
    font-family: var(--dk-font-ui);
    font-size: 12px;
    color: var(--paper);
}
.task-who .seat-char { font-size: 9px; }
.task-wants {
    font-size: 10px;
    color: rgba(244, 239, 226, 0.55);
    max-width: 140px; text-align: center; line-height: 1.25;
}
.task-card {
    position: relative;
    border-radius: 4px;
    background: #f6f3ec center / cover no-repeat;
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.6);
    display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: 4px;
    padding: 4px;
    font-family: var(--dk-font-ui);
    color: #2f2a24;
    text-align: center;
    flex: 0 0 auto;
    transition: transform 0.12s, box-shadow 0.12s;
}
.task-card .task-name { font-size: 10px; font-weight: 700; line-height: 1.2; }
.task-card .task-tier { font-size: 8px; letter-spacing: 0.08em; text-transform: uppercase; }
.task-card.t-normal:not(.art)    { border-bottom: 4px solid #4fc3ba; }
.task-card.t-hard:not(.art)      { border-bottom: 4px solid #ef5f96; }
.task-card.t-difficult:not(.art) { border-bottom: 4px solid #8257c8; }
.task-card.pick { cursor: grab; }
.task-card.pick:hover { transform: translateY(-3px) scale(1.04); }
.task-card.held {
    transform: translateY(-6px) scale(1.08);
    box-shadow: 0 0 0 2px var(--gold), 0 10px 24px rgba(0, 0, 0, 0.7);
}
/* A card that has been dealt out. It KEEPS its place in the row — the grid is what makes a
   drag target stable and what shows which tasks are already spoken for — so it is disabled
   rather than removed. */
.task-card.dealt {
    opacity: 0.26;
    filter: grayscale(0.7);
    cursor: default;
    pointer-events: none;
}
.task-card.dragging { opacity: 0.35; cursor: grabbing; }
/* The floating copy that follows the pointer. It must never be a hit target itself, or
   elementFromPoint would only ever report the ghost and no drop target could be found. */
/* A contract announced on a seat's plate for a couple of seconds. */
.seat-flash {
    position: fixed;
    z-index: 320;
    display: flex; flex-direction: column; align-items: center; gap: 1px;
    padding: 6px 12px;
    border-radius: var(--dk-radius);
    background: rgba(20, 16, 12, 0.94);
    border: 1px solid var(--c-gambler);
    box-shadow: 0 6px 22px rgba(0, 0, 0, 0.7), 0 0 18px -6px var(--c-gambler);
    font-family: var(--dk-font-display);
    font-size: 14px; line-height: 1.25;
    color: var(--paper);
    white-space: nowrap;
    pointer-events: none;
    animation: tt-flash-in 0.22s cubic-bezier(0.2, 0.8, 0.3, 1.3);
}
.seat-flash b { color: var(--c-gambler); font-size: 12px; letter-spacing: 0.06em; }
.seat-flash.out { opacity: 0; transform: translateY(-6px); transition: opacity .24s, transform .24s; }
@keyframes tt-flash-in { from { opacity: 0; transform: translateY(8px) scale(0.92); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .seat-flash { animation: none; } }

.drag-ghost {
    pointer-events: none;
    z-index: 1200;
    opacity: 0.92;
    transform: rotate(-3deg) scale(1.06);
    box-shadow: 0 18px 40px rgba(0, 0, 0, 0.75);
}
/* The drop target, while a card is over it. */
.task-slot.over {
    border-color: var(--gold);
    background: rgba(217, 164, 78, 0.2);
    box-shadow: 0 0 22px -4px var(--gold);
    transform: translateY(-2px);
}
.task-deck.over { background: rgba(217, 164, 78, 0.06); border-radius: var(--dk-radius); }

/* The hover zoom. A FIXED element rather than a scaled card: the panel scrolls, so a transform
   would be clipped and a card in the top row would be cut in half.
   Two things this MUST get right, both of which were wrong first time round and both invisible
   to a test that only measured the element's rectangle:
     - it is its own class, not `.task-card.task-zoom`, because the item modal zooms an
       `.item-card` and a task-scoped rule simply did not match it — leaving it `position:
       static` and laid out inside <body>, behind everything;
     - it has to sit ABOVE the modal it is launched from. `.overlay` is --dk-z-overlay (1100),
       so 400 put the zoom behind the very dialog it belongs to. */
.card-zoom {
    position: fixed;
    z-index: 1150;                       /* over --dk-z-overlay (1100), under --dk-z-start */
    pointer-events: none;
    box-shadow: 0 0 0 1px rgba(217, 164, 78, 0.5), 0 18px 44px rgba(0, 0, 0, 0.8);
    animation: tt-zoom-in 0.12s ease-out;
}
@keyframes tt-zoom-in { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
    .card-zoom { animation: none; }
}
/* A printed card floated by the zoom for the seat-plate props: art on a dark ground, rounded
   like the print so the .card-zoom ring hugs its corners. The bid pin scales with it because
   it is %-sized. */
.flat-zoom-card {
    position: relative;
    border-radius: 12px;
    background: #17120c center / contain no-repeat;
    display: flex; align-items: center; justify-content: center;
    color: var(--paper); font-family: var(--dk-font-ui);
}
/* The draft's landscape character sheet. The box is built to the sheet's OWN measured ratio
   (game.js reads it off the decoded image), so the art fills it edge to edge — `contain` would
   letterbox the few pixels the exports differ by, and against the sheet's dark lower band that
   showed as a white line under the trick ladder. */
/* NO GROUND COLOUR, and no ring. The sheets are RGBA with their own shape — square across the
   top, rounded across the bottom, and Resistance's last pixel row is alpha 18 all the way
   across — so ANY opaque colour behind them shows THROUGH that alpha as a pale rim: cream
   crescents in the bottom corners and a white hairline along the bottom edge. The art carries
   its own printed edge; the only thing this box adds is the drop shadow. The radius is set from
   JS to match the art's own, so `overflow: hidden` cuts the bust watermark to the same curve. */
.char-sheet-zoom {
    position: relative;
    overflow: hidden;
    background: transparent center / 100% 100% no-repeat;
    box-shadow: 0 18px 44px rgba(0, 0, 0, 0.8);
}
/* The character's bust, watermarked over the sheet. Same shape as .sheet-portrait on the
   round-end pages — anchored right, faded out to the left — but at a fraction of its opacity:
   there the portrait is the point of the page, here it must not cost a single line of rules
   text any contrast. Painted OVER rather than under because the sheet is a flat opaque image
   with nothing to show through. */
.char-sheet-bust {
    position: absolute;
    right: -3%; top: 0; bottom: 0;
    width: 46%;
    background: right center / contain no-repeat;
    opacity: 0.1;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 62%, #000 100%);
    mask-image: linear-gradient(90deg, transparent, #000 62%, #000 100%);
    pointer-events: none;
}
.flat-zoom-card .bid-pin { font-size: 16px; }
.task-empty {
    border-radius: 4px;
    border: 1px dashed rgba(217, 164, 78, 0.35);
    background: rgba(0, 0, 0, 0.25);
    display: flex; align-items: center; justify-content: center;
    padding: 4px;
    font-family: var(--dk-font-ui);
    font-size: 9px; line-height: 1.2;
    color: rgba(244, 239, 226, 0.4);
    text-align: center;
}
/* The two buttons are the ONLY way off this screen, and on a short viewport there is no card
   size that makes the whole hand-out fit — the fit pass floors at 40 px, below which a task card
   stops being a card at all. So the panel scrolls and the buttons DO NOT: they stay pinned to
   its bottom edge, visible whatever the deck above them is doing. Inert when nothing overflows.
   The bar carries its own ground, or the cards would scroll through it. */
.task-buttons {
    display: flex; gap: 10px; justify-content: center;
    margin-top: 6px;
    position: sticky;
    bottom: 0;
    padding: 12px 0 10px;
    background: linear-gradient(180deg, rgba(24, 23, 21, 0), #181715 55%);
}
/* A landscape phone has no height to spare. The recipients keep their name and their character
   and lose the objective line — which is printed on the character card they are holding — and
   everything else gives up a few pixels of padding, so the hand-out scrolls as little as it can. */
@media (max-height: 520px) {
    #task-modal .task-panel { padding: 10px 16px 12px; }
    #task-modal .task-panel h2 { font-size: 19px; margin-bottom: 2px; }
    .task-wants { display: none; }
    .task-slot { padding: 5px 7px 6px; }
    .task-slots { margin: 6px 0 2px; padding-top: 6px; }
    .task-deck { margin: 6px 0 2px; }
    .task-buttons { padding: 8px 0 6px; margin-top: 2px; }
}

/* The round-end sheet reuses the Collector's .combo-row grid, so it only needs the result
   chip and a neutral column for the rows that pay nothing either way. */
.task-chip {
    display: inline-block;
    padding: 1px 8px;
    border-radius: 999px;
    font-family: var(--dk-font-ui);
    font-size: 10px; font-weight: 700;
    letter-spacing: 0.05em;
    background: rgba(0, 0, 0, 0.35);
    border: 1px solid rgba(244, 239, 226, 0.25);
    color: rgba(244, 239, 226, 0.6);
}
.task-chip.done {
    background: rgba(120, 235, 190, 0.16);
    border-color: rgba(120, 235, 190, 0.5);
    color: #78ebbe;
}
.combo-pts.dim { color: rgba(244, 239, 226, 0.4); }

/* The task column in the round table — the name, with what it paid its holder under it. */
.round-table .task-cell {
    font-size: 11px; line-height: 1.25;
    color: rgba(244, 239, 226, 0.7);
}
.round-table .task-cell.done   { color: #78ebbe; }
.round-table .task-cell.failed { color: rgba(244, 150, 120, 0.85); }
.round-table .task-delta { display: block; font-size: 10px; opacity: 0.75; }

.collect-panel {
    position: relative;
    width: min(880px, 95vw);
    max-height: calc(100dvh - 24px);
    overflow-y: auto;
    padding: 20px 22px 16px;
    border-radius: var(--dk-radius-lg);
    background: linear-gradient(180deg, #2b2114, #17120c);
    border: 1px solid var(--dk-accent-soft);
    box-shadow: 0 12px 44px rgba(0, 0, 0, 0.72);
    text-align: center;
    color: var(--paper);
    font-family: var(--dk-font-ui);
    cursor: pointer;
}
.collect-panel h2 {
    font-family: var(--dk-font-display);
    font-weight: 700; font-size: 22px;
    color: var(--c-collector);
    margin-bottom: 4px;
}
.collect-grid {
    display: flex; flex-wrap: wrap;
    justify-content: center; gap: 8px;
    margin: 14px 0 10px;
    min-height: 40px;
}
/* The cards are built by makeCard(), which positions them absolutely for the table. In here
   they are laid out by the flex row instead, so the absolute positioning has to come off. */
.collect-grid .card { position: relative; left: auto; top: auto; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.55); }
.collect-hint { font-size: 12px; color: rgba(244, 239, 226, 0.5); }

/* The Collector's scoring sheet. Reuses .crown-panel's box — same parchment shell, one column. */
.crown-panel {
    position: relative;
    width: min(620px, 95vw);
    max-height: calc(100dvh - 24px);
    overflow-y: auto;
    padding: 22px 24px;
    border-radius: var(--dk-radius-lg);
    background: linear-gradient(180deg, #2b2114, #17120c);
    border: 1px solid var(--dk-accent-soft);
    box-shadow: 0 12px 44px rgba(0, 0, 0, 0.72);
    text-align: center;
    color: var(--paper);
    font-family: var(--dk-font-ui);
}
.crown-panel h2 {
    font-family: var(--dk-font-display);
    font-weight: 700; font-size: 24px;
    color: var(--gold);
    margin-bottom: 6px;
}
.combo-row {
    display: flex; align-items: center; gap: 10px;
    padding: 7px 4px;
    border-top: 1px solid rgba(217, 164, 78, 0.16);
    font-size: 14px;
    text-align: left;
}
.combo-name { flex: 0 0 auto; min-width: 132px; font-weight: 700; }
.combo-name em { font-weight: 400; font-size: 11px; opacity: 0.7; }
.combo-cards { flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: 3px; }
.combo-pts { flex: 0 0 auto; font-weight: 700; font-variant-numeric: tabular-nums; }
.combo-pts.pos { color: #8ce0a4; }
.combo-pts.neg { color: #f0907a; }
.combo-row.unused .combo-name { font-weight: 400; opacity: 0.75; }
.combo-row.empty { justify-content: center; opacity: 0.65; }
.combo-row.total { border-top: 2px solid rgba(217, 164, 78, 0.4); font-size: 16px; }
.combo-row.total .combo-name { color: var(--gold); }
.combo-row.win {
    justify-content: center;
    color: var(--c-collector);
    font-weight: 700;
    border-top: 0;
}
#combo-body { margin: 10px 0 16px; }
#combo-title { color: var(--c-collector); }

@media (prefers-reduced-motion: reduce) {
    #collect-modal, #combo-modal, .crown-pip-fly, .seat-plate.crown-lit { transition: none; }
}

/* ── Round-end sheet ───────────────────────────────────────────────────────── */

.round-table { width: 100%; border-collapse: collapse; margin: 6px 0 4px; font-size: 14px; }
.round-table th {
    font-size: 10.5px; font-weight: 700;
    letter-spacing: 0.08em; text-transform: uppercase;
    color: rgba(244, 239, 226, 0.5);
    padding: 3px 6px; text-align: left;
}
.round-table td { padding: 5px 6px; border-top: 1px solid rgba(217, 164, 78, 0.16); text-align: left; }
/* Words left, numbers CENTRED under their own heading. Right-aligned numerals pushed every
   column hard against the one after it, so the tricks read as part of the character's name and
   the heading floated over nothing; tabular figures keep the digits in line without it. */
.round-table .num { text-align: center; font-variant-numeric: tabular-nums; }
.round-table th.num { text-align: center; }
.round-table .gain.pos { color: #8ce0a4; font-weight: 700; }
.round-table .gain.neg { color: #f0907a; font-weight: 700; }
.round-table .total { color: var(--gold); font-weight: 700; }
.round-table tr.me { background: rgba(217, 164, 78, 0.10); }
.round-note { font-size: 12px; color: rgba(244, 239, 226, 0.6); margin-top: 6px; }

/* ── Opening titles ──────────────────────────────────── */

/* The intro sits over everything while it runs. Solid black rather than a scrim: the clip
   letterboxes a 16:9 stage inside whatever it is given, and the start screen showing through
   the bars reads as a rendering fault. Anywhere on it skips. Same shape as games/Haggis. */
.intro-overlay {
    position: fixed;
    inset: 0;
    z-index: 4000;
    background: #000;
    cursor: pointer;
    opacity: 1;
}
/* What is left once the film has finished: the same black, with the video and the skip button
   gone, waiting for the table to be built underneath it. Click-through, so the fade can never
   swallow a click meant for the cards. */
.intro-overlay.intro-curtain {
    cursor: default;
    pointer-events: none;
    transition: opacity 900ms ease;
}
@media (prefers-reduced-motion: reduce) {
    .intro-overlay.intro-curtain { transition: opacity 200ms linear; }
}
/* `contain`, not `cover`: the clip is 16:9 and cropping the wordmark off the top of a tall
   phone screen would be worse than the black bars. */
.intro-overlay .intro-video {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
    background: #000;
}
.intro-skip {
    position: absolute;
    right: 18px; bottom: 16px;
    padding: 7px 16px;
    font-family: var(--dk-font-ui);
    font-size: 13px;
    letter-spacing: 0.06em;
    color: rgba(244, 239, 226, 0.75);
    background: rgba(0, 0, 0, 0.45);
    border: 1px solid rgba(217, 164, 78, 0.32);
    border-radius: 999px;
    cursor: pointer;
    /* The overlay's own click handler does the skipping, so this is a label with a hit area
       rather than a second code path that could get out of step with it. */
    pointer-events: none;
}

/* ── Toast ─────────────────────────────────────────────────────────────────── */

/* The STACK, not a toast: #toast is the column several messages share. It is centred on the
   felt and grows DOWNWARD from the middle, so the first message stays where the eye already
   is and later ones queue under it instead of covering it. See showToast() in game.js. */
#toast {
    position: fixed;
    left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    z-index: 600;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    pointer-events: none;
    max-width: 88vw;
}
/* Empty, it must not sit on the table as an invisible box — it is centred over the trick. */
#toast:empty { display: none; }

.toast-item {
    padding: 12px 26px;
    border-radius: 12px;
    background: rgba(20, 16, 12, 0.92);
    border: 1px solid var(--gold);
    color: var(--paper);
    font-family: var(--tt-serif);
    font-size: 22px; font-weight: 700;
    letter-spacing: 0.04em;
    box-shadow: 0 10px 34px rgba(0, 0, 0, 0.6);
    text-align: center;
    max-width: 100%;
    opacity: 0;
    transform: translateY(6px) scale(0.97);
    transition: opacity 0.26s ease, transform 0.26s cubic-bezier(0.2, 0.8, 0.3, 1.2);
}
.toast-item.in { opacity: 1; transform: none; }
.toast-item.revolt { border-color: var(--c-resistance); color: #ffb877; }

/* An opponent's action, narrated. Quieter than the game's own announcements — smaller, and in
   the UI face rather than the serif — because there are a lot of them in a round and they are
   commentary on somebody else's turn, not something being asked of the player. */
.toast-item.act {
    font-family: var(--dk-font-ui);
    font-size: 15px; font-weight: 700;
    letter-spacing: 0.02em;
    padding: 9px 18px;
    background: rgba(31, 42, 65, 0.94);          /* --tt-blue-deep, the read-screen ground */
    border-color: rgba(217, 164, 78, 0.5);
    color: var(--tt-beige);
}
@media (prefers-reduced-motion: reduce) {
    .toast-item { transition: none; }
}
@media (max-width: 760px) {
    .toast-item { font-size: 17px; padding: 9px 18px; }
    .toast-item.act { font-size: 13px; padding: 7px 14px; }
}

/* ── Move-log and AI-panel chips (the panels themselves are shared chrome) ──── */

.log-card {
    display: inline-block;
    min-width: 1.4em;
    padding: 0 4px;
    border-radius: 3px;
    font-weight: 700; font-size: 11px;
    text-align: center;
    vertical-align: baseline;
}
.log-card.n-r { background: var(--s-r); color: #fff; }
.log-card.n-b { background: var(--s-b); color: #fff; }
.log-card.n-g { background: var(--s-g); color: #fff; }
.log-card.n-k { background: var(--s-k); color: var(--gold); }
.log-card.k-rare { background: #e3d8bd; color: #6b4a20; }
.log-card.k-flag { background: #e8e6df; color: #7a7468; }
.log-card.k-berserker { background: #14110e; color: var(--gold); }

/* ── Responsive ────────────────────────────────────────────────────────────── */

/* Card sizes step down with the viewport; the hover zoom scales back with them so a card can
   never grow wider than the panel that holds it. */
@media (max-width: 1400px), (max-height: 940px) {
    .char-card { width: 208px; }
    .char-art { height: 84px; font-size: 38px; }
    .char-card:hover { transform: translateY(-5px) scale(1.26); }
    .char-card.detail:hover { transform: translateY(-5px); }
    .char-stamp { font-size: 15px; padding: 8px 12px; }
}
@media (max-width: 1120px), (max-height: 760px) {
    .char-card { width: 168px; }
    .char-art { height: 74px; font-size: 34px; }
    .char-card:hover { transform: translateY(-4px) scale(1.24); }
    .char-card.detail:hover { transform: translateY(-4px); }
    .char-grid { padding: 20px 6px 22px; }
    .select-panel { padding: 12px 12px 14px; }
    .select-panel h2 { font-size: 20px; }
    .char-stamp { font-size: 13px; padding: 6px 10px; border-width: 2px; }
}
/* The standings and the Round Card are hidden where they would cost a fifth of the screen —
   but a PORTRAIT tablet under 900px wide has an empty band above the top seat where they fit
   whole, so a tall viewport keeps them. Same three conditions as sidePanelsFit() in game.js,
   which decides whether they are drawn at all; this rule is the belt to that brace. */
@media (max-height: 560px), (max-width: 699px), (max-width: 900px) and (min-aspect-ratio: 5/6) {
    #rank-panel { display: none !important; }
}
@media (max-width: 780px), (max-height: 520px) {
    .char-card { width: 128px; }
    .char-head { font-size: 13px; padding: 3px 6px; }
    .char-wants { font-size: 9.5px; padding: 4px 6px 6px; }
    .char-body { font-size: 9.5px; }
    .char-grid { gap: 8px; padding: 16px 4px 18px; }
    .char-card:hover { transform: translateY(-3px) scale(1.2); }
    .char-card.detail:hover { transform: translateY(-3px); }
    .char-stamp { font-size: 11px; padding: 5px 8px; }
    .select-status { font-size: 12px; }
}
/* No hover on touch — a tap must never leave a card stuck zoomed over its neighbours. */
@media (hover: none) {
    .char-card:hover { transform: none; box-shadow: none; z-index: auto; }
}
@media (max-height: 420px) {
    .char-card { width: 100px; }
    .char-wants { display: none; }
}

/* ── The round-end carousel ──────────────────────────────────────────────────
   One page per character, paged with the arrows. Its own skin rather than the shared warm-brown
   panel: this is the only screen in the game that is purely a READ, so it gets a calmer,
   colder ground and lets the character's own portrait carry the identity. The multicolour band
   the other panels wear is deliberately absent — with a portrait behind the numbers it was a
   second thing competing to say whose screen this is. */
.round-sheet {
    background:
        radial-gradient(120% 90% at 82% 30%, rgba(58, 84, 116, 0.55), transparent 62%),
        linear-gradient(180deg, #23303f, #151d27);
    border-color: rgba(198, 178, 142, 0.30);
    overflow: hidden;                       /* the portrait bleeds to the panel's own edges */
    text-align: left;
}
.round-sheet::before { content: none; }     /* no identity band — the portrait is the identity */
/* The bust, bled into the panel's right edge and faded into the ground so the numbers stay
   readable across it. `contain` keeps a portrait a portrait at every panel size. */
.sheet-portrait {
    position: absolute;
    right: -4%; top: 0; bottom: 0;
    width: 52%;
    background: right center / contain no-repeat;
    opacity: 0.5;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 42%, #000 100%);
    mask-image: linear-gradient(90deg, transparent, #000 42%, #000 100%);
    pointer-events: none;
}
.sheet-portrait.none { display: none; }
/* A column that FILLS the page, so Continue can sit at the foot of the card rather than
   directly under the last number — the pages are all as tall as the tallest, and a button
   floating in the middle of the empty half read as another table row. */
.sheet-content {
    position: relative; z-index: 1;
    max-width: 62%;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
}
.sheet-content > .sheet-body {
    width: 100%;
    /* min-height:0 is what lets a flex child actually shrink and scroll. */
    flex: 0 1 auto;
    min-height: 0;
    overflow-y: auto;
}
.sheet-continue { margin-top: auto; }
.sheet-crowns { display: inline-flex; gap: 3px; margin-left: 8px; vertical-align: middle; }
.sheet-crowns .crown-pip { width: 18px; height: 18px; }
.round-sheet h2 {
    color: var(--paper);
    letter-spacing: 0.04em;
    margin-bottom: 4px;
}
.round-sheet .round-note { color: rgba(226, 216, 195, 0.72); }
/* Beige on blue, rather than the warm gold the felt-side panels use. */
.round-sheet .combo-row,
.round-sheet .round-table td,
.round-sheet .round-table th { border-color: rgba(198, 178, 142, 0.18); }
.round-sheet .combo-name,
.round-sheet .round-table td { color: #e8dfcc; }
/* The numbers are what this screen exists for, so they take the display face too. */
.round-sheet .combo-pts,
.round-sheet .combo-row.total .combo-name,
.round-sheet .round-table tr.total td {
    font-family: var(--dk-font-display);
    letter-spacing: 0.02em;
}
.round-sheet .combo-pts.pos { color: #9ad7a4; }
.round-sheet .combo-pts.neg { color: #e8968f; }
.round-sheet .combo-row.total,
.round-sheet .round-table tr.total td { color: #f2e7cf; border-top-color: rgba(198, 178, 142, 0.45); }
.round-sheet .combo-row.win { color: var(--gold); }
/* The pager. Small, quiet, and hidden outright when there is only one page. The arrows flank
   the row rather than sitting under it: they move the ROW, and a control that moves something
   reads better beside the thing it moves. */
.sheet-nav {
    display: flex; align-items: center; gap: 12px;
    margin-top: 14px;
}
.sheet-arrow {
    min-width: 40px; min-height: 40px;
    padding: 4px 12px;
    font-size: 20px; line-height: 1;
    border-color: rgba(198, 178, 142, 0.4);
    background: rgba(255, 255, 255, 0.05);
    color: #e8dfcc;
    /* Above the stack: the pages behind overflow the viewport on both sides, and they are
       positioned, so without this they take both the pixels and the click meant for the arrow
       they are sitting over. Higher than any page (the deck tops out at 40). */
    position: relative;
    z-index: 60;
    flex: 0 0 auto;
    /* Opaque, because it is read against a card rather than against the scrim. */
    background: rgba(21, 29, 39, 0.92);
}
.sheet-arrow:not(:disabled):hover { background: rgba(45, 62, 84, 0.96); }
.sheet-arrow:disabled { opacity: 0.25; }
.sheet-count {
    font-family: var(--dk-font-display);
    font-size: 14px; letter-spacing: 0.08em;
    color: rgba(226, 216, 195, 0.6);
}
/* A quieter Continue: on a read screen it is the way out, not the point of the screen. It sits
   ON the page it finishes with, at the foot of the numbers and clear of the portrait, so a
   sheet is a self-contained thing you are done with rather than a slide under a shared bar. */
.sheet-continue {
    width: auto; min-width: 150px;
    padding: 8px 22px;
    min-height: 38px;
    font-size: 14px;
}
/* Only the page on top can be pressed — the ones behind are dimmed scenery, and a button on a
   card you are not reading is a mis-click waiting to happen. */
.sheet-page:not(.here) .sheet-continue { visibility: hidden; }
.sheet-page:not(.here) .sheet-wait { display: none; }
.sheet-page .table-wait { margin-top: 6px; }
/* ── The carousel itself ─────────────────────────────────────────────────────
   A REAL one: every seat's page is in the track at once, the read one centred and its
   neighbours peeking in from both sides, dimmed and pushed back. The row slides; nothing is
   replaced. That is what makes the arrows legible — you can see what you are about to move to
   and how much of the round is left to read.

   The layout is one variable, `--at` (the index being read). The viewport is exactly ONE page
   wide and centred on the screen, so translating the track by whole pages lands the active one
   under the eye without any percentage arithmetic. Overflow is visible on purpose — the peek
   IS the overflow — and game.js hides anything past the immediate neighbours. */
.sheet-carousel {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    max-width: 100vw;
}
.sheet-viewport {
    width: min(620px, calc(100vw - 24px));
    overflow: visible;                     /* the stack behind lives outside this box */
    flex: 0 0 auto;
}
/* Multi-page: the two arrows flank the box and they must FIT — 2 x 40px plus the row's gaps,
   rounded up to 120px. Sized in vw alone (82vw) the pair hung off both edges of a 640px-wide
   screen, and an arrow whose centre is outside the window cannot be clicked at all: the round
   could not be paged and the match stopped there. The pulled-in box is also what gives the
   pages behind somewhere to show from. */
.sheet-carousel.multi .sheet-viewport { width: min(620px, calc(100vw - 120px)); }

/* The pages are STACKED, not laid in a row: every one is centred on the same point and the
   ones you are not reading are pushed out sideways, shrunk and dimmed, so they read as a deck
   with the current page on top. A filmstrip put a whole second page on screen, which competes
   with the one you are meant to be reading; a stack only promises that there IS more.
   game.js sets each page's transform, because the offsets depend on the measured page width
   and CSS has no abs() to lean on. */
.sheet-track {
    position: relative;
    width: 100%;
    height: var(--page-h, auto);
    --page-h: auto;
}
.sheet-page {
    position: absolute;
    left: 50%; top: 0;
    width: 100%;
    /* Every page the same height, measured by game.js from the tallest — the Collector's
       combination sheet is twice a plain trick ladder, and a stack whose cards change size as
       they come forward reads as a bug rather than as a deck. */
    min-height: var(--page-h);
    /* The shared .crown-panel scrolls itself; a stacked page must not, or the cards in the deck
       behind would each grow their own scrollbar. So the page is capped to what the screen can
       show (game.js caps --page-h to match) and it is the BODY that scrolls inside it — which
       keeps Continue pinned to the foot of the card and REACHABLE. On a 360px-tall screen the
       un-capped version pushed the button off the bottom with no way to scroll to it, and the
       round simply could not be dismissed. */
    max-height: calc(100dvh - 118px);
    overflow: hidden;
    margin: 0;
    /* A column, so the content can be told to fill the card: min-height alone leaves the
       page's height `auto`, and a percentage height inside it resolves to nothing. */
    display: flex;
    flex-direction: column;
    transform: translateX(-50%);
    transform-origin: 50% 50%;
    cursor: pointer;
    transition: transform 0.36s cubic-bezier(0.2, 0.8, 0.3, 1), opacity 0.36s ease,
                filter 0.36s ease, box-shadow 0.36s ease;
}
/* Behind: desaturated and darkened, so a peeking edge is clearly a card and not a second
   screen. The overlay itself does the darkening — a filter on the page keeps its own portrait
   and numbers in step without a second element. */
.sheet-page:not(.here) { filter: saturate(0.55) brightness(0.62); }
.sheet-page.here {
    cursor: default;
    filter: none;
    box-shadow: 0 18px 54px rgba(0, 0, 0, 0.8);
}
.sheet-page:not(.here) .sheet-body { pointer-events: none; }
/* Under the stack: how far through you are, then the button that leaves. Outside the track, so
   it neither travels with the pages nor takes a scale from them. */
.sheet-foot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    margin-top: 14px;
}
@media (prefers-reduced-motion: reduce) {
    .sheet-page { transition: none; }
}


/* ── The round table ─────────────────────────────────────────────────────────
   The overview after the carousel. Same cool ground and beige type as the pages it follows, so
   the two read as one screen in two parts rather than as two unrelated dialogs. */
#round-modal .panel, #over-modal .panel {
    background:
        radial-gradient(90% 70% at 50% 0%, color-mix(in srgb, var(--tt-blue-lit) 40%, transparent),
                        transparent 70%),
        linear-gradient(180deg, var(--tt-blue), var(--tt-blue-deep));
    border: 1px solid rgba(198, 178, 142, 0.30);
    color: var(--tt-beige);
}
#round-modal .round-table td, #round-modal .round-table th,
#over-modal .round-table td, #over-modal .round-table th {
    border-color: rgba(198, 178, 142, 0.18);
    color: var(--tt-beige);
}
#round-modal .round-table th, #over-modal .round-table th {
    color: rgba(226, 216, 195, 0.62);
    font-family: var(--dk-font-display);
    letter-spacing: 0.06em;
}
#round-modal .round-table tr.me td, #over-modal .round-table tr.me td { color: #f6ecd6; }
#round-modal .round-note, #over-modal .round-note { color: rgba(226, 216, 195, 0.7); }
/* On a narrow panel the portrait would squeeze the numbers into a column, so it steps back
   and becomes a faint ground behind them instead. */
@media (max-width: 620px), (max-height: 520px) {
    .sheet-content { max-width: none; }
    .sheet-portrait { width: 100%; right: 0; opacity: 0.18;
        -webkit-mask-image: none; mask-image: none; }
}

/* ── Multiplayer lobby ────────────────────────────────────────────────────────
 * Reuses the shared .overlay / .panel / .primary-btn chrome from core/deckades.css; only the
 * pieces the lobby adds are defined here. Controls are >=44px so the code input and the seat
 * list stay usable on a phone.
 *
 * The lobby is a READ screen — you stop and look at a code and a list of names — so it takes
 * the same King's-cloak blue ground and beige text as the draft and the round-end carousel
 * rather than the felt-side browns. */
.mp-panel { max-width: 420px; }
.mp-blurb { font-size: 13.5px; line-height: 1.55; color: rgba(232, 223, 204, 0.72);
            margin-bottom: 10px; }
.mp-seats-note { font-size: 12.5px; color: rgba(232, 223, 204, 0.5); margin-bottom: 12px; }
.mp-field { display: flex; flex-direction: column; gap: 4px; text-align: left;
            margin: 10px 0; font-size: 13px; color: rgba(232, 223, 204, 0.75); }
.mp-field input {
    min-height: var(--dk-tap); padding: 8px 12px; border-radius: var(--dk-radius);
    border: 1px solid rgba(77, 102, 150, 0.45); background: rgba(31, 42, 65, 0.85);
    color: var(--tt-beige); font: 400 16px var(--dk-font-ui);  /* 16px: iOS zooms in below it */
}
.mp-field input:focus { outline: none; border-color: var(--dk-accent); }
#mp-code { letter-spacing: 0.22em; text-transform: uppercase; text-align: center; }
.mp-code {
    font-family: var(--dk-font-display); font-size: 34px; font-weight: 700;
    letter-spacing: 0.28em; text-indent: 0.28em;      /* balance the trailing space */
    color: var(--dk-accent); margin: 10px 0 6px;
    text-shadow: 0 0 20px var(--dk-accent-soft);
}
.mp-copy { margin-bottom: 12px; }
.mp-seats { width: 100%; margin: 8px 0 4px; border-collapse: collapse; }
.mp-seats td { padding: 7px 10px; font-size: 14px; text-align: left;
               border-bottom: 1px solid rgba(77, 102, 150, 0.3); }
.mp-seats td:first-child { width: 2em; opacity: 0.5; font-variant-numeric: tabular-nums; }
.mp-seats tr.me td { color: var(--dk-accent); }
.mp-ai { opacity: 0.6; font-style: italic; }
/* A seat that has not answered the rematch yet. Deliberately quieter than the name it
   follows — it is a status, not the player. */
.mp-wait { opacity: 0.55; font-style: italic; font-size: 12.5px; }
.mp-list-title { margin: 14px 0 2px; font-size: 12px; letter-spacing: 0.12em;
    text-transform: uppercase; color: rgba(232, 223, 204, 0.45); }
.mp-err { min-height: 20px; font-size: 13px; color: var(--dk-msg-alert); margin: 8px 0; }

/* ── The card power reference ────────────────────────────────────────────────
   The printed strength card, one side at a time. Sized off the VIEWPORT HEIGHT and not off a
   width, because it is a portrait card that has to be read whole: capping the width instead
   let it run off the bottom of a short landscape screen. Same corner, same panel skin as the
   other header tools — the exclusivity in header.js means only one of them is ever up. */
#hier-popup {
    position: fixed; right: 12px; top: calc(var(--dk-header-h) + 6px);
    display: flex; flex-direction: column; align-items: center; gap: 8px;
    /* Capped at the size the card is actually READABLE at — the art is 746x1040, and blown up
       past this it only gets blurrier without getting clearer. The dvh term is what keeps the
       whole card on a short landscape screen, where height, not width, is the constraint. */
    width: min(320px, calc((62dvh - 56px) * 746 / 1040), 86vw);
    padding: 10px 12px 12px;
    background: var(--dk-panel);
    border: 1px solid var(--dk-panel-line);
    border-radius: var(--dk-radius);
    box-shadow: var(--dk-panel-shadow);
    color: var(--dk-panel-text);
    font-family: var(--dk-font-ui);
    z-index: var(--dk-z-panel);
}
/* The header button's icon is the card itself, shrunk. Fixed to the button's own height rather
   than a font size, because it is art and not a glyph. */
#hier-btn img {
    display: block; height: 27px; width: auto;
    border-radius: 2px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.65);
    /* The button is the target, not the picture on it: a tap that lands on the art must
       report the BUTTON, or the outside-click dismiss in header.js sees a foreign target. */
    pointer-events: none;
}
.hier-head {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%; gap: 8px;
}
#hier-close {
    /* Drawn small, hit big: the negative margins let a 36px target sit in a 20px row without
       pushing the card down or covering it. */
    flex: none; width: 36px; height: 36px; margin: -8px -10px -8px 0; padding: 0;
    background: transparent; border: none;
    /* The same cross as the ? popup's, from the same token — this was a violet literal. */
    color: var(--dk-popup-close); font-size: 13px; line-height: 1; cursor: pointer;
}
#hier-close:hover { color: var(--dk-popup-close-hover); }
.hier-title {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--dk-panel-dim);
}
.hier-img {
    width: 100%; border-radius: 6px; display: block;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.55);
    transition: transform 0.15s ease;
    backface-visibility: hidden;
}
.hier-img.flipping { transform: rotateY(90deg); }
@media (prefers-reduced-motion: reduce) { .hier-img { transition: none; } }
#hier-flip {
    width: 100%; min-height: var(--dk-tap);
    background: rgba(77, 102, 150, 0.35);
    border: 1px solid var(--dk-panel-line); border-radius: var(--dk-radius);
    color: var(--dk-panel-text); font: 500 13px var(--dk-font-ui);
    letter-spacing: 0.04em; cursor: pointer;
}
#hier-flip:hover { background: rgba(107, 138, 196, 0.45); }

/* The floating sheets' close chip. A mouse dismisses them by moving away, so the chip is only
   drawn where there is no hover to move — on a touch screen, where a sheet with no way out sat
   over the player's own hand. `pointer-events: auto` because the sheet around it is deliberately
   transparent to them. */
.zoom-close {
    position: absolute; top: 5px; right: 5px;
    display: none; align-items: center; justify-content: center;
    width: 30px; height: 30px; padding: 0;
    background: rgba(12, 8, 24, 0.82);
    border: 1px solid rgba(217, 164, 78, 0.55); border-radius: 50%;
    color: var(--tt-beige); font-size: 14px; line-height: 1;
    pointer-events: auto; cursor: pointer;
}
@media (hover: none) { .card-zoom > .zoom-close { display: flex; } }

/* ── The Rules link in the start screen's launch column — sized to its own text ──── */
.rules-link {
    align-self: center;
    display: inline-block;
    width: auto;
    margin-top: 4px;
    padding: 7px 16px;
    border: 1px solid var(--dk-accent-soft);
    border-radius: var(--dk-radius);
    background: transparent;
    color: var(--paper);
    font-family: var(--dk-font-ui); font-size: 13px; font-weight: 600;
    text-align: center; text-decoration: none; white-space: nowrap;
    opacity: .85;
    transition: background .15s, opacity .15s;
}
.rules-link:hover { background: var(--dk-accent-soft); opacity: 1; }

/* ── Crawlable copy (#intro in index.html): below the fold, never display:none ─────────
   The start screen is fixed and the table absolute, so the document has nothing to scroll;
   body's overflow: hidden keeps the block below the fold from ever being scrolled into. */
#intro {
    margin: 100vh auto 0; max-width: 720px; padding: 4px 20px 8px; text-align: center;
    color: var(--dk-text-muted, #c9bfae); font-family: var(--dk-font-ui); font-size: 14px;
    line-height: 1.55;
}
#intro h1 { color: var(--paper); font-family: var(--dk-font-display); font-weight: 700;
            font-size: 27px; letter-spacing: 0.04em; margin: 0 0 4px; }
#intro .intro-tagline { font-size: 12px; letter-spacing: 2px; text-transform: uppercase; margin: 0 0 14px; }
#intro h2 { color: var(--paper); font-family: var(--dk-font-display); font-weight: 700;
            font-size: 17px; letter-spacing: 0.03em; margin: 14px 0 5px; }
#intro p { margin: 0 0 9px; }
#intro strong { color: var(--paper); }
#intro em { color: var(--dk-accent); font-style: normal; }
#intro a { color: var(--dk-accent); }
