/* style.css — Haggis. THIS GAME ONLY.
 *
 * The header bar, AI badge, tool buttons, the 📜 / 🤖 panels, the start screen, the
 * dialog shell, 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/Haggis/style.css
 *       -> must return nothing
 *
 * What is left is Haggis's own surface: the board, the cards, the play zones, the Haggis
 * pile, the seat badges and the round-end reveal.
 */

:root {
    /* The three skin tokens a game may re-declare. Haggis takes them from the rulebook
       cover's stained glass: a turquoise ring over sea-green, on deep petrol blue. The
       accent is the point pip on the cards and the trick-value badge — the thing every
       decision in the game is about. */
    --dk-accent:      #46bfb0;
    --dk-accent-soft: rgba(70, 191, 176, 0.35);
    --dk-tint:        rgba(34, 116, 120, 0.26);  /* a turquoise wash over the shared felt */

    /* ONE face for the whole game: the display serif the start screen's HAGGIS wordmark
       is set in, adopted as the UI font too. Everything in this file (and the shared
       chrome) already asks for --dk-font-ui, so repointing it here re-skins the badges,
       the cards, the toast, the points cards and the panels in a single line. */
    --dk-font-ui: var(--dk-font-display);

    /* Cover palette, for everything below that is NOT one of the four skin tokens.
       --glass is the pale lattice, --jade the leaf green, --deep the outer ring. */
    --glass: rgba(198, 240, 236, 0.95);
    --jade:  #6fe0bf;
    --deep:  #0f2226;

    /* Card geometry — JS writes these from updateCardDims() on load and on resize. */
    /* Pre-JS defaults only — updateCardDims() overwrites all three. */
    --card-w: 70px;
    --card-h: 100px;
    --card-radius: 4px;

    /* Suit colours. The card faces are artwork now, so nothing paints with these
       any more; they are kept as the reference the scanned deck maps onto —
       hearts=red, diamonds=gold, clubs=green, stars=blue, spades=purple. Note
       --suit-spades is the odd one out: the printed spades suit is PURPLE, and
       this navy is left over from the CSS-drawn cards. */
    --suit-spades:   #2c3e50;
    --suit-hearts:   #c0392b;
    --suit-diamonds: #e6b800;
    --suit-clubs:    #1a6b38;
    --suit-stars:    #2664c9;
    --glow: rgba(140, 226, 218, 0.95);
}

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

/* ── Full-viewport canvas ──────────────────────────────────────────────────── */

body {
    background-color: var(--deep);
    width: 100vw;
    height: 100vh;
    overflow: hidden;
    /* On BODY, not just #game: the flight/deal/reveal animations append their cards to
       document.body, so a card that flipped face-up mid-flight rendered its rank in the
       browser's default serif and then switched to Fredoka the moment it landed inside
       #game. Everything that draws a card has to inherit the same face. */
    font-family: var(--dk-font-ui);
}

/* 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: the player sees Haggis's own art while choosing settings, then sits down at
   the same table as every other Deckades game. */
body::before {
    content: '';
    position: fixed;
    inset: 0;
    background-image: url('assets/cover.webp');
    background-size: cover;
    background-position: center;
    transform: scale(1.18);
    transform-origin: center;
    pointer-events: none;
    z-index: -2;
}

/* Scrim over the cover art, so the start card's text stays legible on top of it. */
body::after {
    content: '';
    position: fixed;
    inset: 0;
    background: rgba(6, 18, 20, 0.35);
    pointer-events: none;
    z-index: -1;
}

/* ── Touch / tablet support ────────────────────────────────────────────────── */

html, body {
    touch-action: manipulation;
    overscroll-behavior: none;
    -webkit-tap-highlight-color: transparent;
}
.card, button, .bet-btn, .bomb-btn {
    touch-action: manipulation;
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}

/* The table: the shared felt photo (games/table.avif) under a dark scrim, tinted 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: relative;
    width: 100vw;
    height: 100vh;
    overflow: hidden;
    z-index: 1;
    background: var(--dk-table);
    font-family: var(--dk-font-ui);
    color: white;
    touch-action: none;
}

/* …with the box art combined INTO the felt rather than replacing it: the stained-glass
   medallion is cropped out of its square cover (a radial mask drops the marble border and
   the publisher logo) and laid into the middle of the table at low opacity, like a crest
   inlaid in the wood. The board sits on top of it and is semi-transparent, so the crest
   reads through the play area without ever competing with a card.

   z-index 0 keeps it under every positioned child; #board is at 5 and the cards above it. */
/* The mask has to be CONCENTRIC with the medallion, and the medallion's geometry is
   measured, not guessed: in assets/cover.webp (895x900) the disc's dark rim runs
   x 97..799 and y 99..802 — centre (0.5006, 0.5006), radius 39.3% of the box. The mask
   was centred at `50% 45%` against art centred at 50%, so its window overhung the disc
   by 5% at the top (that gap is the pale marble of the cover, which read as a white
   halo over the crown) and fell 4% short at the bottom (which cut the rim off under
   "Sean Ross"). Concentric at 50% now, and the falloff ends AT the rim (39%) rather
   than past it, so no marble can show at any angle.

   background-position cannot fix this: `cover` on a near-square image in a square box
   leaves ~0.5% of slack, so the art has nowhere to slide. The window is what moves.

   `top` then lifts the whole crest by the 5% the old window was offset by, which keeps
   the visible artwork where it has always sat on the table — only now it is the whole
   medallion instead of a slice of it. */
#game::before {
    --crest: min(48vh, 520px);
    content: '';
    position: absolute;
    left: 50%; top: calc(50% - var(--crest) * 0.05);
    width: var(--crest); height: var(--crest);
    transform: translate(-50%, -50%);
    background: url('assets/cover.webp') center / cover no-repeat;
    -webkit-mask-image: radial-gradient(ellipse 39% 39% at 50% 50%, #000 0 88%, transparent 100%);
    mask-image: radial-gradient(ellipse 39% 39% at 50% 50%, #000 0 88%, transparent 100%);
    opacity: 0.22;
    filter: saturate(0.8);      /* the cards stay the most saturated thing on the table */
    pointer-events: none;
    z-index: 0;
}

/* ── Live ranking table (top-right, under the header) ──────────────────────── */

/* Match position, directly above the standings it counts. Same column and width as
   #ranking-table, whose `top` starts one line lower (RANK_TOP in game.js). Sits BELOW
   the header floor (56px) — above it the text ran under the AI badge. */
#round-counter {
    position: fixed;
    top: 61px; right: 14px;
    width: 172px;
    z-index: 200;
    pointer-events: none;
    user-select: none;
    text-align: center;
    font-family: var(--dk-font-ui);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: rgba(198, 240, 236, 0.75);
    text-shadow: 0 2px 8px rgba(0, 0, 0, 0.85);
}

#ranking-table {
    position: fixed;
    /* Must equal RANK_TOP in game.js — the right seat's fan is kept clear of it. */
    top: 84px; right: 14px;
    width: 172px;
    z-index: 200;
    pointer-events: none;
    user-select: none;
    font-family: var(--dk-font-ui);
}
#ranking-table .rank-row {
    position: absolute;
    left: 0; right: 0;
    height: 34px;
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 0 12px;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.45);
    border: 1px solid var(--dk-accent-soft);
    color: rgba(226, 240, 240, 0.85);
    backdrop-filter: blur(4px);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
    transition: top 380ms cubic-bezier(.45, .1, .3, 1), border-color 0.2s, background 0.2s;
}
#ranking-table .rank-row.active-player {
    border-color: var(--dk-accent);
    background: rgba(12, 46, 48, 0.75);
}
#ranking-table .rank-place {
    font-size: 11px; font-weight: 600;
    color: rgba(130, 210, 202, 0.8);
    min-width: 18px; text-align: left;
}
#ranking-table .rank-name {
    flex: 1; font-size: 13px; font-weight: 600;
    color: rgba(226, 240, 240, 0.85);
    text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#ranking-table .rank-bet {
    font-size: 10px; color: rgba(140, 226, 218, 0.6);
    min-width: 30px; text-align: right;
}
#ranking-table .rank-score {
    font-size: 16px; font-weight: 700;
    color: rgba(150, 232, 222, 0.95);
    min-width: 30px; text-align: right;
}

/* ── Board ─────────────────────────────────────────────────────────────────── */
/* width/height MUST match boardDims() in game.js. Deliberately smaller than the fans
   around it: the middle only ever holds one trick, and the cards are what the player
   actually reads. */

#board {
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: min(46vw, 1000px);
    height: min(50vh, 600px);
    display: grid;
    grid-template-columns: 14% 1fr 14%;
    grid-template-rows: 1.45fr 1fr;
    gap: 6px;
    padding: 10px;
    background: rgba(0, 0, 0, 0.30);
    border-radius: 18px;
    border: 1px solid rgba(255, 255, 255, 0.07);
    box-shadow: 0 0 60px rgba(0, 0, 0, 0.5), inset 0 0 30px rgba(0, 0, 0, 0.2);
    z-index: 5;
}

/* Play zones — positioning targets for played trick cards.
   3 players: left (slot 1), right (slot 3), bottom (slot 0).
   2 players: top (slot 2) and bottom (slot 0) only. */
.play-zone { position: relative; overflow: visible; }
/* "PASS" banner laid across a player's hand (positioned by JS at the hand centre). */
.pass-overlay {
    position: absolute;
    transform: translate(-50%, -50%) rotate(-6deg);
    z-index: 70;
    pointer-events: none;
    font-family: var(--dk-font-ui);
    font-weight: 800;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: rgba(198, 240, 236, 0.96);
    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.95), 0 0 22px rgba(0, 0, 0, 0.7);
    background: rgba(8, 26, 28, 0.5);
    border: 1px solid var(--dk-accent-soft);
    border-radius: 14px;
    padding: 6px 26px;
    white-space: nowrap;
}
/* Side opponents span the full board height so their played cards sit on the
   board's vertical centre line (same as the trick label / centre area). */
#play-zone-1 { grid-column: 1; grid-row: 1 / 3; }   /* left opponent  */
#center-area { grid-column: 2; grid-row: 1; }
#play-zone-3 { grid-column: 3; grid-row: 1 / 3; }   /* right opponent */
/* Human (bottom). Nudged down inside its cell — the row it shares with the two side
   seats put its cards level with theirs, which read as a fourth seat rather than as the
   near side of the table. The offset is card-relative so it survives a resize, and it
   moves the ANIMATION target too: animatePlay() flies to the zone's live rect. */
#play-zone-0 {
    grid-column: 2;
    grid-row: 2;
    transform: translateY(calc(var(--card-h) * 0.22));
}
/* The two-player opponent, straight across the table. Mirror of the human's zone: the
   same cell column, the top row, nudged the same distance the other way so the two
   players' cards sit an equal step either side of the board's middle. */
#play-zone-2 {
    grid-column: 2;
    grid-row: 1;
    transform: translateY(calc(var(--card-h) * -0.22));
}
/* Two players own the centre column outright — no side seats share it — so both zones
   push further out toward the hand that played them, which is what says WHOSE cards
   those are without reading a badge. The two nudges must stay equal and opposite or the
   trick sits off the board's middle. They also move the flight target: animatePlay()
   flies to the zone's live rect. */
body.seats-2 #play-zone-0 { transform: translateY(calc(var(--card-h) * 0.56)); }
body.seats-2 #play-zone-2 { transform: translateY(calc(var(--card-h) * -0.56)); }
/* Under ~620px tall the board and both fans already compete for the same rows: measured
   at 640x360, the played cards overlap the hand by 41px with the nudge at ZERO. That is a
   pre-existing tiny-viewport problem and a separate fix; the outward push is simply
   switched off here so it is not added to it. */
@media (max-height: 620px) {
    body.seats-2 #play-zone-0,
    body.seats-2 #play-zone-2 { transform: none; }
}

/* ── Haggis pile ─────────────────────────────────────────────────────────────
   Three players: the fixed strip above the board, in the top-centre area the two side
   fans leave free. Two players: the LEFT MARGIN — with an opponent across the top that
   strip is their hand, and the two-player game has no side fans, so the whole margin is
   free. It is also eight cards there instead of three, which is why renderHaggis() fits
   the stagger to the room rather than assuming it. */

#haggis-area {
    position: fixed;
    top: 60px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    z-index: 6;
    pointer-events: none;
}
body.seats-2 #haggis-area {
    top: 50%;
    /* Anchored to the BOARD's left edge, not the screen's: out at the viewport corner the
       pile read as a stray stack of cards belonging to nothing. Right-anchored, so it
       stays tight against the table however wide the margin gets — the calc restates
       #board's own min() width, and MUST be kept in step with it and with boardDims()
       and haggisRight() in game.js. HAGGIS_BOARD_GAP there is the 14px. */
    left: auto;
    right: calc(50% + min(23vw, 500px) + 14px);
    /* Vertically centred on the viewport, which is also the board's centre line, so the
       pile reads as belonging to the table rather than floating near the header. */
    transform: translateY(-50%);
    align-items: flex-end;
}
/* Pre-JS default only — renderHaggis() sizes this to the staggered backs it draws. */
#haggis-pile {
    position: relative;
    width: 58px; height: 72px;
}
#haggis-label {
    font-size: 10px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(226, 240, 240, 0.5);
}

/* ── Trick banner: trick type + points, pinned to the top of the play area ──── */

#center-area { position: relative; }

#trick-banner {
    position: fixed;
    top: max(140px, calc(50% - min(25vh, 300px) + 8px));
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    gap: 10px;
    z-index: 150;
    pointer-events: none;
}
#trick-label {
    font-size: 13px;
    letter-spacing: 0.06em;
    color: rgba(226, 240, 240, 0.7);
    text-align: center;
    min-height: 16px;
    display: flex;
    align-items: center;
}
#trick-points {
    font-family: var(--dk-font-ui);
    font-size: 14px;
    font-weight: 700;
    letter-spacing: 0.04em;
    color: #04322e;
    background: radial-gradient(circle at 35% 30%, #d6f6f0, #46bfb0 80%);
    border: 1px solid rgba(10, 80, 74, 0.5);
    border-radius: 11px;
    padding: 2px 10px;
    min-height: 16px;
    text-align: center;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
#trick-points:empty { display: none; }

/* Two players: the top-centre strip the banner normally pins to IS the opponent's played
   cards — #play-zone-2 lives in that cell — so the caption printed straight over them.
   The board's LEFT column is free in this layout (no side seats), so the banner moves
   there and stacks, label above points, instead of running as one wide row.
   Like #bet-bar, this restates #board's own min() sizes to land on the board's edge: the
   banner is a sibling of #board, not a child, so it cannot inherit them. Keep both in
   step with #board's width/height and with boardDims() in game.js.
   It sits on the vertical middle of the board's left column, level with the trick it
   describes. The Haggis pile shares that band but is OUTSIDE the board (its right edge is
   14px left of the board's left edge) while this is 14px inside, so the two sit either
   side of the edge and cannot collide. The max() is a cheap floor only — with the board at
   46vw the calc cannot actually go negative. */
body.seats-2 #trick-banner {
    top: 50%;
    left: max(12px, calc(50% - min(23vw, 500px) + 14px));
    transform: translateY(-50%);
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    max-width: min(26vw, 170px);
}
body.seats-2 #trick-label { text-align: left; }

/* ── Status line ─────────────────────────────────────────────────────────────
 * Whose turn it is, and what the human may do right now — the text that used to sit in
 * the header. It belongs BELOW the human's fan: that is where the player is already
 * looking when they need it. positionStatusLine() writes left/top in the same layout
 * pass as the hand, so the strip can never drift away from the cards it describes.
 *
 * Its own ids, not #header-msg: that selector belongs to the shared sheet and this text
 * is no longer in the header. The good/alert hues are READ from the shared tokens —
 * reading them is fine, re-declaring them would not be. */

#status-line {
    position: fixed;
    transform: translateX(-50%);
    max-width: 92vw;
    text-align: center;
    pointer-events: none;
    z-index: 120;
}
#status-msg {
    display: inline-block;
    font-family: var(--dk-font-display);
    font-size: 17px;
    letter-spacing: 0.4px;
    color: rgba(226, 240, 240, 0.88);
    text-shadow: 0 2px 8px rgba(0, 0, 0, 0.85);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 92vw;
    opacity: 0;
    transition: opacity 0.25s, color 0.2s;
}
#status-msg.show      { opacity: 1; }
#status-msg.msg-good  { color: var(--dk-msg-good); }
#status-msg.msg-alert { color: var(--dk-msg-alert); }

@media (max-height: 560px) {
    #status-msg { font-size: 14px; }
}

/* ── Cards (printed artwork, assets/cards/) ────────────────────────────────── */

/* No background and no border: the face is a WebP whose rounded corners live in
   its own alpha channel, so anything painted on the element itself would show
   through those corners as a square halo. --card-radius is set from JS to the
   artwork's real corner radius, which is what keeps the selection ring — drawn
   as a box-shadow on the element — hugging the art instead of squaring off. */
.card {
    position: absolute;
    width: var(--card-w);
    height: var(--card-h);
    border-radius: var(--card-radius, 4px);
    background: none;
    border: none;
    /* box-shadow, not drop-shadow: it follows border-radius, which now matches
       the artwork, so it costs nothing per frame and still sits correctly under
       the rounded corners. The .selected / .invalid-flash rings use the same
       property, so they replace this shadow rather than fighting it. */
    box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.45);
    transition: transform 0.12s, box-shadow 0.12s, filter 0.12s;
    flex-shrink: 0;
}
/* Fills the card exactly. The art is cut to the same ratio the layout uses
   (CARD_RATIO in game.js), so `100% 100%` scales it without distorting. */
.card .c-art {
    display: block;
    width: 100%;
    height: 100%;
    pointer-events: none;
    -webkit-user-drag: none;
}

/* Wild face cards (J/Q/K) — the printed face carries no suit, so the gold ring is
   the only thing marking one out on the table. */
.card.wild-card {
    box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.55), 1px 2px 7px rgba(0, 0, 0, 0.45);
}

/* Face-down (opponent / haggis / deck) cards — the deck's own back, the green thistle
   knotwork, cut from assets/Haggis Card Back.pdf at the same trim as the faces, so it
   agrees with CARD_RATIO by construction rather than by re-stretching. Its rounded
   corners are baked into the alpha channel, so it maps 1:1 with `100% 100%` and a
   `cover` fit would slice those corners off. */
.card.face-down {
    background: url('gfx/cardback.webp') center / 100% 100% no-repeat;
}
.card.face-down::after { content: none; }

.card.in-hand { cursor: pointer; }
/* Brightness only — a border-color hover died with the border, and the art has to
   stay the thing that reads. */
.card.in-hand:hover:not(.selected) {
    filter: brightness(1.08);
    z-index: 100 !important;
}
.card.selected {
    transform: translateY(-20px);
    box-shadow: 0 0 0 2px gold, 1px 14px 20px rgba(0, 0, 0, 0.6);
    z-index: 100 !important;
}
.card.inactive { opacity: 0.62; cursor: default; }

.card.invalid-flash { animation: invalid-shake 0.4s ease; }
@keyframes invalid-shake {
    0%, 100% { box-shadow: 0 0 0 2px gold; }
    25% { box-shadow: 0 0 0 2px #e74c3c; transform: translateY(-20px) translateX(-4px); }
    75% { box-shadow: 0 0 0 2px #e74c3c; transform: translateY(-20px) translateX(4px); }
}

/* ── Player info badges (name / bet / score) ───────────────────────────────── */

.pinfo-badge {
    position: fixed;
    z-index: 50;
    pointer-events: none;
    background: rgba(0, 0, 0, 0.58);
    border: 1px solid rgba(255, 255, 255, 0.16);
    border-radius: 14px;
    padding: 9px 18px;
    text-align: center;
    font-family: var(--dk-font-ui);
    transition: border-color 0.2s, box-shadow 0.2s;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
.pinfo-badge.active-player {
    border-color: var(--dk-accent);
    box-shadow: 0 2px 12px rgba(60, 180, 170, 0.4);
}
/* Bomb give-away targets: glowing, clickable opponent badges. */
.pinfo-badge.bomb-target {
    border-color: rgba(255, 90, 70, 0.95);
    cursor: pointer;
    animation: bomb-glow 1s ease-in-out infinite alternate;
}
.pinfo-badge.bomb-target:hover {
    background: rgba(80, 20, 12, 0.82);
}
@keyframes bomb-glow {
    from { box-shadow: 0 0 6px 1px rgba(255, 80, 60, 0.45); }
    to   { box-shadow: 0 0 18px 5px rgba(255, 80, 60, 0.85); }
}

/* Transient toast banner (bomb give-away — the prompt to the human and the AI's result).
   Sits in the TOP PART OF THE BOARD, just under the trick banner, not up by the header:
   it talks about the trick in the middle, and the bomb prompt has to be read in the same
   glance as the glowing name badges it refers to. The `top` follows #trick-banner's own
   formula (board top = 50% - min(25vh, 300px)) so the two never drift apart. */
#toast {
    position: fixed;
    top: max(176px, calc(50% - min(25vh, 300px) + 44px));
    left: 50%;
    transform: translateX(-50%) translateY(-8px);
    z-index: 400;
    padding: 10px 20px;
    background: rgba(8, 20, 22, 0.92);
    border: 1px solid var(--dk-accent-soft);
    border-radius: 12px;
    color: rgba(234, 246, 245, 0.96);
    font-family: var(--dk-font-ui);
    font-size: 15px;
    font-weight: 500;
    letter-spacing: 0.03em;
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.55);
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.25s, transform 0.25s;
}
#toast.show {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

/* ── Hand-sort button icon ───────────────────────────────────────────────────
   Two cards from the deck's own art, tilted into a small fan, inside the shared
   .hdr-tool-btn. Only the span and the imgs are styled here — the button itself is
   chrome and stays deckades.css's. 12x22 is CARD_RATIO (0.545) at icon size, so the
   two faces are not squashed against the ones on the table. */
.sort-icon {
    position: relative;
    display: block;
    width: 23px;
    height: 24px;
    pointer-events: none;      /* the click is the button's, never an image drag */
}
.sort-icon img {
    position: absolute;
    top: 1px;
    width: 12px;
    height: 22px;
    border-radius: 2px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
}
.sort-icon img:first-child { left: 0;  transform: rotate(-11deg); }
.sort-icon img:last-child  { left: 11px; transform: rotate(9deg); }

/* ── Hand-sort panel ─────────────────────────────────────────────────────────
   Opened from the 🗂️ header button, and deliberately NOT a #move-log-panel rule:
   those two ids are shared chrome in deckades.css and this one is Haggis-only, so it
   is built here out of the shared TOKENS instead. That is what keeps it identical to
   the other two panels without a game writing rules for their selectors.
   Sits on the same shelf under the header, at the same right margin. */
#sort-panel {
    position: fixed;
    right: 12px;
    top: calc(var(--dk-header-h) + 6px);
    width: min(200px, 80vw);
    padding: 11px 13px;
    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: 1300;
}
.sort-opt {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 34px;          /* + the 44px-tall button that opens it, on a rare tap */
    margin-top: 4px;
    padding: 6px 8px;
    border: 1px solid transparent;
    border-radius: 7px;
    background: none;
    color: inherit;
    font: inherit;
    font-size: 13px;
    text-align: left;
    cursor: pointer;
    touch-action: manipulation;
    transition: background 0.15s, border-color 0.15s;
}
.sort-opt:hover { background: rgba(150, 100, 220, 0.18); }
.sort-opt.active {
    border-color: var(--dk-accent-soft);
    background: rgba(150, 100, 220, 0.14);
    color: #fff;
}
/* Reserved whether or not it is ticked, so the labels do not shift as you switch. */
.sort-tick {
    flex: none;
    width: 12px;
    color: var(--dk-accent);
    font-weight: 700;
}

/* ── Bet chips ───────────────────────────────────────────────────────────────
   gfx/chip_*.webp are the deck's own poker chips, cut round from its print PDF by
   scripts/cut_chips_from_print_pdf.py — transparent outside the die line. Drawn
   wherever a bet amount used to be written, at four sizes.

   Every size states BOTH dimensions on purpose. Two callers measure their own box
   in the frame it is built — showBetBubble() reads offsetWidth to pin the bubble,
   renderBadges() to clamp the badge inside the viewport — and an <img> with no CSS
   size measures 0 until the file has loaded, which would place both off by a chip. */
.bet-chip {
    display: inline-block;
    vertical-align: middle;
    object-fit: contain;
    /* The print PDF's drop shadow was cut away with the bleed, so the disc gets its
       lift here instead, at the size it is actually drawn. */
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
}
/* The two small sizes are set by their numeral, not by the space going spare: the
   printed digits are ~40% of the disc, so below ~18px the chip stops saying which
   bet it is and becomes a coloured dot. The 34px ranking row is the ceiling. */
.bet-chip.chip-xs { width: 18px; height: 18px; }                  /* ranking table */
.bet-chip.chip-sm { width: 20px; height: 20px; }                  /* name badge    */
.bet-chip.chip-md { width: 24px; height: 24px; margin-left: 7px; }/* speech bubble */
.bet-chip.chip-lg {                                               /* bet button    */
    width: 52px; height: 52px;
    filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.6));
}

/* ── Bet speech bubble ───────────────────────────────────────────────────────
   A bet is announced, so it is drawn as speech: a bubble over the bettor's name badge
   with a tail pointing back at it. showBetBubble() sets left/top in px, so the transform
   may only centre it horizontally — never add a Y translate to the resting state. */
.bet-bubble {
    position: fixed;
    /* Above the points cards (1150): a bet placed by the last seat still to act can land
       while the first-out card is up, and the bubble is the newer event. */
    z-index: 1160;
    pointer-events: none;
    padding: 7px 15px;
    border-radius: 14px;
    background: rgba(9, 27, 30, 0.95);
    border: 1px solid var(--dk-accent);
    box-shadow: 0 8px 22px rgba(0, 0, 0, 0.55);
    font-family: var(--dk-font-ui);
    font-size: 14px;
    font-weight: 800;
    letter-spacing: 0.04em;
    white-space: nowrap;
    color: #8fe3d8;
    transform: translateX(-50%) scale(0.6);
    transform-origin: 50% 130%;   /* grows out of the badge it points at */
    opacity: 0;
    transition: transform 0.26s cubic-bezier(.2,1.4,.4,1), opacity 0.26s;
}
.bet-bubble.show { transform: translateX(-50%) scale(1); opacity: 1; }
.bet-bubble.fade { opacity: 0; transform: translateX(-50%) translateY(-8px) scale(1); }
/* A declined bet is still speech, but it is not an event worth the accent colour. */
.bet-bubble.declined {
    color: rgba(226, 240, 240, 0.82);
    border-color: rgba(255, 255, 255, 0.24);
}
/* The tail: the bubble's own corner, rotated 45° so only two edges show. */
.bet-bubble::after {
    content: '';
    position: absolute;
    left: 50%;
    bottom: -6px;
    width: 11px;
    height: 11px;
    margin-left: -5.5px;
    background: inherit;
    border-right: 1px solid var(--dk-accent);
    border-bottom: 1px solid var(--dk-accent);
    transform: rotate(45deg);
}
/* Flipped below the badge (top seats, where there is no room above). */
.bet-bubble.below { transform-origin: 50% -30%; }
.bet-bubble.below::after {
    bottom: auto;
    top: -6px;
    border-right: 0;
    border-bottom: 0;
    border-left: 1px solid var(--dk-accent);
    border-top: 1px solid var(--dk-accent);
}
.bet-bubble.declined::after,
.bet-bubble.declined.below::after { border-color: rgba(255, 255, 255, 0.24); }

/* Itemised score card for the first player out. Pinned to their name badge by
   showShepherdCard(), which sets left/top in px — the transform only centres it
   horizontally, so never add a Y translate here or the clamping goes wrong. */
.shepherd-card {
    position: fixed;
    /* Above --dk-z-overlay (1100): at round end all three cards are shown alongside the
       round-end modal, and behind its dim they would be unreadable. */
    z-index: 1150;
    pointer-events: none;
    min-width: 190px;
    padding: 9px 12px 8px;
    border-radius: 12px;
    background: rgba(9, 27, 30, 0.93);
    border: 1px solid var(--dk-accent-soft);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
    font-family: var(--dk-font-ui);
    font-size: 13px;
    color: rgba(232, 246, 245, 0.94);
    transform: translateX(-50%) scale(0.7);
    transform-origin: 50% 100%;
    opacity: 0;
    transition: transform 0.28s cubic-bezier(.2,1.4,.4,1), opacity 0.28s;
}
.shepherd-card.show { transform: translateX(-50%) scale(1); opacity: 1; }
.shepherd-card .sc-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 6px;
    padding-bottom: 5px;
    border-bottom: 1px solid rgba(198, 240, 236, 0.22);
}
.shepherd-card .sc-name { font-weight: 800; font-size: 15px; color: #8fe3d8; }
.shepherd-card .sc-tag {
    margin-left: auto;
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(198, 240, 236, 0.75);
}
.shepherd-card .sc-row {
    display: flex;
    justify-content: space-between;
    gap: 16px;
    line-height: 1.6;
    white-space: nowrap;
}
.shepherd-card .sc-row b { color: var(--jade); font-weight: 800; }
.shepherd-card .sc-total {
    margin-top: 4px;
    padding-top: 4px;
    border-top: 1px solid rgba(198, 240, 236, 0.22);
    font-weight: 700;
}
.shepherd-card .sc-total b { font-size: 15px; }

/* The whole round-end dialog is gone; this is what replaces it. Above the points
   cards (1150) because it is the only thing to click. */
/* The human's badge is FIXED width so it never shifts when its own text grows (a
   declared bet appends "· bet 30"). Must equal HUMAN_BADGE_W in game.js. */
.pinfo-badge.self {
    /* Must equal HUMAN_BADGE_W in game.js. */
    width: 126px;
    text-align: center;
}
.pinfo-badge.self .pinfo-name,
.pinfo-badge.self .pinfo-meta {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

#next-round-btn {
    position: fixed;
    left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    z-index: 1200;
    width: auto;              /* .primary-btn is full-width inside a .panel */
    min-width: 210px;
}
#next-round-btn.hidden { display: none; }

/* ── End-of-round pile reveal (black sheep's hand + Haggis) ─────────────────── */
.reveal-caption {
    position: fixed;
    z-index: 775;
    pointer-events: none;
    transform: translate(-50%, -50%);
    font-family: var(--dk-font-ui);
    font-weight: 700;
    font-size: 16px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: rgba(198, 240, 236, 0.96);
    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.95);
    background: rgba(8, 26, 28, 0.55);
    border: 1px solid var(--dk-accent-soft);
    border-radius: 12px;
    padding: 4px 18px;
    white-space: nowrap;
}
/* The pile this labels has just been turned face-up directly beneath it, and the deck's
   art is pale stained glass on white — a bare number with a text-shadow disappeared into
   it. Same plate as .reveal-caption above, but opaque rather than translucent: this one
   lands ON the cards, where the caption sits above them on the table. */
.reveal-points {
    position: fixed;
    z-index: 775;
    pointer-events: none;
    transform: translate(-50%, -50%);
    font-family: var(--dk-font-ui);
    font-weight: 800;
    font-size: 30px;
    color: #8fe3d8;
    text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
    background: rgba(6, 20, 22, 0.92);
    border: 1px solid var(--dk-accent-soft);
    border-radius: 14px;
    padding: 4px 22px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
    white-space: nowrap;
}
/* Read from across the table, so sized like a place card rather than a caption. */
.pinfo-name {
    font-size: 17px;
    font-weight: 600;
    letter-spacing: 0.05em;
    color: rgba(226, 240, 240, 0.94);
    white-space: nowrap;
}
.pinfo-name .out-tag { color: rgba(120, 235, 205, 0.95); margin-left: 5px; }
.pinfo-meta {
    font-size: 14px;
    color: rgba(140, 226, 218, 0.85);
    margin-top: 3px;
}
.pinfo-score { font-weight: 700; color: rgba(150, 232, 222, 0.95); }

/* ── Decision bar (bet / play / pass / bomb) ───────────────────────────────── */

/* Two separate bars. The BET box stays off to the right of the hand — it is a
   once-per-round decision. PASS / PLAY are pressed every turn, so they sit centred
   directly under the fan, positioned by positionStatusLine() in the same JS layout pass
   as the cards rather than pinned to a screen corner. Keeping them apart is the point:
   next to each other, "Bet 30" was one button-height from "Play".

   The hint that used to live beside them has moved to #status-line, above the fan. */
/* The bet is drawn over the board's bottom-right corner — the one part of that grid
   nothing else occupies (the play zones take row 1 and the bottom centre). It used to
   be fixed to the viewport corner, which is where the human's own face-up J/Q/K group
   sits, so it needed a JS lift every layout pass to stay off the fan.
   It is a sibling of #board, not a child, and the corner is computed rather than
   inherited: #board is a stacking context at z-index 5, and on a short viewport the
   fan (cards at z-index 1..17 in #game) reaches up over the board's bottom edge, so
   anything parented to the board is painted UNDER those cards no matter how high its
   own z-index — the bottom row of this bar was unclickable at 640x360. Hence the
   calc()s: they restate #board's own min() sizes to land on the same corner, and MUST
   be kept in step with #board's width/height and with boardDims() in game.js.
   A row of chips over a full-width "no bet", not one column: a single column of four
   rungs grows tall enough to overhang the board. The chips are round and compact
   enough to sit three abreast, which is narrower than the 2x2 of text buttons this
   replaced — it has to stay inside a board only min(46vw, 1000px) wide. */
#bet-bar {
    position: absolute;
    right: calc(50% - min(23vw, 500px) + 10px);
    bottom: calc(50% - min(25vh, 300px) + 10px);
    z-index: 130;   /* above the fan's cards (max 100, on a selected card) */
    pointer-events: auto;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
}
/* Declining has no chip, so it is the one text button here — given the whole row
   under them rather than a fourth cell it would not fill. */
#bet-bar .bet-btn:not(.chip-btn) { grid-column: 1 / -1; }
#decision-bar {
    position: fixed;
    transform: translateX(-50%);   /* left/top come from positionStatusLine() */
    z-index: 120;
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 14px;
}
#bet-bar, #decision-bar {
    pointer-events: auto;
    padding: 9px 12px;
    background: rgba(8, 20, 22, 0.62);
    border: 1px solid var(--dk-accent-soft);
    border-radius: 12px;
    box-shadow: 0 6px 22px rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(4px);
}
/* Short viewports (640x360 and friends): the board itself is only ~180px tall, so the
   bet keeps its corner but shrinks to stay inside it rather than overhanging the
   board's edge into the side fans. */
@media (max-height: 480px) {
    #bet-bar {
        right: calc(50% - min(23vw, 500px) + 6px);
        bottom: calc(50% - min(25vh, 300px) + 6px);
        gap: 6px;
        padding: 7px 9px;
    }
    /* Narrow enough to leave the played cards visible beside it — hence 3 chips
       abreast and no "pts". */
    #bet-bar button {
        min-width: 52px;
        min-height: 38px;
        padding: 5px 8px;
        font-size: 12px;
    }
    #bet-bar button.chip-btn { min-width: 0; min-height: 0; padding: 2px; }
    #bet-bar button small { display: none; }
    .bet-chip.chip-lg { width: 40px; height: 40px; }
}

#bet-bar:empty, #decision-bar:empty {
    display: none; padding: 0; border: none; background: none; box-shadow: none;
}

#bet-bar button, #decision-bar button {
    min-width: 64px;
    min-height: 46px;
    padding: 10px 22px;
    border-radius: 9px;
    border: 1px solid rgba(255, 255, 255, 0.18);
    background: rgba(16, 48, 52, 0.9);
    color: rgba(226, 240, 240, 0.92);
    font-family: var(--dk-font-ui);
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.04em;
    cursor: pointer;
    touch-action: manipulation;
    transition: background 0.15s, border-color 0.15s, opacity 0.15s;
}
#bet-bar button:hover:not(:disabled), #decision-bar button:hover:not(:disabled) {
    background: rgba(26, 68, 72, 0.95);
    border-color: rgba(140, 226, 218, 0.7);
}
#bet-bar button:disabled, #decision-bar button:disabled { opacity: 0.3; cursor: default; }
#bet-bar button.primary, #decision-bar button.primary {
    background: rgba(34, 160, 146, 0.92);
    border-color: rgba(70, 191, 176, 0.55);
    color: white;
}
#bet-bar button.primary:hover:not(:disabled), #decision-bar button.primary:hover:not(:disabled) { background: rgba(46, 190, 172, 1); }
#bet-bar button.primary:disabled, #decision-bar button.primary:disabled { background: #4a5550; }

/* A bet rung is the chip itself — no plate behind it, so the printed component reads
   as one instead of as a picture pasted on a button. It keeps the button's grid cell
   as its hit area, and takes a lift on hover / press in place of the fill change it
   just gave up. MUST come after the .primary rules above (same specificity). */
#bet-bar button.chip-btn {
    min-width: 0;
    padding: 2px;
    background: none;
    border: none;
    box-shadow: none;
    line-height: 0;
    transition: transform 0.12s ease-out;
}
#bet-bar button.chip-btn:hover:not(:disabled) {
    background: none;
    transform: translateY(-3px) scale(1.05);
}
#bet-bar button.chip-btn:active:not(:disabled) { transform: translateY(0) scale(0.96); }
#bet-bar button.chip-btn:focus-visible {
    outline: 2px solid var(--dk-accent);
    outline-offset: 3px;
    border-radius: 50%;
}
#decision-bar button.danger {
    background: rgba(192, 57, 43, 0.9);
    border-color: rgba(231, 76, 60, 0.5);
    color: white;
}
#decision-bar button.danger:hover:not(:disabled) { background: rgba(231, 76, 60, 1); }
#decision-bar button.danger:disabled { background: #4a5550; }
#bet-bar button small, #decision-bar button small {
    display: block;
    font-size: 10px;
    font-weight: 400;
    opacity: 0.75;
    letter-spacing: 0.02em;
}

/* ── Round-end / game-over dialog contents ─────────────────────────────────── */
/* The .overlay / .panel shell and the .primary-btn come from deckades.css; only what
   goes INSIDE the panel is styled here. */

.modal-eyebrow {
    font-family: var(--dk-font-ui);
    font-size: 0.78rem;
    letter-spacing: 0.30em;
    text-transform: uppercase;
    color: var(--dk-accent);
    opacity: 0.75;
}

#round-scores, #game-over-ranking {
    display: flex;
    flex-direction: column;
    gap: 7px;
    margin: 4px 0 10px;
}
.score-row {
    display: grid;
    grid-template-columns: 28px 1fr auto;
    align-items: center;
    gap: 12px;
    padding: 9px 16px;
    border-radius: 9px;
    background: rgba(0, 0, 0, 0.22);
    border: 1px solid rgba(140, 226, 218, 0.1);
}
.score-row.rank-1 { background: rgba(70, 191, 176, 0.14); border-color: var(--dk-accent-soft); }
.score-row.rank-2 { border-color: rgba(192, 192, 192, 0.25); }
.score-row.rank-3 { border-color: rgba(184, 115, 51, 0.25); }
.score-medal {
    font-size: 1.25rem; font-weight: 700; text-align: center; line-height: 1;
}
.rank-1 .score-medal { color: var(--dk-accent); }
.rank-2 .score-medal { color: #9eaab4; }
.rank-3 .score-medal { color: #b87333; }
.score-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; text-align: left; }
.score-name {
    text-align: left; font-size: 0.95rem; font-weight: 600;
    color: rgba(140, 226, 218, 0.88);
}
.score-breakdown {
    font-size: 0.72rem;
    color: rgba(226, 240, 240, 0.62);
    letter-spacing: 0.02em;
    line-height: 1.3;
}
.score-name .role-tag {
    font-size: 0.7rem; color: rgba(226, 240, 240, 0.5);
    margin-left: 6px; letter-spacing: 0.04em;
}
.score-delta {
    font-family: var(--dk-font-ui);
    font-size: 1.1rem; font-weight: 700;
    color: rgba(140, 226, 218, 0.85);
    text-align: right;
}
.score-delta small { font-size: 0.7rem; opacity: 0.6; font-weight: 400; }

/* ── Publisher credit ──────────────────────────────────────────────────────── */
/* Bottom-left corner, always on: the ranking owns top-left, the bet bar bottom-right
   and the decision bar the bottom centre, so this is the one free corner. z-index sits
   above the fan's cards (max 100 on a selected card) but below the bet/decision bars
   (120/130) and every dialog, and pointer-events:none keeps it out of the way of a
   click meant for a card underneath. Deliberately quiet — it is a credit, not UI. */
#pgc-credit {
    position: fixed;
    left: 12px;
    bottom: 10px;
    z-index: 110;
    pointer-events: none;
    user-select: none;
    font-family: var(--dk-font-ui);
    font-size: 0.72rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: rgba(198, 240, 236, 0.42);
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.75);
}
@media (max-height: 480px) {
    #pgc-credit { left: 8px; bottom: 6px; font-size: 0.62rem; }
}

/* ── Banner flash ──────────────────────────────────────────────────────────── */

@keyframes banner-pop {
    0%   { opacity: 0; transform: translate(-50%,-50%) scale(0.4); }
    18%  { opacity: 1; transform: translate(-50%,-50%) scale(1.12) rotate(-2deg); }
    32%  { transform: translate(-50%,-50%) scale(0.96) rotate(1deg); }
    48%  { transform: translate(-50%,-50%) scale(1.04); }
    65%  { opacity: 1; }
    100% { opacity: 0; transform: translate(-50%,-50%) scale(1.0); }
}
