:root{
    --accent:#3fae5a;
    --accent-hover:#48c667;
    --danger:#d64545;
    --text:#f4ece0;
    --muted:#a99e8e;
    --wood:#5a3a22;
    --wood-light:#7a5233;
    --panel:rgba(20,14,10,0.72);
    --panel-border:rgba(255,255,255,0.08);
    --radius:16px;
    --shadow:0 18px 50px rgba(0,0,0,0.45);
    /* Vertical space one seat name takes (font-size 13px x line-height 1.2). Top-half seats
       hang their name above the avatar, so the table is pushed down by twice this. */
    --seat-name-h:16px;
    /* Player-list width, shared with the collapse handle that parks against its right edge. */
    --pl-w:266px;
    /* Player-list top: clears the top bar and repeats the Rules button's 14px gap to the top. */
    --pl-top:65px;
}

*{box-sizing:border-box;}
html,body{height:100%;margin:0;}
body{
    color:var(--text);
    min-height:100vh;
    font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    display:flex;
    flex-direction:column;
    background:
        radial-gradient(1200px 700px at 50% -10%, #33251a 0%, transparent 60%),
        radial-gradient(900px 600px at 50% 120%, #14100c 0%, transparent 55%),
        linear-gradient(160deg,#221812,#0f0b08);
    background-attachment:fixed;
}

/* ---------- Top bar ---------- */
.topbar{
    position:fixed; top:0; left:0; right:0; z-index:40;
    display:flex; justify-content:space-between; align-items:center;
    padding:14px 18px;
    pointer-events:none;   /* let clicks fall through the empty middle to the Lobby button */
}
.topbar-left{display:flex; gap:10px; align-items:center;}
.topbar-right{display:flex; gap:10px; align-items:center; margin-left:auto;}
.topbar-title{
    pointer-events:none;
    font-weight:800; font-size:clamp(15px,2.4vw,22px);
    letter-spacing:.01em; color:var(--text);
    white-space:nowrap;
    text-shadow:0 1px 3px rgba(0,0,0,0.45);
}
.topbar-btn{
    appearance:none; border:1px solid var(--panel-border); background:var(--panel);
    color:var(--text); padding:9px 14px; border-radius:12px; font-size:14px; font-weight:600;
    cursor:pointer; display:inline-flex; align-items:center; gap:8px;
    pointer-events:auto;   /* but the actual buttons stay clickable */
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);
    transition:transform .12s ease, background .2s ease, border-color .2s ease;
}
.topbar-btn:hover{transform:translateY(-1px); border-color:rgba(255,255,255,0.22);}
.topbar-btn:active{transform:translateY(0);}
.topbar-btn[hidden]{display:none;}   /* the class sets inline-flex, so [hidden] needs to win */
.reset-game-btn{border-color:rgba(214,69,69,.5); color:#ffd6d0; background:rgba(214,69,69,.16);}
.reset-game-btn:hover{border-color:rgba(214,69,69,.8); background:rgba(214,69,69,.28);}
/* Drunk Pirate host turn controls: light-red Skip + pink Redraw. */
.pirate-skip-btn{border-color:rgba(240,120,110,.55); color:#ffdbd5; background:rgba(240,120,110,.20);}
.pirate-skip-btn:hover{border-color:rgba(240,120,110,.85); background:rgba(240,120,110,.32);}
.pirate-redraw-btn{border-color:rgba(232,110,178,.55); color:#ffd8ef; background:rgba(232,110,178,.20);}
.pirate-redraw-btn:hover{border-color:rgba(232,110,178,.85); background:rgba(232,110,178,.32);}
/* "Game settings": visible/clickable to everyone in the lobby (read-only panel for non-hosts),
   but only tinted green — reading as a privileged control — while the viewer IS the host. */
#game-settings-btn.is-host{border-color:rgba(63,174,90,.38); color:#e4f2e8; background:rgba(63,174,90,.12);}
#game-settings-btn.is-host:hover{border-color:rgba(63,174,90,.6); background:rgba(63,174,90,.2);}
#game-settings-btn.is-host.is-open{border-color:rgba(63,174,90,.72); background:rgba(63,174,90,.25);}
.settings-btn{padding:8px 10px;}
.settings-btn.is-open{border-color:rgba(255,255,255,.3); background:rgba(255,255,255,.1);}
.settings-icon{width:22px; height:22px; display:block;}

/* Background-music toggle: two icon states — music-off (default) / music (playing). */
.music-btn{padding:8px 10px;}
.music-btn .mic{display:none; align-items:center; justify-content:center;}
.music-btn .mic img, .music-btn .mic .mic-svg{width:22px; height:22px; display:block;}
.music-btn .ic-off{display:inline-flex;}                    /* default: music off */
.music-btn.is-on .ic-off{display:none;}
.music-btn.is-on .ic-on{display:inline-flex;}               /* playing, audible */
.music-btn.is-on{border-color:rgba(72,198,103,.55); background:rgba(72,198,103,.14);}
.music-btn.is-loading{opacity:.6; cursor:progress;}
/* The player must stay in the DOM (not display:none) to keep audio alive — hide it off to the side. */
#yt-music-wrap{position:fixed; left:-9999px; bottom:0; width:1px; height:1px; overflow:hidden; opacity:0; pointer-events:none;}

/* Settings dropdown (game page) */
.settings-panel{position:fixed; top:60px; right:14px; z-index:55; width:min(300px,92vw);
    display:flex; flex-direction:column; gap:12px; padding:16px;
    background:linear-gradient(180deg,#241a13,#1b130d); border:1px solid var(--panel-border);
    border-radius:var(--radius); box-shadow:var(--shadow);}
.settings-panel[hidden]{display:none;}
.settings-heading{margin:0 0 2px; font-size:16px;}
#settings-customizer{margin:0;}
#settings-customizer .avatar-stage{width:92px; height:92px;}
.settings-row{display:flex; align-items:center; justify-content:space-between; gap:10px;}
.settings-label{font-size:14px; color:var(--muted);}
.lang-toggle{display:flex; gap:6px;}
.lang-opt{appearance:none; border:1px solid var(--panel-border); background:rgba(255,255,255,.05); color:var(--text);
    font-size:13px; font-weight:700; padding:5px 9px; border-radius:8px; cursor:pointer; transition:background .14s ease, border-color .14s ease;
    display:inline-flex; align-items:center; justify-content:center; min-width:44px;}
.lang-opt.is-active{border-color:var(--accent); background:rgba(63,174,90,.18); color:#dff5e4;}
.lang-flag{width:28px; height:20px; object-fit:cover; display:block; border-radius:3px; box-shadow:0 1px 3px rgba(0,0,0,.35);}
.settings-leave{margin-top:2px; text-align:center;}
.settings-slider{display:flex; flex-direction:column; gap:6px;}
.settings-slider .settings-label{display:flex; justify-content:space-between; align-items:baseline; color:var(--text);}
/* On-off toggle in front of the slider; the slider takes whatever width is left over. */
.settings-slider-row{display:flex; align-items:center; gap:10px;}
.settings-slider-row .vol-range{flex:1 1 auto; min-width:0;}
.vol-toggle{flex:0 0 auto; appearance:none; cursor:pointer; padding:5px 7px; border-radius:9px;
    border:1px solid var(--panel-border); background:rgba(255,255,255,.05); color:var(--text);
    transition:background .14s ease, border-color .14s ease;}
.vol-toggle:hover{background:rgba(255,255,255,.13);}
.vol-toggle .mic img, .vol-toggle .mic .mic-svg{width:18px; height:18px;}
.vol-toggle.is-on{border-color:rgba(72,198,103,.55); background:rgba(72,198,103,.14);}
.vol-val{font-size:12px; font-weight:700; color:var(--muted); font-variant-numeric:tabular-nums;}
/* Turn-chime picker: Off + the five sounds, sharing the row evenly. "Off" carries a word rather
   than a digit, so the cells are flexed instead of fixed-width. */
.turn-sfx-opts{display:flex; gap:4px;}
.turn-sfx-opt{flex:1 1 0; min-width:0; appearance:none; cursor:pointer;
    border:1px solid var(--panel-border); background:rgba(255,255,255,.05); color:var(--text);
    font-family:inherit; font-size:12px; font-weight:700; padding:6px 4px; border-radius:8px;
    transition:background .14s ease, border-color .14s ease;}
.turn-sfx-opt:hover{background:rgba(255,255,255,.13);}
.turn-sfx-opt.is-active{border-color:var(--accent); background:rgba(63,174,90,.18); color:#dff5e4;}
.vol-range{width:100%; accent-color:var(--accent); cursor:pointer; height:22px;}
.flag{width:26px; height:13px; border-radius:3px; display:block; box-shadow:0 1px 3px rgba(0,0,0,.4);}
.lang-label{font-size:12px; letter-spacing:.06em; color:var(--muted);}

/* ---------- Home page confetti (falling pixels behind the title) ---------- */
.confetti{position:fixed; inset:0; overflow:hidden; pointer-events:none; z-index:2;}
.confetti-piece{position:absolute; top:-12px; opacity:.85; animation-name:confettiFall; animation-timing-function:linear; animation-iteration-count:infinite;}
@keyframes confettiFall{
    0%{transform:translate(0,0) rotate(0deg);}
    100%{transform:translate(var(--drift,0), 112vh) rotate(360deg);}
}
@media (prefers-reduced-motion: reduce){.confetti{display:none;}}

/* Champion confetti: same falling-pixel animation as the home page's birthday confetti (see
   .confetti / confettiFall above), just re-parented so it can sit over the champion panel
   (z-index above .pirate-champ's 60) instead of the whole home page. */
.pirate-champ-confetti{position:fixed; inset:0; overflow:hidden; pointer-events:none; z-index:61;}
@media (prefers-reduced-motion: reduce){.pirate-champ-confetti{display:none;}}

/* ---------- Home layout ---------- */
.home-main{flex:1; display:flex; align-items:center; justify-content:center; padding:90px 18px 70px;}
.home-card{width:100%; max-width:360px; display:flex; flex-direction:column; align-items:center; gap:16px;}
.home-title{margin:0 0 clamp(18px,5vh,50px); text-align:center; white-space:nowrap; font-size:clamp(28px,9vw,74px); font-weight:800; letter-spacing:-.02em; line-height:1.05; color:var(--text);}
/* Game picker: three cover images (2:1) as select buttons. Wider than the input
   card so each cover is ~2x size; even 16px gaps (the card's gap) above and below. */
/* Up to --cols games sit in a row, and a row that doesn't fill is CENTRED — with 7 games the
   second row holds 2, and grid would strand them against the left edge. Hence flex-wrap plus an
   explicit basis rather than a grid: the basis reproduces exactly what repeat(--cols, 1fr) gave
   (the row's width minus its gaps, split --cols ways), so a full row looks identical either way.
   Only --cols changes at the breakpoints; the arithmetic is written once. */
.game-select{--cols:5; --cover-gap:12px;
    width:min(94vw,1050px); display:flex; flex-wrap:wrap; justify-content:center;
    gap:var(--cover-gap); margin:12px 0;}
.game-select > .game-cover{
    flex:0 0 calc((100% - (var(--cols) - 1) * var(--cover-gap)) / var(--cols));}
@media (max-width:640px){
    /* 5 across gets unreadably small on a phone — wrap into 3, then 2 columns as it narrows
       further (a 2:1 cover still reads fine tiny in width, but not this tiny). */
    .game-select{--cols:3;}
}
@media (max-width:420px){
    .game-select{--cols:2;}
}
.game-cover{appearance:none; padding:0; cursor:pointer; border:2px solid var(--panel-border); background:#0d0a07;
    border-radius:12px; overflow:hidden; aspect-ratio:2 / 1;
    transition:transform .12s ease, border-color .16s ease, box-shadow .16s ease;}
.game-cover img{width:100%; height:100%; object-fit:cover; display:block;}
.game-cover:hover{transform:translateY(-2px); border-color:rgba(255,255,255,.28);}
.game-cover:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
.game-cover.is-selected{border-color:var(--accent); box-shadow:0 0 0 3px rgba(63,174,90,.32);}
.game-cover:not(.is-selected){opacity:.72;}
/* An unfinished game (admin-only): drained of colour and inert. The button also carries the
   `disabled` attribute, and the server refuses to create the lobby anyway — this is the look,
   not the lock. See get_unreleased_game_types() in lib/games.php. */
.game-cover.is-locked{cursor:not-allowed; opacity:.34; filter:grayscale(1);}
.game-cover.is-locked:hover{transform:none; border-color:var(--panel-border);}

/* Avatar carousel */
.avatar-carousel{display:flex; align-items:center; gap:12px; margin-bottom:4px;}
.avatar-stage{
    width:140px; height:140px; border-radius:50%; overflow:hidden;
    border:4px solid var(--wood-light);
    box-shadow:var(--shadow), inset 0 0 0 2px rgba(0,0,0,.25);
    background:#0d0a07; flex:none;
}
.avatar-slot{display:none; width:100%; height:100%;}
.avatar-slot.is-active{display:block;}
.avatar-svg{width:100%; height:100%; display:block;}
.carousel-arrow{
    appearance:none; width:44px; height:44px; border-radius:50%;
    border:1px solid var(--panel-border); background:var(--panel); color:var(--text);
    font-size:26px; line-height:1; cursor:pointer; flex:none;
    display:flex; align-items:center; justify-content:center;
    transition:transform .12s ease, background .2s ease;
}
.carousel-arrow:hover{transform:scale(1.06); background:rgba(255,255,255,.08);}
.carousel-arrow:active{transform:scale(.95);}

/* 3-axis avatar picker: one live preview flanked by three arrow rows (hair/skin/shirt). */
.avatar-customizer{
    display:grid;
    grid-template-columns:auto auto auto auto;
    grid-template-rows:auto auto auto;
    grid-template-areas:
        "lblH aLH av aRH"
        "lblS aLS av aRS"
        "lblP aLP av aRP";
    align-items:center; justify-content:center;
    column-gap:8px; row-gap:8px; margin-bottom:4px;
}
.cust-stage{grid-area:av; margin:0 4px;}
.cust-lbl{font-size:12px; font-weight:700; letter-spacing:.03em; color:var(--muted); text-align:right; white-space:nowrap;}
.cust-lbl[data-part="hair"]{grid-area:lblH;}
.cust-lbl[data-part="skin"]{grid-area:lblS;}
.cust-lbl[data-part="shirt"]{grid-area:lblP;}
.cust-arrow{
    appearance:none; width:34px; height:34px; border-radius:50%;
    border:1px solid var(--panel-border); background:var(--panel); color:var(--text);
    font-size:20px; line-height:1; cursor:pointer; padding:0;
    display:flex; align-items:center; justify-content:center;
    transition:transform .12s ease, background .2s ease;
}
.cust-arrow:hover{transform:scale(1.08); background:rgba(255,255,255,.08);}
.cust-arrow:active{transform:scale(.94);}
.cust-arrow[data-part="hair"][data-dir="-1"]{grid-area:aLH;}
.cust-arrow[data-part="hair"][data-dir="1"]{grid-area:aRH;}
.cust-arrow[data-part="skin"][data-dir="-1"]{grid-area:aLS;}
.cust-arrow[data-part="skin"][data-dir="1"]{grid-area:aRS;}
.cust-arrow[data-part="shirt"][data-dir="-1"]{grid-area:aLP;}
.cust-arrow[data-part="shirt"][data-dir="1"]{grid-area:aRP;}
/* Home page: no part labels — just the centered avatar flanked by its arrow rows. */
#home-customizer{
    grid-template-columns:auto auto auto;
    grid-template-areas:
        "aLH av aRH"
        "aLS av aRS"
        "aLP av aRP";
}
#home-customizer .cust-lbl{display:none;}

/* Inputs */
.text-input{
    width:100%; padding:14px 16px; border-radius:12px; font-size:16px;
    border:1px solid var(--panel-border); background:rgba(0,0,0,.28); color:var(--text);
    outline:none; transition:border-color .2s ease, box-shadow .2s ease;
}
.text-input::placeholder{color:var(--muted);}
.text-input:focus{border-color:var(--accent); box-shadow:0 0 0 3px rgba(63,174,90,.22);}
#lobby-code{letter-spacing:.18em; text-transform:uppercase; text-align:center; font-weight:700;}
#player-name{text-align:center; font-weight:700; letter-spacing:.04em;}

/* Start button */
.btn-start{
    width:100%; padding:15px 16px; border:none; border-radius:12px; cursor:pointer;
    font-size:18px; font-weight:800; letter-spacing:.02em; color:#0c2413;
    background:linear-gradient(180deg,var(--accent-hover),var(--accent));
    box-shadow:0 10px 24px rgba(63,174,90,.35), inset 0 1px 0 rgba(255,255,255,.35);
    transition:transform .12s ease, box-shadow .2s ease, filter .2s ease;
}
.btn-start:hover{transform:translateY(-2px); filter:brightness(1.05);}
.btn-start:active{transform:translateY(0);}

.field-error{color:#ffb0a3; font-size:13px; align-self:flex-start; margin:-8px 0 0;}
.field-error[hidden]{display:none;}

@keyframes shake{
    10%,90%{transform:translateX(-1px);}
    20%,80%{transform:translateX(2px);}
    30%,50%,70%{transform:translateX(-5px);}
    40%,60%{transform:translateX(5px);}
}
.shake{animation:shake .5s;}

/* Voice note */
/* ~5x the original 13px (clamped so it doesn't overflow narrow phones), with a
   comfortable gap above the window's bottom edge. */
.voice-note{text-align:center; color:var(--muted); font-size:clamp(10.5px,2.1vw,23px); line-height:1.25; padding:14px 18px 48px;}

/* ---------- Modal ---------- */
.modal{position:fixed; top:0; left:0; right:0; bottom:0; z-index:60; display:flex; align-items:center; justify-content:center; padding:20px;}
.modal[hidden]{display:none;}
.modal-backdrop{position:absolute; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,.6); -webkit-backdrop-filter:blur(3px); backdrop-filter:blur(3px);}
.modal-card{
    position:relative; width:100%; max-width:460px; max-height:82vh; overflow:auto;
    background:linear-gradient(180deg,#241a13,#1b130d); border:1px solid var(--panel-border);
    border-radius:var(--radius); padding:24px 24px 20px; box-shadow:var(--shadow);
}
.modal-card h2{margin:0 0 12px; font-size:20px;}
.modal-close{position:absolute; top:12px; right:14px; border:none; background:transparent; color:var(--muted); font-size:26px; cursor:pointer; line-height:1;}
.modal-close:hover{color:var(--text);}
.rules-body{font-size:14px; line-height:1.55; color:#e7ddcd;}
.rules-body ol{padding-left:20px; margin:0;}
.rules-body li{margin-bottom:9px;}
.rules-foot{margin-top:14px; color:var(--muted); font-style:italic;}

/* ---------- Cookie consent (bottom third, centred; same card look as .modal-card) ---------- */
.cookie-consent{
    position:fixed; left:50%; bottom:12vh; transform:translateX(-50%); z-index:90;
    width:min(560px, 92vw); pointer-events:none;
}
.cookie-consent[hidden]{display:none;}
.cookie-consent-card{
    pointer-events:auto; text-align:center;
    background:linear-gradient(180deg,#241a13,#1b130d); border:1px solid var(--panel-border);
    border-radius:var(--radius); padding:20px 22px 18px; box-shadow:var(--shadow);
}
.cookie-consent-text{margin:0 0 14px; font-size:15px; line-height:1.5; color:#e7ddcd;}
.cookie-consent-actions{display:flex; gap:10px; flex-wrap:wrap; justify-content:center;}
@media (max-width:640px){
    .cookie-consent{bottom:8vh;}
    .cookie-consent-card{padding:16px 16px 14px;}
    .cookie-consent-text{font-size:14px;}
    .cookie-consent-actions .gbtn{padding:11px 14px; font-size:14px;}
}

/* ---------- Game page (M1 placeholder) ---------- */
.game-main{flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:20px; padding:90px 18px 40px; text-align:center;}
.lobby-banner{font-size:15px; color:var(--muted); letter-spacing:.04em;}
.lobby-banner strong{color:var(--text); font-size:20px; letter-spacing:.22em; margin-left:8px;}
.table-preview{width:min(560px,92vw); height:auto; filter:drop-shadow(0 20px 40px rgba(0,0,0,.5));}
.game-hint{color:var(--muted); max-width:440px; line-height:1.5;}
.game-hint strong{color:var(--text);}

@media (max-width:420px){
    .avatar-stage{width:120px; height:120px;}
    .home-main{padding-top:80px;}
    .lang-label{display:none;}
}

/* ---------- Game board (M3) ---------- */
/* flex-start (not center) so the table keeps a fixed position and never jumps up when
   the dice/buttons appear below it — the panel just grows downward (scroll if needed). */
/* Extra top padding = a seat's radius, so the top seat clears the fixed lobby-code tag, plus
   two name-heights of headroom for the top-half seats that hang their name ABOVE the avatar. */
.page-game .game-main{justify-content:flex-start; gap:14px;
    padding:calc(124px + var(--seat-name-h) * 2) 14px 30px;}
.lobby-tag{position:fixed; top:14px; left:50%; transform:translateX(-50%); z-index:35;
    appearance:none; cursor:pointer;
    color:var(--muted); font-size:22px; font-family:inherit; letter-spacing:.04em; text-align:center;
    background:var(--panel); border:1px solid var(--panel-border); border-radius:12px; padding:8px 18px;
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px); box-shadow:var(--shadow);
    transition:transform .12s ease, border-color .2s ease, background .2s ease;}
.lobby-tag:hover{transform:translateX(-50%) translateY(-1px); border-color:rgba(255,255,255,.22); background:rgba(255,255,255,.06);}
.lobby-tag:active{transform:translateX(-50%) translateY(0);}
.lobby-tag strong{color:var(--text); letter-spacing:.22em; margin-left:6px; font-size:28px;}

/* Capped by width (never wider than 940px) AND by viewport height (65vh) — without the height
   cap, the table stays a fixed 940px/529px box on any screen wider than ~980px, so it eats a
   much bigger share of a short 1080p viewport than a tall 1440p one. 65vh keeps its footprint
   proportionally the same on both (calibrated so 1440p, the reference, is unchanged). */
.table-wrap{width:min(96vw, 940px, 65vh);}
/* 16:9 box via padding (works on all browsers, unlike aspect-ratio). */
.table-felt{position:relative; width:100%; height:0; padding-bottom:56.25%;}
/* The visual green oval is a smaller box centered inside .table-felt, so seats
   (positioned against .table-felt itself, unchanged) end up sitting outside it. */
.table-oval{position:absolute; left:0; top:0; width:100%; height:100%;}
.table-svg{position:absolute; top:0; left:0; width:100%; height:100%; filter:drop-shadow(0 24px 44px rgba(0,0,0,.5));}
/* Per-game table visuals (cup/arrow/cards) are injected here by the active game module.
   Full-size overlay on the felt so its children position with the same % coords as before. */
.game-layer{position:absolute; left:0; top:0; width:100%; height:100%; pointer-events:none;}
/* Hidden for the whole lobby phase (game-core), so no game's table props show before the start. */
.game-layer[hidden]{display:none;}

/* Game cover lying on the felt while the lobby waits — the table is otherwise empty before the
   host starts, giving no clue which game you're in. Covers are 2:1 banners. */
.table-cover{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); z-index:4;
    width:42%; max-width:390px; pointer-events:none;}
.table-cover[hidden]{display:none;}
.table-cover img{display:block; width:100%; height:auto; border-radius:12px;
    border:1px solid rgba(255,255,255,.14); box-shadow:0 16px 38px rgba(0,0,0,.55);}

/* Idle-shutdown warning, centred on the felt. Sits above the table props because when it's up
   nobody is playing anyway — and it's click-through, so the very move that dismisses it is never
   blocked. Explicit [hidden] rule because the display below would otherwise beat the UA default. */
.idle-warn{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); z-index:12;
    display:flex; flex-direction:column; align-items:center; gap:4px; text-align:center;
    width:max-content; max-width:84%; padding:12px 20px; border-radius:14px;
    background:rgba(12,8,4,.82); border:2px solid rgba(255,193,7,.7);
    box-shadow:0 10px 30px rgba(0,0,0,.55), 0 0 22px rgba(255,193,7,.18);
    -webkit-backdrop-filter:blur(3px); backdrop-filter:blur(3px);
    pointer-events:none; animation:idleWarnBreathe 2.2s ease-in-out infinite;}
.idle-warn[hidden]{display:none;}
.idle-warn-title{color:#ffd54a; font-weight:800; font-size:clamp(14px,2.4vw,21px); letter-spacing:.01em;
    text-shadow:0 2px 8px rgba(0,0,0,.7);}
.idle-warn-left{color:var(--text); font-size:clamp(12px,1.9vw,16px); text-shadow:0 2px 6px rgba(0,0,0,.7);}
@keyframes idleWarnBreathe{
    0%,100%{border-color:rgba(255,193,7,.5);  box-shadow:0 10px 30px rgba(0,0,0,.55), 0 0 18px rgba(255,193,7,.14);}
    50%    {border-color:rgba(255,213,74,.95); box-shadow:0 10px 30px rgba(0,0,0,.55), 0 0 30px rgba(255,193,7,.42);}}

/* "Connection lost", same placement and click-through behaviour as the idle warning above (and
   the same [hidden] caveat), but red rather than amber: the idle warning is about the lobby, this
   one is about this screen only. They can't collide — an unreachable server sends no countdown. */
.conn-lost{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); z-index:13;
    display:flex; flex-direction:column; align-items:center; gap:4px; text-align:center;
    width:max-content; max-width:84%; padding:12px 20px; border-radius:14px;
    background:rgba(14,6,6,.85); border:2px solid rgba(214,69,69,.75);
    box-shadow:0 10px 30px rgba(0,0,0,.55), 0 0 22px rgba(214,69,69,.2);
    -webkit-backdrop-filter:blur(3px); backdrop-filter:blur(3px);
    pointer-events:none; animation:connLostBreathe 1.6s ease-in-out infinite;}
.conn-lost[hidden]{display:none;}
.conn-lost-title{color:#ff8a8a; font-weight:800; font-size:clamp(14px,2.4vw,21px);
    text-shadow:0 2px 8px rgba(0,0,0,.7);}
.conn-lost-sub{color:var(--text); font-size:clamp(12px,1.9vw,16px); text-shadow:0 2px 6px rgba(0,0,0,.7);}
@keyframes connLostBreathe{
    0%,100%{border-color:rgba(214,69,69,.55); box-shadow:0 10px 30px rgba(0,0,0,.55), 0 0 18px rgba(214,69,69,.16);}
    50%    {border-color:rgba(255,120,120,.95); box-shadow:0 10px 30px rgba(0,0,0,.55), 0 0 30px rgba(214,69,69,.45);}}

.table-center{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:24%; max-width:180px;}
.direction-arrow{width:100%; color:#e7d3a8; opacity:.55; transition:transform .4s ease;}
.direction-arrow.ccw{transform:scaleX(-1);}   /* mirror when the play direction reverses */
.direction-arrow svg, .direction-arrow .dir-img{width:100%; height:auto; display:block;}
/* "Meier!" shout, pulsing above the direction arrows while the call is pending. */
.meier-shout{position:absolute; left:50%; bottom:calc(100% + 6px); transform:translateX(-50%);
    color:#ffd54a; font-size:clamp(20px,3.4vw,40px); font-weight:900; letter-spacing:.03em; white-space:nowrap;
    text-shadow:0 2px 8px rgba(0,0,0,.7), 0 0 14px rgba(255,193,7,.55); pointer-events:none;
    animation:meierBreathe 1.1s ease-in-out infinite;}
.meier-shout[hidden]{display:none;}
@keyframes meierBreathe{0%,100%{transform:translateX(-50%) scale(.86);} 50%{transform:translateX(-50%) scale(1.14);}}
.cup{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:10%; width:clamp(48px,10%,86px); z-index:5;
    transition:left .6s cubic-bezier(.4,0,.2,1), top .6s cubic-bezier(.4,0,.2,1);}
.cup svg{width:100%; height:auto; display:block; filter:drop-shadow(0 8px 10px rgba(0,0,0,.45));}
.cup.shaking{animation:cupShake .55s ease 3;}   /* concept 5.1: shake 3x */
.cup.tilted{transform:translate(-50%,-50%) rotate(-13deg) translateY(-4%);}
@keyframes cupShake{
    0%,100%{transform:translate(-50%,-50%) rotate(0deg);}
    15%{transform:translate(-54%,-53%) rotate(-11deg);}
    30%{transform:translate(-46%,-53%) rotate(11deg);}
    45%{transform:translate(-54%,-52%) rotate(-9deg);}
    60%{transform:translate(-46%,-52%) rotate(9deg);}
    80%{transform:translate(-50%,-51%) rotate(-4deg);}
}

.seats{position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none;}
/* Only the avatar circle (.seat-inner) is centered on the seat's ellipse point — the name
   and leave button are positioned absolutely below it so they don't pull the centering
   transform's anchor down, which previously made bottom seats look pulled inward and top
   seats pushed outward (the label's height was shifting the whole flex block's center). */
.seat{position:absolute; transform:translate(-50%,-50%);
    width:13%; width:clamp(60px,13%,100px); pointer-events:auto;}
.seat-inner{position:relative; width:100%; height:0; padding-bottom:100%; border-radius:50%; box-sizing:border-box;}
.seat.empty .seat-inner{border:2.5px dashed rgba(230,222,205,.5); background:rgba(0,0,0,.28); cursor:pointer;
    transition:border-color .18s ease, box-shadow .18s ease, background .18s ease;}
.seat.empty .seat-inner:hover{border-color:var(--accent); background:rgba(63,174,90,.12); box-shadow:0 0 0 4px rgba(63,174,90,.18);}
/* Once you hold a seat, empty seats no longer invite clicks. */
.seats.has-seat .seat.empty .seat-inner{cursor:default;}
.seats.has-seat .seat.empty .seat-inner:hover{border-color:rgba(230,222,205,.5); background:rgba(0,0,0,.28); box-shadow:none;}
.seat.occupied .seat-inner{border:3px solid var(--wood-light); background:#0d0a07; overflow:hidden; box-shadow:0 6px 16px rgba(0,0,0,.4);}
.seat.occupied.is-me .seat-inner{border-color:var(--accent); box-shadow:0 0 0 3px rgba(63,174,90,.35), 0 6px 16px rgba(0,0,0,.4);}
/* Whoever's turn it is: a white outline breathes around the avatar (same rhythm as the
   use-card glow, piratePick). */
.seat.occupied.seat-active-turn .seat-inner{animation:seatActivePulse 1.15s ease-in-out infinite;}
@keyframes seatActivePulse{
    0%,100%{box-shadow:0 6px 16px rgba(0,0,0,.4), 0 0 0 3px rgba(255,255,255,.55), 0 0 14px rgba(255,255,255,.35);}
    50%    {box-shadow:0 6px 16px rgba(0,0,0,.4), 0 0 0 4px rgba(255,255,255,.95), 0 0 22px rgba(255,255,255,.7);}
}
.seat-avatar{position:absolute; top:0; left:0; width:100%; height:100%;}
.seat-avatar svg{width:100%; height:100%; display:block;}
.seat-name{position:absolute; left:50%; top:100%; transform:translateX(-50%); margin-top:6px;
    font-size:13px; line-height:1.2; max-width:118%; text-align:center; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--text);}
/* Seats on the top half of the oval (and the exact left/right ones) hang their name ABOVE the
   avatar instead of below, so it points away from the table rather than over it. */
.seat.name-above .seat-name{top:auto; bottom:100%; margin-top:0; margin-bottom:6px;}
.seat.empty .seat-name{color:var(--muted);}
.seat-host-star{position:absolute; top:-6px; left:-6px; z-index:6; font-size:20px; line-height:1;
    color:#ffd54a; text-shadow:0 1px 2px rgba(0,0,0,.7), 0 0 6px rgba(255,193,7,.5); pointer-events:none;}
.seat-host-star[hidden]{display:none;}
/* A bot's difficulty, as stars riding the top of the avatar ring: 1 Dumb, 2 Normal, 3 Pro.
   A SIBLING of .seat-inner, never a child — game-core rewrites that node's children on every
   render, and an occupied seat clips them anyway (overflow:hidden).
   Each star gets an arm spanning the whole circle which rotates about its centre, so the star
   lands ON the ring without any radius arithmetic; --a is set per star in JS and the star itself
   is counter-rotated by the same angle so it stays upright however far round the arc it sits. */
.seat-bot-stars{position:absolute; inset:0; z-index:6; pointer-events:none;}
.seat-bot-stars[hidden]{display:none;}
.seat-bot-star{position:absolute; inset:0; transform:rotate(var(--a,0deg));}
.seat-bot-star b{position:absolute; left:50%; top:0; font-size:13px; line-height:1; color:#ffd54a;
    transform:translate(-50%,-50%) rotate(calc(-1 * var(--a,0deg)));
    text-shadow:0 1px 2px rgba(0,0,0,.85), 0 0 5px rgba(255,193,7,.55);}
/* Your own avatar doubles as the "Leave Seat" control: hovering it dims the avatar with a
   half-transparent black overlay and shows the label; a click leaves the seat. */
.seat.can-leave .seat-inner{cursor:pointer;}
.seat-leave-overlay{position:absolute; inset:0; z-index:5; border-radius:50%; cursor:pointer;
    display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,.5);
    opacity:0; transition:opacity .15s ease;}
.seat.can-leave .seat-inner:hover .seat-leave-overlay{opacity:1;}
.seat-leave-text{color:#fff; font-size:11px; font-weight:800; line-height:1.15; text-align:center; padding:0 6px;
    text-shadow:0 1px 3px rgba(0,0,0,.85);}

.spectator-note{text-align:center; color:var(--muted); font-size:14px; margin-top:4px;}

/* Toasts */
.toasts{position:fixed; top:66px; left:50%; transform:translateX(-50%); z-index:70;
    display:flex; flex-direction:column; gap:8px; align-items:center; pointer-events:none; width:max-content; max-width:90vw;}
.toast-msg{background:var(--panel); border:1px solid var(--panel-border); color:var(--text);
    padding:9px 16px; border-radius:20px; font-size:14px; box-shadow:var(--shadow);
    animation:toastIn .28s ease, toastOut .3s ease 3.2s forwards;}
@keyframes toastIn{from{opacity:0; transform:translateY(-8px);} to{opacity:1; transform:none;}}
@keyframes toastOut{to{opacity:0; transform:translateY(-8px);}}

/* Name-entry modal (reuses .modal / carousel) */
.modal-card--narrow{max-width:360px; display:flex; flex-direction:column; gap:12px; text-align:center;}
.modal-card--narrow h2{margin:0;}
.modal-card--narrow .avatar-carousel{justify-content:center; margin:0;}
.modal-card--narrow .avatar-stage{width:120px; height:120px;}
.modal-card--narrow .field-error{align-self:center; margin:0;}
.link-btn{display:block; margin:2px auto 0; background:none; border:none; color:var(--muted); font-size:13px; cursor:pointer; text-decoration:underline;}
.link-btn:hover{color:var(--text);}

/* Joined-players panel, top-left under the Rules button.
   The dock holds the list + its collapse handle in one flex row: align-items:center keeps the
   handle centred on the list at any height, and the row is top-anchored so the list can only
   grow downwards. --pl-top repeats the Rules button's own 14px gap to the top of the window
   (topbar padding 14px + button height 37px + the same 14px again).
   z-index 45 puts the list over the side card trays (36, 37 hovered) but under the game-settings
   dropdown (55), which must always stay on top of it. */
.player-dock{position:fixed; left:16px; top:var(--pl-top); z-index:45;
    display:flex; align-items:center; gap:6px; pointer-events:none;}
.player-dock > *{pointer-events:auto;}
.player-list{
    /* Fixed Name + Sips columns, then the actions. The last column is 1fr (not auto) so it eats
       the row's leftover width instead of letting it pool against the right edge — with the
       buttons right-aligned inside it, the content then ends on the padding, same as on the left. */
    --pl-cols:84px 58px 1fr;
    width:var(--pl-w); max-height:calc(100vh - var(--pl-top) - 16px); overflow-y:auto;
    padding:14px 14px 12px;
    background:var(--panel); border:1px solid var(--panel-border); border-radius:var(--radius);
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px); box-shadow:var(--shadow);
    transition:transform .22s ease, opacity .22s ease;}
.pl-title{margin:0 0 8px; text-align:center; font-size:14px; font-weight:800; color:var(--text);}
/* Collapsible, so side trays (e.g. Drunk Pirate's) can use the space: the list slides off the
   left edge and only the handle stays behind to bring it back. The list keeps its layout box
   while hidden, so the handle doesn't drift — it's moved to the window edge by its own offset. */
body.pl-collapsed .player-list{transform:translateX(calc(-100% - 24px)); opacity:0; pointer-events:none;}
.pl-toggle{flex:0 0 auto; appearance:none; cursor:pointer;
    width:22px; height:54px; padding:0; display:flex; align-items:center; justify-content:center;
    font-size:17px; font-weight:800; line-height:1; color:var(--muted);
    background:var(--panel); border:1px solid var(--panel-border); border-radius:0 10px 10px 0;
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px); box-shadow:var(--shadow);
    transition:transform .22s ease, color .14s ease, background .14s ease;}
.pl-toggle:hover{color:var(--text); background:rgba(255,255,255,.12);}
.pl-toggle::before{content:"\2039";}          /* ‹ - click to hide */
body.pl-collapsed .pl-toggle{transform:translateX(calc(-16px - var(--pl-w) - 6px));}
body.pl-collapsed .pl-toggle::before{content:"\203A";}   /* › - click to show */
.pl-head{display:grid; grid-template-columns:var(--pl-cols); align-items:center; column-gap:8px;
    margin:0 0 6px; padding-bottom:6px; border-bottom:1px solid var(--panel-border);
    font-size:10px; font-weight:800; letter-spacing:.02em; text-transform:uppercase; color:var(--muted);}
.pl-h-sips{text-align:center;}
/* Sip-free games (e.g. Drunk Pirate): drop the Sips column entirely — the name cell reclaims
   the space and the header/row sip cells are removed from the grid. */
.player-list.no-sips{--pl-cols:minmax(0,1fr) auto;}
.player-list.no-sips .pl-h-sips{display:none;}
/* Turn-skipping games (Drunk Pirate): reclaim the empty column as a host-toggled "AFK" column. */
.pl-h-afk{display:none; text-align:center;}
/* Fixed name + AFK columns (like the Sips layout) so the AFK column sits left-of-centre and every
   row's checkbox lines up under the header — a flexible name column would shift it per row. */
.player-list.uses-afk.no-sips{--pl-cols:84px 40px 1fr;}
.player-list.uses-afk .pl-h-afk{display:block;}
.pl-afk{display:flex; justify-content:center; align-items:center;}
.pl-afk-cb{width:15px; height:15px; cursor:pointer; accent-color:#d64545;}
.pl-afk-cb:disabled{cursor:default; opacity:.6;}
.player-list-items{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:6px;}
.player-list-items li{display:grid; grid-template-columns:var(--pl-cols); align-items:center; column-gap:8px;
    font-size:14px; color:var(--text);}
.pl-name{overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0;}
.pl-name.unseated{font-weight:800;}
/* An onlooker who never entered a name: listed so the table knows someone is there, but dimmed
   and italic so it never reads as a person with "Spectator" for a name. */
.pl-name.is-watcher{font-weight:600; font-style:italic; opacity:.6;}
.pl-sips{text-align:center; font-size:13px; font-weight:700; color:var(--text); font-variant-numeric:tabular-nums;}
.pl-actions{display:flex; gap:4px; justify-content:flex-end;}
.pl-btn{appearance:none; border:1px solid var(--panel-border); background:rgba(255,255,255,.06); color:var(--text);
    font-size:10px; font-weight:700; padding:2px 6px; border-radius:6px; cursor:pointer; line-height:1.5;
    transition:background .14s ease;}
.pl-btn:hover{background:rgba(255,255,255,.14);}
.pl-ban{border-color:rgba(214,69,69,.5); background:rgba(214,69,69,.16); color:#ffd6d0;}
.pl-ban:hover{background:rgba(214,69,69,.3);}
@media (max-width:900px){
    /* No room for a side drawer here: the dock becomes a normal block in the page flow, the list
       fills it, and the handle goes away. A stale .pl-collapsed from a resize must not hide it. */
    .player-dock{position:static; display:block; width:min(400px,92vw); margin:0 auto; order:-1;
        pointer-events:auto;}
    .player-list{width:100%; max-height:none;}
    body.pl-collapsed .player-list{transform:none; opacity:1; pointer-events:auto;}
    .pl-toggle{display:none;}
}

/* ---------- Lobby chat ---------- */
/* Same drawer pattern as the player list above — a panel plus a handle that stays behind when it
   slides away — but vertically centred, closed by default, and stacked above the whole board.
   z-index 54 is deliberate: over the table, the player list (45), the top bar (40) and every
   game's own furniture, but under the two top-bar dropdowns (55) which must always win.
   The panel has no fixed height: it grows with its contents up to half the window, and only then
   does the log start scrolling. --chat-w is shared with the handle's collapsed offset below. */
.chat-dock{--chat-w:20vw;
    position:fixed; left:16px; top:50%; transform:translateY(-50%); z-index:54;
    display:flex; align-items:center; gap:6px; pointer-events:none;}
.chat-dock > *{pointer-events:auto;}
.chat-panel{width:var(--chat-w); max-height:50vh; display:flex; flex-direction:column; gap:8px;
    padding:12px; box-sizing:border-box;
    background:var(--panel); border:1px solid var(--panel-border); border-radius:var(--radius);
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px); box-shadow:var(--shadow);
    transition:transform .22s ease, opacity .22s ease;}
/* Closed is the default state, so the collapse is written on :not(.chat-open) rather than on an
   "is closed" class — the drawer is shut before any script has run. It keeps its layout box while
   hidden (like the player list) so the handle beside it doesn't jump. */
body:not(.chat-open) .chat-panel{transform:translateX(calc(-100% - 24px)); opacity:0; pointer-events:none;}
.chat-title{margin:0; text-align:center; font-size:14px; font-weight:800; color:var(--text);}
/* min-height:0 lets this shrink inside the flex column — without it the log would refuse to go
   below its content height and the panel's max-height could never take effect. */
.chat-log{flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain;
    display:flex; flex-direction:column; gap:6px; padding-right:2px;}
.chat-empty{margin:auto; text-align:center; font-size:13px; color:var(--muted);}
/* flex:0 0 auto for the same reason the log rows carry it — a column flexbox squashes its children
   before it scrolls them. Nothing here hides overflow today, so this is prevention, not a fix. */
.chat-msg{flex:0 0 auto; font-size:13px; line-height:1.35; color:var(--text); overflow-wrap:anywhere;}
/* A seated player's name is painted with their SEAT colour from JS (the same one the Lobby Logs
   use — see SEAT_COLORS in game-core.js), so one person is one colour across both drawers. These
   two colours are only the fallback for someone holding no seat: a spectator, or a name from
   before they sat down. The inline colour wins over both, which is why "me" no longer gets the
   accent — being seated already says which one is you, in the same colour the log says it in. */
.chat-who{font-weight:800; color:var(--muted); margin-right:6px;}
.chat-who::after{content:":";}
.chat-text{color:var(--text);}
.chat-msg.is-me .chat-who{color:var(--accent);}
.chat-compose{display:flex; gap:6px; align-items:stretch;}
.chat-input{flex:1 1 auto; min-width:0; padding:8px 10px; border-radius:10px;
    border:1px solid var(--panel-border); background:rgba(0,0,0,.3); color:var(--text); font:inherit;
    font-size:13px;}
.chat-input:disabled{opacity:.55;}
.gbtn.chat-send{padding:8px 14px; font-size:13px; border-radius:10px; box-shadow:none; flex:0 0 auto;}
.gbtn.chat-send:hover{transform:none; filter:brightness(1.06);}

.chat-toggle{flex:0 0 auto; appearance:none; cursor:pointer;
    width:22px; height:54px; padding:0; display:flex; align-items:center; justify-content:center;
    font-size:17px; font-weight:800; line-height:1; color:var(--muted);
    background:var(--panel); border:1px solid var(--panel-border); border-radius:0 10px 10px 0;
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px); box-shadow:var(--shadow);
    transition:transform .22s ease, color .14s ease, background .14s ease;}
.chat-toggle:hover{color:var(--text); background:rgba(255,255,255,.12);}
.chat-toggle::before{content:"\203A";}                    /* › - click to show */
body.chat-open .chat-toggle::before{content:"\2039";}      /* ‹ - click to hide */
/* With the panel gone the handle slides over to the window edge, keeping it where the eye left it
   rather than stranded where the (still laid-out) panel used to end. */
body:not(.chat-open) .chat-toggle{transform:translateX(calc(-16px - var(--chat-w) - 6px));}
/* Somebody typed while the drawer was shut: breathe green until it's opened or the timer in
   game-core.js takes the class off again (5s). */
.chat-toggle.has-unread{color:#c8f5d4; border-color:rgba(63,174,90,.7);
    animation:chatUnread 1.15s ease-in-out infinite;}
@keyframes chatUnread{
    0%,100%{box-shadow:0 0 0 2px rgba(63,174,90,.55), 0 0 12px rgba(63,174,90,.4); background:rgba(63,174,90,.14);}
    50%{box-shadow:0 0 0 3px rgba(72,198,103,.9), 0 0 20px rgba(72,198,103,.75); background:rgba(63,174,90,.28);}}

/* The player list gives up its drawer at this width and becomes a block in the flow; the chat
   keeps its drawer instead, because it's opt-in — forcing it open would eat a third of a phone
   screen for something nobody asked to see. It only needs to stop being 20% of a narrow window. */
@media (max-width:900px){
    .chat-dock{--chat-w:min(78vw, 340px); left:0;}
    .chat-panel{max-height:60vh; border-radius:0 var(--radius) var(--radius) 0;}
    body:not(.chat-open) .chat-toggle{transform:translateX(calc(-1 * var(--chat-w) - 6px));}
}

/* ---------- Lobby logs ---------- */
/* The chat drawer mirrored to the right edge: same sizes, same z-index, same closed-by-default
   behaviour, but read-only (no compose row). Every game has one — the play-by-play is lobby state
   (see add_log in lib/store.php), and even a game whose module logs nothing still gets the seat
   changes and the round banners. */
.log-dock{--log-w:20vw;
    position:fixed; right:16px; top:50%; transform:translateY(-50%); z-index:54;
    display:flex; align-items:center; gap:6px; pointer-events:none;}
.log-dock > *{pointer-events:auto;}
.log-panel{width:var(--log-w); max-height:50vh; display:flex; flex-direction:column; gap:8px;
    padding:12px; box-sizing:border-box;
    background:var(--panel); border:1px solid var(--panel-border); border-radius:var(--radius);
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px); box-shadow:var(--shadow);
    transition:transform .22s ease, opacity .22s ease;}
body:not(.log-open) .log-panel{transform:translateX(calc(100% + 24px)); opacity:0; pointer-events:none;}
.log-title{margin:0; text-align:center; font-size:14px; font-weight:800; color:var(--text);}
.log-list{flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain;
    display:flex; flex-direction:column; gap:5px; padding-right:2px;}
.log-empty{margin:auto; text-align:center; font-size:13px; color:var(--muted);}
/* The stripe down the left of a row is the acting player's colour, so the drawer can be skimmed
   for "who has been doing what" without reading a word of it. */
/* flex:0 0 auto on every row is load-bearing, not tidiness. .log-list is a column flexbox, so its
   children default to flex-shrink:1; once the content passes the panel's max-height they get
   squashed instead of scrolling. Text rows are normally saved by the automatic minimum size — but
   that protection is DISABLED on any item with non-visible overflow, which is exactly .log-banner
   (it hides overflow for its ellipsis). Without this the banners silently collapse to nothing on a
   short window while every other line looks fine. */
.log-row{flex:0 0 auto;
    font-size:12.5px; line-height:1.35; color:var(--text); overflow-wrap:anywhere;
    padding-left:8px; border-left:3px solid var(--log-ink, transparent); border-radius:2px;}
.log-who{font-weight:800;}
.log-row.is-bad{color:#ffb3ab;}
/* The clock is scaffolding, not content: tabular figures so the column of times stays in line, and
   grey enough to read past when you're skimming for names. */
.log-time{color:var(--muted); font-weight:600; margin-right:6px;
    font-variant-numeric:tabular-nums; font-size:11px;}
/* Section break around a "===== New Round =====" banner: a real blank line, as asked, so rounds
   are separated by whitespace rather than only by a rule. */
.log-gap{height:7px; flex:0 0 auto;}
.log-banner{flex:0 0 auto; font-size:11.5px; font-weight:800; letter-spacing:.06em; text-align:center;
    color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}

.log-toggle{flex:0 0 auto; appearance:none; cursor:pointer;
    width:22px; height:54px; padding:0; display:flex; align-items:center; justify-content:center;
    font-size:17px; font-weight:800; line-height:1; color:var(--muted);
    background:var(--panel); border:1px solid var(--panel-border); border-radius:10px 0 0 10px;
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px); box-shadow:var(--shadow);
    transition:transform .22s ease, color .14s ease, background .14s ease;}
.log-toggle:hover{color:var(--text); background:rgba(255,255,255,.12);}
.log-toggle::before{content:"\2039";}                    /* ‹ - click to show */
body.log-open .log-toggle::before{content:"\203A";}       /* › - click to hide */
body:not(.log-open) .log-toggle{transform:translateX(calc(16px + var(--log-w) + 6px));}

@media (max-width:900px){
    .log-dock{--log-w:min(78vw, 340px); right:0;}
    .log-panel{max-height:60vh; border-radius:var(--radius) 0 0 var(--radius);}
    body:not(.log-open) .log-toggle{transform:translateX(calc(var(--log-w) + 6px));}
}

/* Game control panel (M4b) */
/* margin-top keeps the dice result + action buttons clear of the bottom seat,
   which overhangs the table box (avatar circle + name + leave button). */
.game-panel{width:min(96vw, 940px, 65vh); display:flex; flex-direction:column; align-items:center; gap:12px; min-height:44px; margin-top:52px; padding-bottom:90px;}
.status-bar{position:fixed; bottom:18px; left:50%; transform:translateX(-50%); z-index:35;
    width:min(94vw,900px); text-align:center;
    background:var(--panel); border:1px solid var(--panel-border); border-radius:16px; padding:14px 22px;
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px); box-shadow:var(--shadow);}
.game-status{color:var(--text); font-size:18px; font-weight:700; text-align:center; min-height:20px; line-height:1.35;}
/* Game module display area (Meier dice / a big card / a hand). Empty between turns. */
.game-info{display:flex; flex-direction:column; align-items:center; gap:8px; width:100%;}
.game-info:empty{display:none;}
.dice-row{display:flex; flex-direction:column; align-items:center; gap:8px;}
.dice-row[hidden]{display:none;}
/* Placeholder shown by not-yet-built games (Drunk Pirate / Uno) once started. */
.game-coming-soon{display:flex; flex-direction:column; align-items:center; gap:8px; text-align:center;
    background:var(--panel); border:1px solid var(--panel-border); border-radius:16px;
    padding:22px 26px; max-width:440px; box-shadow:var(--shadow);}
.game-coming-soon .cs-emoji{font-size:46px; line-height:1;}
.game-coming-soon .cs-title{font-size:22px; font-weight:800; color:var(--text);}
.game-coming-soon .cs-sub{font-size:14px; color:var(--muted); line-height:1.4;}
.dice-faces{display:flex; align-items:center; gap:12px;}
.die{width:52px; height:52px; display:block; filter:drop-shadow(0 4px 6px rgba(0,0,0,.4));}
.dice-call{font-size:38px; color:var(--text); font-weight:800; line-height:1; letter-spacing:.02em;}
.game-actions{display:flex; gap:10px; flex-wrap:wrap; justify-content:center;}
.gbtn{border:none; border-radius:11px; padding:12px 20px; font-size:15px; font-weight:800; cursor:pointer; color:#fff;
    box-shadow:0 8px 18px rgba(0,0,0,.3); transition:transform .12s ease, filter .2s ease;}
.gbtn:hover{transform:translateY(-2px); filter:brightness(1.06);}
.gbtn:active{transform:translateY(0);}
.gbtn:disabled{opacity:.55; cursor:default; transform:none; filter:none;}
.gbtn-green{background:linear-gradient(180deg,#48c667,#3fae5a); color:#0c2413;}
.gbtn-red{background:linear-gradient(180deg,#e05a5a,#d64545);}
.gbtn-bust{background:linear-gradient(180deg,#b8232f,#8f1620); box-shadow:0 0 0 2px rgba(255,90,90,.25) inset;}
.gbtn-amber{background:linear-gradient(180deg,#e2a33c,#cf8b28); color:#2a1c06;}
.gbtn-orange{background:linear-gradient(180deg,#f0973a,#e07d1f); color:#2a1706;}
.gbtn-neutral{background:linear-gradient(180deg,#6b7280,#565d68);}
/* A "game" card's side-lobby invite: deliberately blue, distinct from every sip-resolving
   green/amber/red button, since this one leaves the table instead of drinking. */
.gbtn-blue{background:linear-gradient(180deg,#4a90d9,#3574b8);}

/* Declare UI: Truth / Call Meier on top, the faint-red lie-value grid below. */
.declare-wrap{display:flex; flex-direction:column; align-items:center; gap:14px;}
.declare-top{display:flex; gap:10px; flex-wrap:wrap; justify-content:center;}
.lie-grid{display:flex; gap:10px; justify-content:center; align-items:flex-start;}
.lie-col{display:flex; flex-direction:column; gap:6px;}
.liebtn{appearance:none; min-width:56px; padding:9px 12px; border-radius:9px; cursor:pointer;
    font-size:15px; font-weight:800; letter-spacing:.03em;
    border:1px solid rgba(214,69,69,.42); background:rgba(214,69,69,.14); color:#ffd9d3;
    transition:background .14s ease, border-color .14s ease, transform .12s ease;}
.liebtn:hover{background:rgba(214,69,69,.28); border-color:rgba(214,69,69,.72); transform:translateY(-1px);}
.liebtn:active{transform:translateY(0);}
.liebtn:disabled{opacity:.5; cursor:default; transform:none;}
/* "Don't you lie" (66): lying on it costs a double drink — mark it hotter. */
.liebtn-66{border-color:rgba(214,69,69,.85); background:rgba(214,69,69,.26); color:#fff0ee;}

@media (max-width:520px){
    :root{--seat-name-h:14px;}   /* names shrink to 11px here, so the table shift shrinks too */
    .seat{width:15%; width:clamp(48px,15%,80px);}
    .seat-name{font-size:11px;}
    .seat-leave-text{font-size:9px; padding:0 3px;}
    /* Uno's "Catch" label sits in the same little circle, so it shrinks with it. */
    .seat.occupied.uno-catch-target .seat-inner::after{font-size:9px; padding:0 3px;}
    .seat-host-star{font-size:15px; top:-4px; left:-4px;}
    .seat-bot-star b{font-size:10px;}
    .die{width:44px; height:44px;}
    .dice-call{font-size:28px;}
    .gbtn{padding:11px 16px; font-size:14px;}
    .lobby-tag{font-size:16px; padding:6px 12px;}
    .lobby-tag strong{font-size:19px;}
    .game-status{font-size:18px;}
    .status-bar{bottom:10px; padding:10px 14px;}
    .game-panel{margin-top:30px; padding-bottom:74px;}
    .lie-grid{gap:7px;}
    .lie-col{gap:5px;}
    .liebtn{min-width:46px; padding:8px 8px; font-size:14px;}
}

/* ============================ Drunk Pirate ============================ */
/* Table centre: a decorative deck of card-backs; the direction arrow sits behind it. */
/* Cards are 2:1 (sideways) so the 2:1 Drunk Pirate cover fits them exactly. */
.pirate-deck{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
    width:clamp(118.75px,25%,234.375px); aspect-ratio:2/1; z-index:5;}
.pirate-cardback{background-size:cover; background-position:center;
    border-radius:8px; border:1px solid rgba(0,0,0,.45); box-shadow:0 3px 7px rgba(0,0,0,.45);}
.pirate-deck .pirate-cardback{position:absolute; inset:0;}
.pirate-deck .pirate-cardback:nth-child(2){transform:translate(1px,-2px) rotate(-2deg);}
.pirate-deck .pirate-cardback:nth-child(3){transform:translate(-1px,-4px) rotate(1deg);}
.pirate-deck .pirate-cardback:nth-child(4){transform:translate(2px,-6px) rotate(3deg);}
.pirate-deck .pirate-cardback:nth-child(5){transform:translate(-2px,-8px) rotate(-3deg);}
.pirate-deck .pirate-cardback:nth-child(6){transform:translate(0,-10px) rotate(2deg);}
/* Play-direction indicator, identical to Uno's: two copies of direction.png, one to the LEFT of
   the deck and one to the RIGHT (180° from each other), reading together as a single loop around
   the table. The wrapper carries no visuals of its own — it is sized to exactly match
   .pirate-deck so left:100% / right:100% land flush against the cards. */
.pirate-center{width:clamp(118.75px,25%,234.375px); max-width:none; aspect-ratio:2/1;}
/* --dir-gap holds the base 1vh clear space plus 10% of the arrow's own width, which is what
   pushes the pair further out from the table centre (see the margin note below). */
.pirate-dir{--dir-w:9.1666vh; --dir-gap:calc(1vh + var(--dir-w) * .1);
    position:absolute; top:50%; width:var(--dir-w); max-width:22vw; pointer-events:none;
    opacity:.55; transition:transform .4s ease;}
/* Turned 90°, an arrow's VISUAL width is the image's height — direction.png is 2:1, so half of
   --dir-w — but its layout box stays the full unrotated width. The negative margin cancels that
   difference (--dir-w/4); --dir-gap is then the clear space left between the arc and the deck. */
.pirate-dir-right{left:100%;  margin-left:calc(var(--dir-w) / -4 + var(--dir-gap));}
.pirate-dir-left{right:100%; margin-right:calc(var(--dir-w) / -4 + var(--dir-gap));}
/* The card that flies from the deck to the active player on a draw. */
.pirate-fly-layer{position:absolute; inset:0; pointer-events:none; z-index:6;}
.pirate-fly{position:absolute; width:clamp(76px,16%,150px); aspect-ratio:2/1; transform:translate(-50%,-50%);
    transition:left .55s cubic-bezier(.3,0,.2,1), top .55s cubic-bezier(.3,0,.2,1), transform .55s ease, opacity .55s ease;}

/* A "special" event (Meteor, ...): meteor.png flies in from a top corner to a server-rolled
   destination, explode.png bursts there, then a black 50%-opacity "crater" circle (sized to the
   table's own height) marks the impact and stays until the round moves past it (see
   checkCraterAnchor() in pirate.js). All three live in .pirate-fly-layer, so their left/top %
   match the seats' own coordinate space exactly. Purely decorative — see lib/games/pirate.php
   SPECIAL_TYPES. */
/* transform (translate + the flight-angle rotate) is set inline by playMeteor() in pirate.js,
   not here — it needs the rotate angle computed per-throw, so a static CSS value can't express it. */
.pirate-meteor{position:absolute; width:clamp(96px,22%,180px); aspect-ratio:1/1;
    background-size:contain; background-repeat:no-repeat; background-position:center; z-index:7;
    transition:left 1.8s cubic-bezier(.4,0,.7,1), top 1.8s cubic-bezier(.4,0,.7,1);}
/* Sized to the crater's own diameter; scales 0 -> 150% of that, centered on the impact point,
   over exactly one second. */
.pirate-explosion{position:absolute; transform:translate(-50%,-50%) scale(0);
    height:125%; aspect-ratio:1/1;
    background-size:contain; background-repeat:no-repeat; background-position:center; z-index:9;
    transition:transform 1s ease-out;}
/* 125% of the TABLE's height (the fly layer matches the table box exactly), so the impact area
   covers the same ground for every player at any resolution or zoom — and, being CSS rather than
   a px size measured once in JS, it stays correct if the window is resized mid-effect. */
.pirate-crater{position:absolute; transform:translate(-50%,-50%); border-radius:50%;
    height:125%; aspect-ratio:1/1;
    background:rgba(0,0,0,.5); z-index:8; opacity:1; transition:opacity .6s ease;}
.pirate-crater-fade{opacity:0;}

/* "Flood" (special: "flood"): unlike Meteor this is REAL server-counted state, not a one-shot
   decorative trigger — see renderFlood() in pirate.js and game.flood / floodDrink() in
   lib/games/pirate.php. A fixed strip covering the bottom 75% of the viewport, pointer-events
   auto so it physically blocks the game underneath while it's up.

   z-index 39 puts it over everything that plays the GAME — the status bar and its Done button
   (35), the card panels (36), the sip counter (35) — and under everything that runs the SITE:
   the top bar (40), the player list (45), the chat/log drawers (54), the settings panel (55) and
   every modal (60). It used to be 60, which meant a flood swallowed the settings panel and the
   account modals and left them unclickable until somebody drank it away.
   Nothing is lost by letting the game controls through visually, because none of them work
   anyway: pirate_done, redraw, skip and pass all refuse with 'flood_active' server-side, so this
   overlay was only ever the polite half of the block. Use cards are the exception — they have no
   server guard — and .pirate-panel sits at 36, still covered. */
.pirate-flood{position:fixed; left:0; bottom:0; width:100%; height:75vh; overflow:hidden;
    z-index:39; pointer-events:auto;}
.pirate-flood[hidden]{display:none;}
/* The translating water column: 0% (levelFrac=1, full) sits at translateY(0); fully drunk pushes
   it to translateY(100%), i.e. entirely below the fixed .pirate-flood window. Rising in from
   nothing takes 2s (.pirate-flood-rising, applied only for that first transition); every later
   per-click recede is snappier so a long countdown doesn't feel sluggish. */
.pirate-flood-water{position:absolute; left:0; right:0; top:0; height:100%;
    transition:transform .4s ease-out;}
.pirate-flood-water.pirate-flood-rising{transition-duration:2s;}
/* 5 bands, each a fixed 15vh tall (5 * 15vh = 75vh = the container's own height), so together
   they exactly fill it. A wavy crest line (tiled SVG, redrawn per-band via background-position
   animation for a gentle flowing look) sits over a solid blue-tinted fill. */
.pirate-flood-band{position:absolute; left:0; width:100%; height:15vh;
    background-image:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='28' viewBox='0 0 140 28'%3E%3Cpath d='M0 14 Q35 0 70 14 T140 14 V28 H0 Z' fill='%2379c6ff' fill-opacity='.55'/%3E%3C/svg%3E"),
        linear-gradient(to bottom, rgba(30,110,200,.35), rgba(18,74,160,.6));
    background-repeat:repeat-x, no-repeat;
    background-size:140px 28px, 100% 100%;
    background-position:0 0, 0 0;
    animation:pirate-flood-drift 3.4s linear infinite;
}
.pirate-flood-band:nth-child(even){animation-direction:reverse; filter:brightness(.92);}
@keyframes pirate-flood-drift{from{background-position:0 0, 0 0;} to{background-position:-140px 0, 0 0;}}
/* The counter + "I Drank" button float above the water, unaffected by its translateY. */
.pirate-flood-panel{position:absolute; left:50%; top:22%; transform:translate(-50%,-50%);
    z-index:2; display:flex; flex-direction:column; align-items:center; gap:10px;}
.pirate-flood-count{font-weight:800; font-size:18px; color:#fff; text-shadow:0 2px 6px rgba(0,0,0,.6);}
.pirate-flood-btn{
    padding:12px 26px; border:none; border-radius:999px; font-weight:800; font-size:16px;
    cursor:pointer; color:#0a2a12; background:linear-gradient(180deg,#5fd57f,#3fae5a);
    box-shadow:0 6px 16px rgba(0,0,0,.4); transition:background .15s ease, opacity .15s ease;
}
.pirate-flood-btn:disabled,
.pirate-flood-btn.pirate-flood-btn-cooling{
    background:linear-gradient(180deg,#8a8f93,#6b7075); color:#2a2c2e; cursor:default; opacity:.75;
}

/* "Fire" (special: "fire"): a direct child of .seat itself (NOT .seat-inner, which game-core.js
   rewrites wholesale every render — a child there would be wiped out from under us). Oversized
   and bottom-anchored (not centered) so the flame's own bottom edge lines up with the avatar's
   bottom edge, like it's standing in the fire, rather than floating centered over it.
   fire1..8.png are cycled every 100ms from pirate.js (startFireOn()); which .png is showing lives
   entirely in JS, not CSS. */
.pirate-fire-overlay{position:absolute; left:50%; bottom:0; width:170%; height:170%;
    transform:translateX(-50%); pointer-events:none; z-index:6;
    background-size:contain; background-repeat:no-repeat; background-position:bottom center;}

/* "Hurricane" (special: "hurricane"): roams the WHOLE viewport (position:fixed, left/top in vw/vh,
   not the table's own %), unlike every other special here. Position + rotation are both driven
   frame-by-frame from JS (playHurricane() in pirate.js), not a CSS transition — the path is a
   spline through 8 points, not a straight line, so there's no single before/after state to
   transition between. Trail circles are dropped on the ground once a second and rendered above
   everything (z-index above the hurricane image itself, per spec: "on top of other things"). */
/* Both live INSIDE .pirate-fly-layer, so left/top % and the sizes below are all relative to the
   table's own box — identical for every player regardless of resolution, zoom or window shape
   (a viewport-relative path would cross completely different seats on different screens).
   height is 69.3% of the table's height = 39% of its width = 3 avatars across (an avatar is 13%
   of the table's width), with aspect-ratio keeping it square. Sized here rather than measured in
   JS because a seat's own width is clamp()ed, so measuring one stops tracking the table at very
   small or very large sizes. */
.pirate-hurricane{position:absolute; transform:translate(-50%,-50%); pointer-events:none; z-index:10;
    height:69.3%; aspect-ratio:1/1;
    background-size:contain; background-repeat:no-repeat; background-position:center;}
.pirate-hurricane-trail{position:absolute; transform:translate(-50%,-50%); border-radius:50%;
    height:69.3%; aspect-ratio:1/1;
    background:rgba(0,0,0,.25); pointer-events:none; z-index:11; opacity:1; transition:opacity .6s ease;}
.pirate-hurricane-trail-fade{opacity:0;}

/* "Seafight" (special: "seafight"): two cannons slide into the bottom corners of the VIEWPORT
   (position:fixed — they're framing, not something standing on the table) and fire 9 shells each.
   The shells, explosions and the black holes they leave live INSIDE .pirate-fly-layer instead, so
   their left/top % are the table's own units and every player sees them land over the same seats
   — see playSeafight() in pirate.js. Purely decorative / honor-based, like Meteor and Hurricane. */
.pirate-cannon{position:fixed; bottom:0; z-index:12; pointer-events:none;
    width:clamp(90px,15vw,220px); aspect-ratio:1/1;
    transition:transform .3s ease;}
/* Off-screen until playSeafight() adds .is-in. That first move runs at the server's slide-in
   duration (set inline, since the server owns the number); it's cleared once they've arrived so
   the .3s recoil timing above takes over — 0.3s back on the shot, 0.3s returning. */
.pirate-cannon-left{left:0; transform:translateX(-130%);}
.pirate-cannon-left.is-in{transform:translateX(0);}
.pirate-cannon-left.is-in.is-recoil{transform:translateX(-22%);}
.pirate-cannon-right{right:0; transform:translateX(130%);}
.pirate-cannon-right.is-in{transform:translateX(0);}
.pirate-cannon-right.is-in.is-recoil{transform:translateX(22%);}
/* The flip lives on the inner image, not the outer element: the outer one's transform is already
   spoken for by the slide-in and the recoil, and scaleX there would invert those too. */
.pirate-cannon-img{width:100%; height:100%;
    background-size:contain; background-repeat:no-repeat; background-position:bottom center;}
.pirate-cannon-right .pirate-cannon-img{transform:scaleX(-1);}
/* Every size below is a % of the TABLE's own HEIGHT with aspect-ratio squaring it off — the same
   idiom .pirate-hurricane / .pirate-crater use, so they cover identical ground for every player at
   any resolution or zoom and stay right if the window is resized mid-effect. The table is 16:9, and
   an avatar is 13% of its width, i.e. 23.1% of its height — so 120% of an avatar is 27.7% and 110%
   is 25.4%. */
/* A shell in flight. Position AND rotation are driven frame-by-frame from sfFire() in pirate.js,
   not a CSS transition: the flight time is per-shot (constant speed over a varying distance), and
   the spin has to share the same transform as the centering translate. */
.pirate-sf-ball{position:absolute; transform:translate(-50%,-50%); z-index:9; pointer-events:none;
    height:8%; aspect-ratio:1/1;
    background-size:contain; background-repeat:no-repeat; background-position:center;}
.pirate-sf-boom{position:absolute; transform:translate(-50%,-50%) scale(0); z-index:10;
    height:27.7%; aspect-ratio:1/1; pointer-events:none;
    background-size:contain; background-repeat:no-repeat; background-position:center;
    transition:transform .45s ease-out;}
.pirate-sf-boom.is-out{transform:translate(-50%,-50%) scale(1);}
.pirate-sf-hole{position:absolute; transform:translate(-50%,-50%); border-radius:50%; z-index:8;
    height:25.4%; aspect-ratio:1/1; pointer-events:none;
    /* Same wash as .pirate-hurricane-trail, so overlapping hits read the same way there as here. */
    background:rgba(0,0,0,.25); opacity:1; transition:opacity .6s ease;}
.pirate-sf-hole-fade{opacity:0;}

/* "Champion" (special: "champion"): the rock-paper-scissors knockout bracket. Rounds are columns
   left-to-right; the connector lines are an SVG overlay measured after layout in JS.
   Two separate elements do the two separate jobs:
     .pirate-champ    — a full-VIEWPORT backdrop, fixed regardless of where the table sits, whose
                         only purpose is to swallow every click while the bracket is live (see
                         .is-open, toggled in pirate.js for everyone including the host — the
                         host's only sanctioned action during a championship is the bracket itself).
     .pirate-champ-stage — sized/positioned in JS (positionChampionStage()) to exactly match the
                         table's own on-screen box, so the panel it holds visually reads as
                         "belonging to the table" even though its parent covers the whole screen.
   Clicks land on the panel only for the host (panel.style.pointerEvents, set in JS) — everyone
   else's clicks fall through to the backdrop underneath and are swallowed there instead. */
.pirate-champ{position:fixed; inset:0; z-index:60; pointer-events:none;}
.pirate-champ[hidden]{display:none;}
.pirate-champ.is-open{pointer-events:auto; background:rgba(0,0,0,.45);}
.pirate-champ-stage{position:fixed; z-index:60; pointer-events:none;
    display:flex; align-items:center; justify-content:center; padding:16px;}
.pirate-champ-panel{max-height:96%; max-width:min(96%,720px); overflow:auto;
    display:flex; flex-direction:column; gap:10px; padding:14px 16px;
    background:rgba(18,22,28,.94); border:1px solid rgba(255,255,255,.14); border-radius:14px;
    box-shadow:0 12px 32px rgba(0,0,0,.5);}
.pirate-champ-title{font-size:16px; font-weight:800; color:var(--text); text-align:center;}
.pirate-champ-board{position:relative; display:flex; align-items:center; gap:28px;}
.pirate-champ-col{display:flex; flex-direction:column; justify-content:space-around; gap:14px;}
.pirate-champ-match{display:flex; flex-direction:column; gap:3px; position:relative; z-index:1;}
.pirate-champ-slot{display:block; min-width:104px; padding:5px 9px; text-align:left;
    font-size:13px; font-weight:700; color:var(--text);
    background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.16); border-radius:7px;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.pirate-champ-slot:disabled{cursor:default;}
.pirate-champ-slot.is-pick{cursor:pointer;}
.pirate-champ-slot.is-pick:hover{background:rgba(255,255,255,.16); border-color:rgba(255,255,255,.4);}
.pirate-champ-slot.is-won{background:rgba(76,175,80,.24); border-color:rgba(76,175,80,.6);}
.pirate-champ-slot.is-lost{opacity:.42; text-decoration:line-through;}
.pirate-champ-slot.is-bye{background:rgba(255,193,7,.16); border-color:rgba(255,193,7,.45);}
.pirate-champ-byenote{font-size:11px; color:var(--muted); text-align:center;}
.pirate-champ-foot{font-size:12px; color:var(--muted); text-align:center;}
.pirate-champ-foot.is-winner{font-size:15px; font-weight:800; color:#ffd76a;}
/* Sits behind the boxes (they carry z-index:1) and stretches over the whole board. */
.pirate-champ-lines{position:absolute; inset:0; width:100%; height:100%; pointer-events:none; z-index:0;}
.pirate-champ-lines path{fill:none; stroke:rgba(255,255,255,.28); stroke-width:2;}

@media (max-width:640px){
    .pirate-champ{padding:8px;}
    .pirate-champ-panel{padding:10px;}
    .pirate-champ-board{gap:18px;}
    .pirate-champ-slot{min-width:78px; font-size:12px;}
}

/* The big drawn card in the info area (laid sideways = landscape, text stays readable). */
.pirate-drawn{display:flex; flex-direction:column; align-items:center; gap:10px; width:100%;}
.pd-owner{font-size:18px; font-weight:800; color:var(--text);}
/* Flanks the card with a like (left) / dislike (right) icon column — see .pd-vote-btn below. */
.pd-card-row{display:flex; align-items:center; justify-content:center; gap:16px;}
.pd-card{display:flex; align-items:center; justify-content:center; text-align:center;
    width:min(70vw,360px); height:min(42vw,224px); padding:18px;
    background:linear-gradient(145deg,#f6eede,#e7d9ba); color:#2a1c10;
    border:2px solid #b89b6a; border-radius:16px; box-shadow:0 10px 24px rgba(0,0,0,.42);}
/* Vote button + its like/dislike count, stacked. */
.pd-vote-col{display:flex; flex-direction:column; align-items:center; gap:4px; flex:0 0 auto;}
.pd-vote-count{font-size:13px; font-weight:800; color:var(--text); opacity:.85; min-width:1.2em; text-align:center;}
/* Like/dislike icon buttons either side of the drawn card. Same source image (assets/images/
   pirate/like.png outline, isLiked.png filled once voted) for both: CSS mask + background-color
   recolors it exactly regardless of the PNG's own color, so "tinted green"/"tinted red" don't
   depend on what the art itself looks like. The mask-image URL itself is set inline by JS
   (swaps like.png <-> isLiked.png depending on the viewer's own vote). Dislike is the same icon
   rotated 180° (= flipped on both axes) and tinted red instead of green.
   Guests (no account) get the button disabled — read-only counts, no voting. */
.pd-vote-btn{
    width:40px; height:40px; border:none; padding:0; cursor:pointer; background:none;
    -webkit-mask-size:contain; mask-size:contain;
    -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
    -webkit-mask-position:center; mask-position:center;
    filter:drop-shadow(0 3px 6px rgba(0,0,0,.4));
    transition:transform .12s ease, filter .15s ease;
}
.pd-vote-btn:not(:disabled):hover{filter:drop-shadow(0 3px 6px rgba(0,0,0,.4)) brightness(1.2);}
.pd-vote-btn:disabled{cursor:default; opacity:.7;}
.pd-like{background-color:#3fae5a;}
.pd-like:not(:disabled):hover{transform:scale(1.12);}
.pd-like:not(:disabled):active{transform:scale(.95);}
.pd-dislike{background-color:#d64545; transform:rotate(180deg);}
.pd-dislike:not(:disabled):hover{transform:rotate(180deg) scale(1.12);}
.pd-dislike:not(:disabled):active{transform:rotate(180deg) scale(.95);}
@media (max-width:640px){
    .pd-vote-btn{width:30px; height:30px;}
    .pd-card-row{gap:10px;}
}
.pd-text{font-weight:700; line-height:1.25; word-break:break-word; max-width:100%;}
/* A leading "tag" line (e.g. "Play anytime") highlighted above the rest of a card's text. */
.pd-tag{display:block; color:#b3401a; font-weight:800; text-transform:uppercase; letter-spacing:.04em;
    margin-bottom:5px;}
/* A hidden card shown face-down (onlookers, or before "Reveal" in solo). */
.pd-covered{background-size:cover; background-position:center; padding:0; border-color:#7a5233;}
.pd-text.pd-lg{font-size:clamp(18px,3.4vw,27px);}
.pd-text.pd-md{font-size:clamp(15px,2.6vw,22px);}
.pd-text.pd-sm{font-size:clamp(13px,2vw,18px);}
.pd-enter{animation:pdIn .42s ease .35s both;}
@keyframes pdIn{from{opacity:0; transform:scale(.7);} to{opacity:1; transform:scale(1);}}

.pd-controls{display:flex; flex-direction:column; align-items:center; gap:10px; width:100%;}
.pirate-rule{position:relative; width:min(90vw,360px);}
.pirate-rule textarea{width:100%; min-height:54px; resize:none; box-sizing:border-box;
    border-radius:10px; border:1px solid var(--panel-border); background:var(--panel); color:var(--text);
    padding:10px 12px 22px; font-size:15px; font-family:inherit;}
.pirate-rule-count{position:absolute; right:10px; bottom:8px; font-size:11px; color:var(--muted); pointer-events:none;}
.pirate-flip{display:flex; gap:10px; flex-wrap:wrap; justify-content:center;}
.gbtn.is-picked{outline:3px solid rgba(255,255,255,.85); outline-offset:1px;}
.pd-done{min-width:120px;}
/* While filling a rule card: other seated players glow and become clickable to insert their
   name (the card OWNER's seat, .pirate-owner-seat, is excluded — not just "my own seat",
   since in solo mode the owner may be a puppet rather than the host). */
/* Ring of Fire reuses this for its 8 (drink buddy) and Jack (rule) — only the owner-seat class
   differs, since each game marks the excluded seat with its own prefix. */
.seats-pick-name .seat.occupied:not(.pirate-owner-seat):not(.ring-owner-seat){cursor:pointer;}
/* .ring-buddy-picked is excluded so the red "this one is selected" glow below isn't outranked by
   this gold "selectable" one — the two would otherwise both match the chosen seat. */
.seats-pick-name .seat.occupied:not(.pirate-owner-seat):not(.ring-owner-seat):not(.ring-buddy-picked) .seat-inner{
    border-color:#ffd54a; animation:piratePick 1.15s ease-in-out infinite;}
@keyframes piratePick{
    0%,100%{box-shadow:0 0 0 3px rgba(255,213,74,.85), 0 0 14px rgba(255,193,7,.45);}
    50%{box-shadow:0 0 0 4px rgba(255,213,74,1), 0 0 22px rgba(255,193,7,.75);}}

/* Lobby-wide card trays pinned to the bottom corners; hover a mini card to expand it. */
.pirate-panel{position:fixed; bottom:92px; z-index:36; width:min(46vw,316px);
    background:var(--panel); border:1px solid var(--panel-border); border-radius:14px; padding:12px;
    box-shadow:var(--shadow); -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);}
.pirate-panel[hidden]{display:none;}
.pirate-panel-active{left:7vw;}
.pirate-panel-perm{right:7vw;}
.pp-title{font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); font-weight:800;
    margin-bottom:8px; text-align:center;}
.pp-cards{display:flex; flex-wrap:wrap; gap:6px; justify-content:center;}
.pirate-mini{position:relative; width:68px; height:102px; border-radius:8px;
    background-size:cover; background-position:center; border:1px solid rgba(0,0,0,.45);
    box-shadow:0 2px 4px rgba(0,0,0,.4); cursor:help; transition:transform .12s ease;}
/* The hovered mini itself grows slightly and rises above its neighbours. */
.pirate-mini:hover{transform:scale(1.1); z-index:40;}
/* Raise the whole tray above the other trays while it's hovered, so a pop-up extending past
   the tray edge can't be covered by a neighbouring tray. One step is enough (the others sit at
   36) and keeps a hovered tray below the player list at 45, which must stay on top of them. */
.pirate-panel:hover{z-index:37;}
/* Expiry warning wash on a permanent card running low on rounds (colour/opacity set inline). */
.pirate-mini-tint{position:absolute; inset:0; z-index:1; border-radius:8px; pointer-events:none;}
/* Solo mode: owner name banner across the top of a use-card mini. */
.pirate-mini-owner{position:absolute; top:0; left:0; right:0; z-index:2;
    background:rgba(0,0,0,.62); color:#ffd54a; font-size:10px; font-weight:800;
    text-align:center; padding:2px 3px; border-radius:8px 8px 0 0;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap; pointer-events:none;}
/* Enlarged (scale 1.5) so hovered lasting/use/permanent cards read bigger. Anchored
   bottom-center, so it grows upward+outward from just above the mini it belongs to. */
.mini-pop{position:absolute; bottom:calc(100% + 16px); left:50%; transform:translateX(-50%) scale(1.44); transform-origin:bottom center;
    width:230px; background:rgba(20,14,10,0.95); border:1px solid var(--panel-border); border-radius:12px; padding:12px;
    box-shadow:var(--shadow); opacity:0; visibility:hidden; transition:opacity .12s ease, transform .12s ease;
    z-index:9999; pointer-events:none;}
.pirate-mini:hover .mini-pop{opacity:1; visibility:visible; transform:translateX(-50%) scale(1.5);}
/* JS adds .pop-below to a mini in the top third of the screen: open the pop-up below the card
   (anchored at its top edge, growing downward) so it never runs off the top of the viewport. */
.pirate-mini.pop-below .mini-pop{top:calc(100% + 16px); bottom:auto; transform-origin:top center;}
.mini-pop-card{background:linear-gradient(145deg,#f6eede,#e7d9ba); color:#2a1c10; border:1px solid #b89b6a;
    border-radius:8px; padding:10px; min-height:70px; display:flex; align-items:center; justify-content:center;
    text-align:center; margin-bottom:8px;}
.mini-pop-text{font-weight:700; line-height:1.2;}
.mini-pop-text.pd-lg{font-size:14px;} .mini-pop-text.pd-md{font-size:13px;} .mini-pop-text.pd-sm{font-size:12px;}
.mini-pop-owner{font-size:12px; color:var(--text); font-weight:700;}
.mini-pop-rounds{font-size:12px; color:var(--muted); margin-top:2px;}
.mini-pop-rule{font-size:12px; color:var(--text); margin-top:6px; font-style:italic;}
/* Use Cards tray: right centre, above the Permanent tray. Your own use cards glow + click. */
/* --use-lift (set from JS) pushes this tray up by one card height per extra Permanent-tray
   row, so the upward-growing Permanent tray never reaches it. */
aside.pirate-panel-use{right:7vw; top:50%; bottom:auto;
    transform:translateY(calc(-50% - var(--use-lift, 0px))); transition:transform .2s ease;}
/* "Cards that ran out": the mirror of the Use tray on the left, lifted the same way over the
   Active tray. Only holds cards that expired on the CURRENT turn, so it empties itself again
   as soon as play moves on. */
aside.pirate-panel-ranout{left:7vw; top:50%; bottom:auto;
    transform:translateY(calc(-50% - var(--ran-lift, 0px))); transition:transform .2s ease;}
/* Spent cards read as "gone" — drained of colour, restored on hover so the pop-up is legible. */
.pirate-panel-ranout .pirate-mini{filter:grayscale(.5); opacity:.85;}
.pirate-panel-ranout .pirate-mini:hover{filter:none; opacity:1;}
.pirate-mini.is-mine{cursor:pointer; animation:piratePick 1.15s ease-in-out infinite;}
/* "Delete" ability: while a player is resolving/playing a card with `delete`, every card of the
   matching kind (Use or Permanent tray) glows red in THEIR client only (see pirate.js) instead of
   the usual gold "pickable" pulse, and hovering warns with a big red X that a click removes it
   for everyone right now. */
.pirate-mini.pirate-mini-deletable{cursor:pointer; animation:pirateDeletePulse 1.15s ease-in-out infinite;}
@keyframes pirateDeletePulse{
    0%,100%{box-shadow:0 0 0 3px rgba(224,54,54,.85), 0 0 14px rgba(224,54,54,.5);}
    50%{box-shadow:0 0 0 4px rgba(224,54,54,1), 0 0 22px rgba(224,54,54,.8);}
}
.pirate-mini-delete-x{position:absolute; inset:0; z-index:5; display:flex; align-items:center; justify-content:center;
    font-size:34px; font-weight:900; line-height:1; color:#ff4a3f;
    text-shadow:0 0 5px rgba(0,0,0,.85), 0 0 12px rgba(255,74,63,.9);
    opacity:0; pointer-events:none; transition:opacity .1s ease;}
.pirate-mini-deletable:hover .pirate-mini-delete-x{opacity:1;}
/* "specialuse: steal" ability: while it's live, every use card belonging to SOMEBODY ELSE that
   was already in the tray glows green in the stealer's client only (see pirate.js). Green rather
   than the delete red because the card survives — only its owner changes. Cards that entered the
   tray after the steal card was played stay unlit; the server rejects them too. */
.pirate-mini.pirate-mini-stealable{cursor:pointer; animation:pirateStealPulse 1.15s ease-in-out infinite;}
@keyframes pirateStealPulse{
    0%,100%{box-shadow:0 0 0 3px rgba(63,174,90,.85), 0 0 14px rgba(63,174,90,.5);}
    50%{box-shadow:0 0 0 4px rgba(63,174,90,1), 0 0 22px rgba(63,174,90,.8);}
}
.pirate-mini-steal-grab{position:absolute; inset:0; z-index:5; display:flex; align-items:center; justify-content:center;
    font-size:30px; line-height:1;
    text-shadow:0 0 5px rgba(0,0,0,.85), 0 0 12px rgba(63,220,110,.9);
    opacity:0; pointer-events:none; transition:opacity .1s ease;}
.pirate-mini-stealable:hover .pirate-mini-steal-grab{opacity:1;}
/* Bottom-left running stats counter (rounds played / cards drawn this game). */
.pirate-stats{position:fixed; left:16px; bottom:16px; z-index:35; pointer-events:none;
    font-size:12px; font-weight:700; color:var(--muted); line-height:1.55;
    background:rgba(0,0,0,.28); border:1px solid var(--panel-border); border-radius:10px; padding:7px 11px;
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);}
.pirate-stats[hidden]{display:none;}
/* Bottom-right toggle: show card texts instead of the back side (no behaviour yet). */
.pirate-show-texts{position:fixed; right:16px; bottom:16px; z-index:35; cursor:pointer;
    display:flex; align-items:center; gap:8px; max-width:min(46vw,316px);
    font-size:12px; font-weight:700; color:var(--muted); line-height:1.35;
    background:rgba(0,0,0,.28); border:1px solid var(--panel-border); border-radius:10px; padding:7px 11px;
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);}
.pirate-show-texts .pst-check{width:15px; height:15px; flex:0 0 auto; cursor:pointer; accent-color:var(--accent);}
/* "Show card texts" mode: each tray mini shows its text instead of the card back. All sizes
   are viewport-relative (vw) with no px caps, and the cards are flexed to exactly 3 per row,
   so the card area keeps the same share of the screen at every resolution. Toggled by the
   bottom-right checkbox flipping .pirate-show-texts-on onto <body>. */
.pirate-mini-face, .pirate-face-owner, .pirate-face-rule{display:none;}
.pirate-show-texts-on .pirate-panel{width:20.77vw;}
.pirate-show-texts-on .pp-cards{gap:0.281vw;}
.pirate-show-texts-on .pirate-mini{
    flex:0 0 calc((100% - 1vw) / 3);               /* 3 per row; the extra slack (vs. the 0.562vw of gaps) keeps sub-pixel rounding from ever wrapping the 3rd card */
    width:auto; height:auto; aspect-ratio:180 / 112;   /* same shape as the main .pd-card */
    /* Same fill as .pd-card; !important on the shorthand overrides the inline card-back
       image that JS sets on the mini. Laid out as three vertical zones: owner / text / rule. */
    background:linear-gradient(145deg,#f6eede,#e7d9ba) !important; color:#000;
    border:1px solid #b89b6a; border-radius:0.281vw;
    display:flex; flex-direction:column; padding:0; overflow:visible; min-width:0;}
.pirate-show-texts-on .pirate-mini-owner{display:none;}   /* solo banner replaced by the owner bar */
/* Middle 80%: the card text. */
.pirate-show-texts-on .pirate-mini-face{flex:1 1 auto; min-height:0; min-width:0; display:flex;
    align-items:center; justify-content:center; text-align:center; padding:0 0.28vw;
    font-weight:700; line-height:1.15; overflow:hidden; word-break:break-word;}
.pirate-show-texts-on .pirate-mini-face.pd-lg{font-size:0.603vw;}
.pirate-show-texts-on .pirate-mini-face.pd-md{font-size:0.523vw;}
.pirate-show-texts-on .pirate-mini-face.pd-sm{font-size:0.456vw;}
/* Top 10% owner bar + bottom 10% rule bar: black background, white single-line text. */
.pirate-show-texts-on .pirate-face-owner,
.pirate-show-texts-on .pirate-face-rule{
    flex:0 0 15%; min-width:0; min-height:0; display:flex; align-items:center; justify-content:center;
    overflow:hidden; background:#000; color:#fff; font-weight:700; font-size:0.38vw; line-height:1; padding:0 0.28vw;}
.pirate-show-texts-on .pirate-face-owner{border-radius:0.281vw 0.281vw 0 0;}
.pirate-show-texts-on .pirate-face-rule{border-radius:0 0 0.281vw 0.281vw;}
.pirate-show-texts-on .pirate-face-owner span,
.pirate-show-texts-on .pirate-face-rule span{
    min-width:0; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
/* A played use card: full-screen blocking overlay, above everything, until the owner dismisses. */
.pirate-use-modal{position:fixed; inset:0; z-index:200; display:flex; flex-direction:column;
    align-items:center; justify-content:center; gap:20px; padding:22px;
    background:rgba(0,0,0,.74); -webkit-backdrop-filter:blur(3px); backdrop-filter:blur(3px);}
.pirate-use-modal[hidden]{display:none;}
.use-modal-turn{color:#ffd54a; font-weight:800; text-align:center; letter-spacing:.03em; text-transform:uppercase;
    text-shadow:0 2px 8px rgba(0,0,0,.6); font-size:clamp(14px,2.2vw,19px);}
.use-modal-owner{color:#fff; font-weight:800; text-align:center; text-shadow:0 2px 8px rgba(0,0,0,.6);
    font-size:clamp(18px,3vw,26px);}
.pirate-use-modal .use-big{width:min(84vw,560px); height:min(48vw,330px);}
.use-modal-actions{display:flex; gap:10px;}
.use-modal-actions .gbtn{font-size:17px; padding:14px 26px;}
@media (max-width:640px){
    .pirate-panel{bottom:80px; width:34vw; padding:7px;}
    .pd-card{width:82vw; height:52vw;}
    .pirate-use-modal .use-big{width:88vw; height:56vw;}
}

/* "specialuse": select — the 12-card menu shown to the player who played the card, flanking the
   use-card overlay it belongs to. z-index sits ABOVE .pirate-use-modal (200) so the columns stay
   clickable over its backdrop, while the layer itself passes clicks through everywhere else.
   Two columns of 6 anchored at 30% / 70% of the viewport width, each vertically centred, so they
   frame the played card in the middle rather than covering it. */
/* --sel-card-w drives both the tile width AND how far each column is pushed outward (half a card
   each way), so the two can never drift apart when the size is retuned. */
.pirate-select{position:fixed; inset:0; z-index:210; pointer-events:none;
    --sel-card-w:clamp(156px,19.5vw,252px);
    /* Six of these stack per column, so the height is additionally capped against the viewport
       (6 * 14vh = 84vh) — without that, the taller tile would run the columns off the top and
       bottom of a short window. */
    --sel-card-h:min(clamp(73px,9.1vw,118px), 14vh);}
.pirate-select[hidden]{display:none;}
.pirate-select-hint{position:absolute; left:50%; top:10px; transform:translateX(-50%);
    pointer-events:none; color:#ffd54a; font-weight:800; text-align:center; letter-spacing:.03em;
    text-shadow:0 2px 8px rgba(0,0,0,.85); font-size:clamp(13px,2vw,18px);}
.pirate-select-col{position:absolute; top:50%;
    display:flex; flex-direction:column; gap:6px; pointer-events:auto;}
/* Anchored at 30%/70%, then shifted a further half-tile outward so the columns clear the played
   card in the middle. The -50% is the usual centring; the extra term is the outward push. */
.pirate-select-col.is-left{left:30%; transform:translate(calc(-50% - var(--sel-card-w) / 2), -50%);}
.pirate-select-col.is-right{left:70%; transform:translate(calc(-50% + var(--sel-card-w) / 2), -50%);}
/* Plain white rather than the card-back artwork — twelve small tiles are being read for their
   text, and the illustrated face fights it. Font size is set per tile by fitSelectText() in
   pirate.js, which shrinks it until the text actually fits. */
.pirate-select-card{position:relative; width:var(--sel-card-w); height:var(--sel-card-h);
    /* Same parchment gradient, text colour and border as the drawn card in the middle of the
       table (.pd-card) — these are cards too, just small, so they should read as the same stock. */
    background:linear-gradient(145deg,#f6eede,#e7d9ba); color:#2a1c10;
    border:1px solid #b89b6a; border-radius:7px; cursor:pointer; overflow:hidden;
    box-shadow:0 3px 10px rgba(0,0,0,.55); transition:transform .12s ease, box-shadow .12s ease;}
.pirate-select-card:hover{transform:scale(1.06); box-shadow:0 5px 16px rgba(0,0,0,.7);}
.pirate-select-card.is-chosen{box-shadow:0 0 0 3px #ffd54a, 0 0 18px rgba(255,213,74,.85);}
.pirate-select-text{position:absolute; inset:6% 7%; display:flex; align-items:center; justify-content:center;
    text-align:center; font-weight:700; line-height:1.15; overflow:hidden;}
/* Measured against its parent by fitSelectText(); must wrap freely for that to mean anything. */
.pirate-select-inner{display:block; max-width:100%; overflow-wrap:break-word;}
/* The "Play anytime" header on a use card. Scaled relative to the tile's own font size (which
   fitSelectText() shrinks per card) so it stays proportional instead of dwarfing shrunken text,
   and tightened up — at this size the full-size margin would eat most of the tile. */
.pirate-select-inner .pd-tag{font-size:.85em; margin-bottom:2px;}
@media (max-width:640px){
    /* Too narrow to push the columns outward as well — pull them back toward the edges instead. */
    .pirate-select{--sel-card-w:40vw; --sel-card-h:min(23.8vw, 14vh);}
    .pirate-select-col.is-left{left:0; transform:translate(2vw,-50%);}
    .pirate-select-col.is-right{left:100%; transform:translate(calc(-100% - 2vw),-50%);}
}

/* ---------- Lobby tools: seat-count stepper + Solo Mode + the game module's own controls ----------
   A host-only dropdown under the "Game settings" button, mirroring .settings-panel on the other
   side of the top bar: same solid backdrop and z-index. It opens straight over the player list
   (45), which it must always cover. */
/* `left`/`top` are set from JS, which puts the panel beside its button (tops aligned) and clear
   of the player list (whose edges move with the language / the list's collapsed state). */
.lobby-tools{
    position:fixed; left:18px; top:60px; z-index:55;
    display:flex; flex-direction:column; gap:9px;
    padding:16px; border-radius:var(--radius); min-width:200px;
    background:linear-gradient(180deg,#241a13,#1b130d); border:1px solid var(--panel-border);
    box-shadow:var(--shadow);
    color:var(--text); font-size:14px;
}
.lobby-tools[hidden]{display:none;}
.lt-seats{display:flex; align-items:center; gap:8px;}
.lt-seats[hidden]{display:none;}
.lt-seats-label{font-weight:700;}
.lt-seat-count{min-width:1.5em; text-align:center; font-weight:800; font-variant-numeric:tabular-nums;}
.lt-seat-btn{
    appearance:none; width:28px; height:28px; border-radius:8px; cursor:pointer;
    border:1px solid var(--panel-border); background:rgba(255,255,255,.06); color:var(--text);
    font-size:18px; line-height:1; font-weight:700;
    display:inline-flex; align-items:center; justify-content:center;
    transition:background .15s ease, border-color .15s ease, transform .1s ease;
}
.lt-seat-btn:hover:not(:disabled){background:rgba(63,174,90,.18); border-color:var(--accent); transform:translateY(-1px);}
.lt-seat-btn:disabled{opacity:.35; cursor:default;}
.lt-solo{display:flex; align-items:center; gap:8px; cursor:pointer; user-select:none; font-weight:600;}
.lt-solo[hidden]{display:none;}
.lt-solo-cb{width:16px; height:16px; accent-color:var(--accent); cursor:pointer;}
/* Golden badge shown in place of the toggle once the lobby is in solo mode. */
.lt-solo-text{font-weight:800; letter-spacing:.02em; text-align:center;
    color:#ffd54a; text-shadow:0 1px 4px rgba(0,0,0,.55);}
.lt-solo-text[hidden]{display:none;}

/* Extension slot for a game module's own lobby-tools controls (e.g. Pirate's "Enable Packs"). */
.lt-extra{display:flex; flex-direction:column; gap:7px; margin-top:12px; padding-top:12px;
    border-top:1px solid var(--panel-border);}
.lt-extra[hidden]{display:none;}

/* The host-only "Catastrophes" popup (Drunk Pirate): reuses .lobby-tools for the same look,
   just with its own title + one row per special event ("Meteor" for now, more later). */
.lt-cat-title{font-weight:800; font-size:15px;}
.pirate-cat-row{width:100%;}
/* Blank spacer separating the catastrophe buttons from the "queue a card by id" tool below —
   that one isn't a catastrophe, so it shouldn't read as another button in the same list. */
.pirate-cat-gap{height:10px;}
.pirate-cat-input{width:100%; box-sizing:border-box; padding:9px 11px; margin-bottom:7px;
    border:1px solid var(--panel-border); border-radius:9px;
    background:rgba(0,0,0,.35); color:var(--text); font-size:14px; font-family:inherit;}
.pirate-cat-input::placeholder{color:var(--muted);}
.pirate-cat-input:focus{outline:none; border-color:#ffd54a;}

/* Non-hosts can open the settings panel to see it, but not touch it: game-core hard-disables every
   control except the plain "Close" button (CSS alone can't stop a <label> click from toggling its
   checkbox), and this greys the whole panel out to match. Server-side host checks are the real
   guard (see api.php); this is purely so a non-host isn't left clicking dead controls. */
.lobby-tools.lt-readonly button:not(.lt-close),
.lobby-tools.lt-readonly select{
    pointer-events:none; opacity:.45; filter:grayscale(.4);
}
/* The row, not just the box: a click on a <label> is forwarded to its checkbox, so the label has
   to stop pointer events too or the tick would still flip. */
.lobby-tools.lt-readonly label,
.lobby-tools.lt-readonly input{pointer-events:none;}
/* The whole row reads as inert, not just the box. Text is dimmed with `color` rather than opacity
   so it doesn't compound with the .45 above on the controls nested inside these rows. */
.lobby-tools.lt-readonly label{cursor:default;}
.lobby-tools.lt-readonly .lt-seats,
.lobby-tools.lt-readonly .lt-solo,
.lobby-tools.lt-readonly .lt-solo-text,
.lobby-tools.lt-readonly .lt-weight-row,
.lobby-tools.lt-readonly .lt-pack-row{color:var(--muted);}

/* Unticked boxes fade back with the rest of the panel; ticked ones keep their full accent colour
   so a non-host can still read which settings are on (.lt-on is set in game-core). */
.lobby-tools.lt-readonly input[type="checkbox"]{opacity:.45;}
.lobby-tools.lt-readonly .lt-on input[type="checkbox"]{opacity:1;}
.lt-title{font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); font-weight:800;}
.lt-pack-row{display:flex; align-items:center; gap:8px; cursor:pointer; user-select:none; font-weight:600;}
/* display:flex above beats the browser's own [hidden]{display:none}, so a row that is hidden from
   JS needs saying again — the core "Play with sips" row is hidden for the games without it. */
.lt-pack-row[hidden]{display:none;}
/* A setting the HOST is allowed to change, but not right now (Hanabi's gameplay settings are
   locked while a game is running). Deliberately the same treatment .lt-readonly gives a non-host
   — greyed, unclickable, still readable — so "not now" and "not you" look alike. */
.lt-locked{color:var(--muted); pointer-events:none; cursor:default;}
.lt-locked input[type="checkbox"], .lt-locked input[type="radio"], .lt-locked input[type="range"]{opacity:.45;}
/* A section heading whose whole section is locked, greyed to match the rows under it. */
.lt-title.is-off{color:var(--muted); opacity:.7;}
/* A note under a setting's label, explaining what it costs or does. Block, so it drops onto its
   own line under the label rather than pushing the checkbox row wider. */
.lt-hint{display:block; font-weight:600; font-size:11px; color:var(--muted); margin-top:1px;}

/* A numeric setting on a slider: label, track, value. The value box is a fixed width with tabular
   figures, so the track cannot jiggle sideways as the number under the thumb changes width. */
.lt-slider-row{display:flex; align-items:center; gap:8px; font-weight:600;}
.lt-slider-label{flex:1 1 auto; min-width:0;}
.lt-slider{flex:0 0 92px; accent-color:var(--accent); cursor:pointer; margin:0;}
.lt-slider-val{flex:0 0 auto; min-width:1.6em; text-align:right;
    font-variant-numeric:tabular-nums; color:var(--accent);}
.lt-locked .lt-slider-val{color:var(--muted);}
.lt-pack-cb{width:16px; height:16px; accent-color:var(--accent); cursor:pointer;}

/* Plain dismiss at the foot of the lobby-tools panel — kept visually quiet so it never reads as
   a "save"/"confirm" step (the settings above it apply immediately). */
.lt-close{appearance:none; cursor:pointer; align-self:center; margin-top:4px;
    padding:5px 16px; border-radius:9px;
    border:1px solid var(--panel-border); background:rgba(255,255,255,.06); color:var(--muted);
    font-family:inherit; font-size:12px; font-weight:700;
    transition:background .14s ease, color .14s ease;}
.lt-close:hover{background:rgba(255,255,255,.14); color:var(--text);}

/* Divider between two groups inside the lobby-tools extension slot. */
.lt-sep{border-top:1px solid var(--panel-border); margin:5px 0 3px;}

/* Uno's AI players: a difficulty picker and one button. Difficulties that aren't built yet are
   present but disabled, so the roadmap shows without offering something the server would refuse.
   The note underneath says WHY the button is off (wrong phase, 0-7 on, table full) — a silently
   dead button just reads as broken.
   The picker is the shared .lt-seg segmented control (defined below), three across, so the
   choice is readable at a glance instead of hidden behind a dropdown. */
.lt-bot-seg{margin:2px 0 5px;}
/* Difficulties that aren't written yet: visibly present, visibly not available. Overrides
   .lt-seg-btn:hover, which would otherwise light them up under the cursor. */
.lt-seg-btn:disabled,
.lt-seg-btn:hover:disabled{cursor:default; opacity:.4; background:rgba(255,255,255,.04);
    color:var(--muted);}
.gbtn.lt-bot-add{width:100%; padding:6px 10px; font-size:12px;}
.gbtn.lt-bot-add:disabled{opacity:.45; cursor:default; filter:grayscale(.5);}
.lt-bot-note{margin-top:4px; font-size:10.5px; line-height:1.3; color:var(--muted);}
.lt-bot-note[hidden]{display:none;}

/* Two-sided segmented toggle (Standard | Custom). */
.lt-seg{display:flex; border:1px solid var(--panel-border); border-radius:9px; overflow:hidden;
    background:rgba(255,255,255,.04);}
.lt-seg-btn{flex:1 1 0; min-width:0; appearance:none; border:none; cursor:pointer;
    background:transparent; color:var(--muted); padding:6px 8px;
    font-size:12px; font-weight:800; letter-spacing:.02em;
    transition:background .15s ease, color .15s ease;}
.lt-seg-btn:not(.is-active){background:rgba(255,255,255,.08); color:var(--muted);}
.lt-seg-btn:hover:not(.is-active){background:rgba(255,255,255,.14); color:var(--text);}
.lt-seg-btn.is-active{background:var(--accent); color:#000;}

/* The weight rows, revealed below the toggle while Custom is selected. */
.lt-weights{display:flex; flex-direction:column; gap:5px; margin-top:2px;}
.lt-weights[hidden]{display:none;}
.lt-weight-row{display:flex; align-items:center; justify-content:space-between; gap:10px; font-weight:600;}
.lt-weight-label{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.lt-weight-ctrls{display:inline-flex; align-items:center; gap:5px; flex:0 0 auto;}
.lt-weight-val{min-width:2.2em; text-align:center; font-weight:800; font-variant-numeric:tabular-nums;}
.lt-weight-btn{width:24px; height:24px; font-size:15px;}
.lt-weight-spacer{width:24px;}   /* keeps Immediate's value aligned with the button rows */

/* Small muted note under a checkbox row (e.g. "Re-passing (-50% each step)"). */
.lt-passable-hint{font-size:11px; font-weight:500; font-style:italic; color:var(--muted); margin-top:-2px;}

/* Homepage bottom-left activity counter (active lobbies / players playing vs. spectating). */
.activity-counter{
    position:fixed; left:18px; bottom:18px; z-index:5;
    display:flex; flex-direction:column; gap:4.5px;
    padding:13.5px 18px; border-radius:12px;
    background:var(--panel); border:1px solid var(--panel-border);
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);
    color:var(--muted); font-size:18px; font-weight:600; line-height:1.4;
}
@media (max-width:640px){
    .activity-counter{left:10px; bottom:10px; padding:10.5px 15px; font-size:16.5px;}
}

/* Homepage bottom-right Leaderboard button (opens a modal with top-10 lists). */
.leaderboard-btn{
    position:fixed; right:18px; bottom:18px; z-index:5;
    appearance:none; cursor:pointer; font-family:inherit;
    padding:13.5px 18px; border-radius:12px; font-size:18px; font-weight:700;
    background:var(--panel); border:1px solid var(--panel-border); color:var(--text);
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);
    transition:transform .12s ease, border-color .2s ease, background .2s ease;
}
.leaderboard-btn:hover{transform:translateY(-1px); border-color:rgba(255,255,255,.22); background:rgba(255,255,255,.06);}
.leaderboard-btn:active{transform:translateY(0);}
@media (max-width:640px){
    .leaderboard-btn{right:10px; bottom:10px; padding:10.5px 15px; font-size:16.5px;}
}

/* Reconnect prompt (main page). Parked under the top bar on the right, out of the way of the
   name/code form in the middle and of both bottom corners, which are already taken. Only ever
   visible when the server has confirmed there is a live lobby to go back to — see home.js.
   Explicit [hidden] rule because the display below would otherwise beat the UA default. */
.reconnect-card{
    position:fixed; top:76px; right:18px; z-index:6; width:min(280px,calc(100vw - 36px));
    display:flex; flex-direction:column; align-items:flex-start; gap:6px;
    padding:14px 16px; border-radius:14px;
    background:var(--panel); border:1px solid rgba(63,174,90,.5);
    box-shadow:0 10px 30px rgba(0,0,0,.45), 0 0 18px rgba(63,174,90,.14);
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);
    animation:reconnectIn .28s ease-out;
}
.reconnect-card[hidden]{display:none;}
.reconnect-title{color:var(--accent); font-weight:800; font-size:16px;}
.reconnect-text{color:var(--text); font-size:14px; line-height:1.4;}
.reconnect-seat{color:var(--muted); font-size:13px;}
.reconnect-seat[hidden]{display:none;}
.reconnect-go{margin-top:4px; width:100%; padding:9px 14px; font-size:15px;}
.reconnect-close{
    position:absolute; top:6px; right:8px; appearance:none; border:0; background:none;
    color:var(--muted); font-size:20px; line-height:1; cursor:pointer; padding:2px 4px;
}
.reconnect-close:hover{color:var(--text);}
@keyframes reconnectIn{from{opacity:0; transform:translateY(-8px);} to{opacity:1; transform:none;}}
@media (max-width:640px){
    .reconnect-card{top:64px; right:10px; left:10px; width:auto;}
}

/* Solo mode: empty seats become clickable "Add a player" slots. */
.seat.add-slot .seat-inner{border-style:solid; border-color:var(--accent);
    background:rgba(63,174,90,.14); cursor:pointer;}
.seat.add-slot .seat-inner:hover{background:rgba(63,174,90,.24); box-shadow:0 0 0 4px rgba(63,174,90,.18);}
.seat-add{position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    color:#eafff0; font-weight:800; font-size:13px; letter-spacing:.02em; pointer-events:none;}

@media (max-width:640px){
    .lobby-tools{padding:8px 10px; gap:7px; font-size:13px;}   /* left/top come from JS */
    .lt-seat-btn{width:26px; height:26px; font-size:16px;}
}

/* ============================== Uno ============================== */
/* Cards are composed from layered images in /assets/images/uno/: card_blank.jpg (a white card
   body) multiply-tinted to the card colour, card_shading.png painted black for depth, and a
   symbol layer (0.png for every card for now). Face-down cards use a dark tint + no symbol. */
.uno-center{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
    display:flex; gap:2.2vh; align-items:center; pointer-events:none; z-index:5;}
.uno-center[hidden]{display:none;}
.uno-pile{position:relative; pointer-events:auto;}
/* The discard pile shows several recent cards stacked (each an absolutely-positioned child at
   its own fixed rotation), so it needs an explicit size instead of relying on a single
   in-flow child card to size the box. */
.uno-discard{width:7.7778vh; height:11.3889vh;}

/* Play-direction indicator: two copies of direction.png, one to the LEFT of the piles and one
   to the RIGHT (180° from each other), reading together as a single loop around the table.
   Each is turned 90° (in JS) so its arc runs vertically down the side of the cards, and
   mirrors when the play direction reverses. */
.uno-dir{--dir-w:8.3333vh; --dir-gap:1vh;
    position:absolute; top:50%; width:var(--dir-w); max-width:22vw; pointer-events:none;
    opacity:.55; transition:transform .4s ease;}
/* Turned 90°, an arrow's VISUAL width is the image's height — direction.png is 2:1, so half of
   --dir-w — but its layout box stays the full unrotated width. The negative margin cancels that
   difference (--dir-w/4); --dir-gap is then the clear space left between the arc and the cards,
   so raising it pushes the arrows further out from the table centre. */
.uno-dir-right{left:100%;  margin-left:calc(var(--dir-w) / -4 + var(--dir-gap));}
.uno-dir-left{right:100%; margin-right:calc(var(--dir-w) / -4 + var(--dir-gap));}

/* Sized off viewport HEIGHT (not a fixed px), calibrated so 1440p keeps its old fixed-px size —
   on a shorter screen (e.g. 1080p) every card on the table shrinks right along with the table
   itself (which is already capped by 65vh), instead of staying a fixed pixel size and looking
   oversized relative to it. */
.uno-card{position:relative; width:7.7778vh; height:11.3889vh; border-radius:14px; overflow:hidden;
    box-shadow:0 6px 16px rgba(0,0,0,.5); box-sizing:border-box;}
/* --uno-hand-scale layers the hand's fit-safety-net on top (shrinks further only if the fan
   still wouldn't fit even at this viewport-relative size) — the table-relative piles/minis
   above don't need it since they just track the table's own vh-based size directly. */
.uno-card.big{width:calc(10.8333vh * var(--uno-hand-scale,1)); height:calc(15.8333vh * var(--uno-hand-scale,1)); border-radius:18px;}
.uno-card.mini{position:absolute; top:0; width:2.7778vh; height:4.0278vh; border-radius:7px; box-shadow:0 2px 5px rgba(0,0,0,.5);}
.uno-card > div{position:absolute; inset:0; background-position:center; background-repeat:no-repeat; background-size:cover;}
.uc-base{background-image:url(/assets/images/uno/card_blank.jpg); background-color:var(--uc,#262626); background-blend-mode:multiply;}
.uc-shade{background-color:#000; opacity:.5;
    -webkit-mask:url(/assets/images/uno/card_shading.png) center/cover no-repeat;
            mask:url(/assets/images/uno/card_shading.png) center/cover no-repeat;}
/* Symbol layer: the actual image is set per-card in JS (symbolFile, or back.png when face-down);
   0.png is only a fallback. */
.uc-sym{background-image:url(/assets/images/uno/0.png); background-size:contain;}
/* per-colour card tint (multiplied onto the white card body) */
.uc-r{--uc:#e2231a;} .uc-y{--uc:#f5c400;} .uc-g{--uc:#3aa856;} .uc-b{--uc:#1b7ced;} .uc-w{--uc:#262626;}

.uno-pile-count{position:absolute; bottom:-9px; left:50%; transform:translateX(-50%); z-index:2;
    background:rgba(0,0,0,.72); color:#fff; font-size:11px; font-weight:700; border-radius:10px; padding:1px 7px;}
.uno-draw.actionable{cursor:pointer;}
.uno-draw.actionable .uno-card{animation:seatActivePulse 1.15s ease-in-out infinite;}

/* per-seat face-down mini stacks on the felt */
.uno-seatcards{position:absolute; inset:0; pointer-events:none; z-index:4;}
.uno-seat-stack{position:absolute; transform:translate(-50%,-50%);}
/* Inner fan carries the inward rotation (cards' tops face the table centre); it's a zero-size
   box anchored on the stack point, so its rotation pivots around that point. */
.uno-seat-fan{position:absolute; left:0; top:0;}
.uno-seat-count{position:absolute; top:-15px; left:0; z-index:3; background:rgba(0,0,0,.72); color:#fff;
    font-size:11px; font-weight:700; border-radius:9px; padding:0 6px;}
.uno-seat-uno{position:absolute; top:-15px; left:28px; z-index:3; background:#d64545; color:#fff;
    font-size:10px; font-weight:900; letter-spacing:.05em; border-radius:9px; padding:1px 6px;
    animation:seatActivePulse 1.15s ease-in-out infinite;}
.uno-seat-uno.safe{background:var(--accent); animation:none;}

/* local hand: a fanned arc in the gap between the table and the bottom of the screen.
   Each card is placed (in JS) on an elliptical arc — width up to the table width, rise up to
   50% of the table height at full spread — and rotated from -30° (left) to +30° (right). */
.uno-hand{position:fixed; inset:0; z-index:36; pointer-events:none;}
.uno-hand[hidden]{display:none;}

/* "Sort" toggle: a small pill pinned to the bottom-right corner, above the hand fan. */
/* left/top are set inline per render (centred under the hand fan, just below its lowest point);
   the translate keeps it centred on that point. */
.uno-sort{position:fixed; transform:translateX(-50%); z-index:37; display:flex; align-items:center; gap:8px;
    background:var(--panel); border:1px solid var(--panel-border); border-radius:999px; padding:7px 15px;
    color:var(--text); font-size:14px; box-shadow:var(--shadow); cursor:pointer;
    -webkit-user-select:none; user-select:none;}
.uno-sort[hidden]{display:none;}
.uno-sort input{width:16px; height:16px; accent-color:var(--accent); cursor:pointer;}
.uno-hand-slot{position:absolute; pointer-events:auto; transition:left .15s ease, top .15s ease;}
.uno-hand-slot.playable{cursor:pointer;}
/* Queueable while waiting for your turn: clickable, but deliberately NO hover lift — only the
   card you actually queue rises (neighbours still part sideways on hover). */
.uno-hand-slot.selectable{cursor:pointer;}
/* Lift on hover, but never reorder stacking — bumping z-index on hover would let the lifted
   card's full rectangle start capturing the mouse over its neighbours too, ballooning its
   hitbox past what it was at rest. Keep the original stacking; only the visual moves.
   translateY is applied to the card, which already sits inside the tilted .uno-hand-slot, so
   the lift naturally follows that tilt (diagonally) instead of straight up on screen. */
.uno-hand .uno-card{transition:transform .12s ease;}
.uno-hand .uno-card.playable{transform:translateY(-10px); animation:unoPlayablePulse 1.15s ease-in-out infinite;}
.uno-hand-slot.playable:hover .uno-card{transform:translateY(-24px);}
/* Queued while waiting for your turn: same lift as a playable card, gold so it reads as
   "I picked this" rather than "this is legal right now". */
.uno-hand .uno-card.preselected{transform:translateY(-10px); animation:unoPreselectPulse 1.15s ease-in-out infinite;}
@keyframes unoPreselectPulse{
    0%,100%{box-shadow:0 6px 16px rgba(0,0,0,.5), 0 0 0 2px rgba(255,213,74,.85);}
    50%{box-shadow:0 6px 16px rgba(0,0,0,.5), 0 0 0 3px rgba(255,213,74,1), 0 0 12px rgba(255,193,7,.7);}}
@keyframes unoPlayablePulse{
    0%,100%{box-shadow:0 6px 16px rgba(0,0,0,.5), 0 0 0 2px rgba(255,255,255,.85);}
    50%{box-shadow:0 6px 16px rgba(0,0,0,.5), 0 0 0 2px rgba(255,255,255,1), 0 0 10px rgba(255,255,255,.6);}}
/* Double-down (host rule): the identical twin of the card you just played, offered as an
   optional follow-up. Amber to match the "End turn" button that declines it, so the card and
   the button read as the two halves of one either/or choice. */
.uno-hand .uno-card.doubling{transform:translateY(-10px); animation:unoDoublePulse 1.15s ease-in-out infinite;}
@keyframes unoDoublePulse{
    0%,100%{box-shadow:0 6px 16px rgba(0,0,0,.5), 0 0 0 2px rgba(232,164,64,.85);}
    50%{box-shadow:0 6px 16px rgba(0,0,0,.5), 0 0 0 3px rgba(232,164,64,1), 0 0 14px rgba(232,164,64,.75);}}

/* Jump-in (host rule): an exact copy of the top card, playable OUT of turn. Cyan rather than
   white, so it never reads as "it's your turn" — it's a cut-in you may take, not one you owe. */
.uno-hand .uno-card.jumpable{transform:translateY(-10px); animation:unoJumpPulse 1.15s ease-in-out infinite;}
@keyframes unoJumpPulse{
    0%,100%{box-shadow:0 6px 16px rgba(0,0,0,.5), 0 0 0 2px rgba(64,224,232,.85);}
    50%{box-shadow:0 6px 16px rgba(0,0,0,.5), 0 0 0 3px rgba(64,224,232,1), 0 0 14px rgba(64,224,232,.75);}}

/* wild colour picker */
.uno-color-modal{position:fixed; inset:0; z-index:80; background:rgba(0,0,0,.55);
    display:flex; align-items:center; justify-content:center;}
.uno-color-modal[hidden]{display:none;}
.uno-color-card{background:var(--panel); border:1px solid var(--panel-border); border-radius:16px;
    padding:22px 24px; text-align:center; box-shadow:var(--shadow);}
.uno-color-title{color:var(--text); font-weight:700; margin-bottom:14px;}
.uno-color-row{display:flex; gap:14px;}
.uno-color-btn{width:56px; height:56px; border-radius:12px; border:2px solid rgba(255,255,255,.2);
    cursor:pointer; transition:transform .1s ease; background:var(--uc,#666);}
.uno-color-btn:hover{transform:translateY(-2px);}

/* "UNO!" call button: nudged up out of the action row so it reads as its own shout.
   Uses top (not transform) so .gbtn's hover lift still works. */
.uno-call-btn{position:relative; top:-10px;}

/* winner banner */
.uno-winner{text-align:center;}
.uno-winner .uw-title{font-size:24px; font-weight:800; color:var(--text);}
.uno-winner .uw-sub{font-size:14px; color:var(--muted); margin-top:6px;}

@media (max-width:640px){
    .uno-card{width:92px; height:136px;}
    .uno-card.big{width:calc(120px * var(--uno-hand-scale,1)); height:calc(176px * var(--uno-hand-scale,1));}
    .uno-card.mini{width:32px; height:46px;}
    .uno-discard{width:92px; height:136px;}
}

/* ============================== Immunity ============================== */
/* Cards are the clean SVG deck in /assets/images/shared/cards/ (rank+suit, e.g. AS.svg;
   1B.svg is the back). Everything on the table is sized off viewport height so the whole
   board shrinks with the (65vh-capped) table on shorter screens. All hands are public, so
   the board below the table shows every player's cards; matching is computed client-side. */
.imm-board{position:absolute; inset:0; pointer-events:none;}
.imm-board[hidden]{display:none;}

/* Face-down draw pile, tucked into the felt's left interior (clear of the seat ring) and
   centred on the same middle line as the grid. */
.imm-drawpile{position:absolute; left:20%; top:50%; transform:translateY(-50%); height:6vh; aspect-ratio:5/7;}
.imm-drawpile[hidden]{display:none;}
.imm-draw-count{position:absolute; left:50%; bottom:-2.1vh; transform:translateX(-50%);
    background:rgba(0,0,0,.72); color:#fff; font-size:1.3vh; font-weight:700; border-radius:1vh; padding:.1vh .8vh;}

/* 4x4 grid: columns fill left->right as the rounds are played. The four cards in a column
   overlap by half their height (a compact downward stack), so a bigger card size still fits. */
/* Centred at 50%: the box includes the column headers, so an equal gap falls above the
   labels and below the last card — matching the top and bottom seats' symmetry. */
.imm-grid{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
    --imm-grid-h:6.9vh; display:grid; grid-template-columns:repeat(4,auto); gap:1vh; z-index:5;}
.imm-col{display:flex; flex-direction:column; align-items:center; gap:0;}
.imm-col-head{font-size:1.5vh; font-weight:700; letter-spacing:.05em; text-transform:uppercase; opacity:.9; margin-bottom:.5vh;}
.imm-col-self .imm-col-head{color:#7fbfff;}   /* self-drinking columns */
.imm-col-deal .imm-col-head{color:#f0b64d;}   /* dealing columns */
.imm-cell{height:var(--imm-grid-h); aspect-ratio:5/7; display:flex; align-items:center; justify-content:center;}
.imm-cell + .imm-cell{margin-top:calc(var(--imm-grid-h) * -0.5);}   /* 50% vertical overlap */
.imm-cell.imm-empty{border:.2vh dashed rgba(255,255,255,.18); border-radius:.6vh;}

/* base card image */
.imm-card{display:block; width:auto; aspect-ratio:5/7; border-radius:.6vh; background:#fff;
    box-shadow:0 .4vh 1vh rgba(0,0,0,.45);}
.imm-cell .imm-card.grid{height:100%;}
.imm-card.back{position:absolute; inset:0; height:100%; width:100%; border-radius:.6vh;}
.imm-card.hand{height:12vh;}
.imm-card.handmini{height:6.2vh;}       /* other players: half the size of your own hand */
.imm-card.left{height:8vh;}
.imm-card.match{box-shadow:0 0 0 .25vh #ffd24a, 0 0 1vh rgba(255,210,74,.7);}
/* At the immunity reveal, your hand cards that also appear among the Non-Immunity cards. */
.imm-card.match-nonimm{box-shadow:0 0 0 .28vh #e2231a, 0 0 1vh rgba(226,35,26,.75);}

@keyframes immIn{ from{opacity:0; transform:translateY(-8px) scale(.7);} to{opacity:1; transform:none;} }
.imm-card.imm-in{animation:immIn .32s ease both;}

/* Revealed "Non-Immunity Cards" — a labelled panel sitting on top of the grid, with a shadow
   so the four leftovers read as a distinct group and don't get mixed with the columns. */
.imm-leftover{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); z-index:8;
    display:flex; flex-direction:column; align-items:center; gap:.8vh; padding:1.4vh 1.8vh;
    background:rgba(10,7,4,.85); border:1px solid rgba(255,255,255,.14); border-radius:1vh;
    box-shadow:0 1.5vh 4vh rgba(0,0,0,.6);}
.imm-leftover[hidden]{display:none;}
.imm-leftover-label{font-size:1.7vh; font-weight:800; color:#ffd24a; letter-spacing:.03em; text-transform:uppercase;}
.imm-leftover-row{display:flex; gap:.8vh;}

/* ----- hands below the table (#game-info) ----- */
.imm-hands{display:flex; flex-direction:column; align-items:center; gap:14px; width:100%;}
.imm-others{display:grid; grid-template-columns:repeat(2,auto); gap:8px 24px; justify-content:center; margin-top:2px;}
/* Each hand gets its own framed panel so the players read as separate groups of cards. */
.imm-hand-block{display:flex; flex-direction:column; align-items:center; padding:8px 10px; border-radius:12px;
    border:1px solid rgba(255,255,255,.14); background:rgba(255,255,255,.05);}
.imm-hand-block.empty{opacity:.5;}
.imm-hand-name{display:flex; align-items:center; justify-content:center; gap:6px; margin-bottom:5px;
    font-size:13px; font-weight:700; color:var(--text);}
.imm-hand-block.big .imm-hand-name{font-size:15px;}
.imm-shield{font-size:13px; filter:drop-shadow(0 0 3px rgba(79,180,220,.8));}
.imm-hand-cards{display:flex; gap:6px; align-items:flex-end; justify-content:center;}
.imm-nocards{font-size:12px; font-style:italic; color:var(--muted); padding:2.5vh 1vh;}

/* A dealing round can show two badges side by side: what's left to give, then what came in. */
.imm-badges{display:flex; flex-wrap:wrap; gap:5px; justify-content:center;}
.imm-badge{margin-top:6px; font-size:12px; font-weight:800; letter-spacing:.02em; padding:2px 11px; border-radius:999px;}
.imm-badge.drink{background:#d64545; color:#fff;}
.imm-badge.give{background:#e6a12e; color:#241a10;}
.imm-badge.recv{background:#d64545; color:#fff;}
.imm-badge.immune{background:#2f8fbf; color:#eaf6ff;}

/* immune players glow; during a dealing round the other blocks become click-to-give targets */
.imm-hand-block.immune{box-shadow:0 0 0 2px rgba(79,180,220,.9), 0 0 14px rgba(79,180,220,.45);}
.imm-givable{cursor:pointer; border-color:#ffd54a; transition:transform .1s ease;
    animation:piratePick 1.15s ease-in-out infinite;}
.imm-givable:hover{transform:translateY(-3px);}
/* While handing out sips, the opponents' seat avatars glow the same gold as their hand panels. */
/* Uno 0-7 rule: after playing a 7, pick whose hand to take — same gold pick-glow as Immunity's
   sip dealing, since it's the same "click an opponent's avatar" gesture. */
.seat.occupied.uno-swap-target{cursor:pointer;}
.seat.occupied.uno-swap-target .seat-inner{border-color:#ffd54a; animation:piratePick 1.15s ease-in-out infinite;}

/* Someone is on their last card and never called Uno: their own avatar breathes a red outline,
   and hovering it offers "Catch". Deliberately NOT a button in the action row — a button lands in
   everyone's face at the same instant and gets mashed on sight, so whoever forgot never had a
   chance. A quiet red ring on one avatar has to be spotted first, which is the whole game of it.
   Sits AFTER .seat-active-turn (line ~461) on purpose: same specificity, so this wins the
   animation when the player who forgot is also the one on turn — the red is the louder signal. */
.seat.occupied.uno-catch-target .seat-inner{cursor:pointer;
    animation:unoCatchPulse 1.5s ease-in-out infinite;}
@keyframes unoCatchPulse{
    0%,100%{box-shadow:0 6px 16px rgba(0,0,0,.4), 0 0 0 2px rgba(214,69,69,.45), 0 0 10px rgba(214,69,69,.25);}
    50%    {box-shadow:0 6px 16px rgba(0,0,0,.4), 0 0 0 4px rgba(214,69,69,.9), 0 0 20px rgba(214,69,69,.6);}
}
/* The hover label is a pseudo-element fed by data-catch-label (set in uno.js): game-core rewrites
   .seat-inner's CHILDREN on every render, which would wipe an injected overlay, but the node and
   its attributes survive. Same dim-and-label look as the "Leave Seat" overlay. */
.seat.occupied.uno-catch-target .seat-inner::after{
    content:attr(data-catch-label); position:absolute; inset:0; z-index:5; border-radius:50%;
    display:flex; align-items:center; justify-content:center; text-align:center; padding:0 6px;
    background:rgba(104,16,16,.62); color:#fff; font-size:11px; font-weight:800; line-height:1.15;
    text-shadow:0 1px 3px rgba(0,0,0,.85); opacity:0; transition:opacity .15s ease;}
.seat.occupied.uno-catch-target .seat-inner:hover::after{opacity:1;}

.seat.occupied.imm-give-target{cursor:pointer;}
.seat.occupied.imm-give-target .seat-inner{border-color:#ffd54a; animation:piratePick 1.15s ease-in-out infinite;}
/* NOTE: deliberately no "this target is immune" hover state — the client isn't told who is
   immune, and every target must look the same. */

@media (max-width:640px){
    .imm-card.hand{height:78px;}
    .imm-card.handmini{height:42px;}
    .imm-others{gap:6px 14px;}
}

/* ============================== Blackjack ============================== */
/* Shared SVG deck in /assets/images/shared/cards/ (rank+suit, e.g. AS.svg; 1B.svg is the back).
   Everything on the felt is sized off viewport height so the whole board shrinks with the
   (65vh-capped) table on shorter screens. Two decks (104) form the draw stack; the autonomous
   dealer's cards sit in the centre, and every player's mini fan sits in front of their seat. */
.bj-board{position:absolute; inset:0; pointer-events:none;}
.bj-board[hidden]{display:none;}

/* base card image (heights are set per context) */
.bj-card{display:block; width:auto; aspect-ratio:5/7; border-radius:.6vh; background:#fff;
    box-shadow:0 .4vh 1vh rgba(0,0,0,.45);}

/* Face-down draw stack, left interior, centred on the table midline (kept clear of the side
   seats' mini fans). */
.bj-drawpile{position:absolute; left:26%; top:50%; transform:translate(-50%,-50%); height:6vh; aspect-ratio:5/7;}
.bj-drawpile[hidden]{display:none;}
.bj-card.back{position:absolute; inset:0; height:100%; width:100%; border-radius:.6vh;}
.bj-count{position:absolute; left:50%; bottom:-2.1vh; transform:translateX(-50%);
    background:rgba(0,0,0,.72); color:#fff; font-size:1.3vh; font-weight:700; border-radius:1vh; padding:.1vh .8vh;}

/* Autonomous dealer's cards, centred (box width/height + card left/height set in JS: four cards
   fit with a small gap, a fifth+ overlaps to stay inside). */
.bj-dealer{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); z-index:5; perspective:700px;}
.bj-dealer-card{position:absolute; top:0; aspect-ratio:5/7; border-radius:.6vh; background:#fff;
    box-shadow:0 .4vh 1vh rgba(0,0,0,.5); transition:left .3s ease;}
.bj-dealer-label{position:absolute; top:-2.5vh; left:50%; transform:translateX(-50%);
    font-size:1.5vh; font-weight:800; letter-spacing:.06em; text-transform:uppercase; color:#e9d9b6; white-space:nowrap;}
.bj-dealer-total{position:absolute; bottom:-2.8vh; left:50%; transform:translateX(-50%);
    background:rgba(0,0,0,.78); color:#fff; font-size:1.6vh; font-weight:800; border-radius:1vh; padding:.1vh 1vh;
    font-variant-numeric:tabular-nums;}
.bj-dealer-total.bust{background:#d64545;}
.bj-dealer-total.win{background:#e3b23c; color:#241a10;}

/* Face-up drop stack (right): the last 10 used cards, tilt + shadow set in JS. Pulled in from
   the edge so it doesn't crowd the right-hand player's cards. */
.bj-discard{position:absolute; left:74%; top:50%; transform:translate(-50%,-50%); height:6vh; aspect-ratio:5/7;}
.bj-discard[hidden]{display:none;}
.bj-discard-card{position:absolute; inset:0; height:100%; width:100%; border-radius:.6vh;}

/* A card in flight from the draw stack to its seat/dealer target. */
.bj-fly{box-shadow:0 .8vh 1.8vh rgba(0,0,0,.55);}

/* per-seat face-up mini fans on the felt */
.bj-seatcards{position:absolute; inset:0; pointer-events:none; z-index:4;}
.bj-seat-stack{position:absolute; transform:translate(-50%,-50%);}
.bj-seat-fan{position:absolute; left:0; top:0;}
.bj-mini{position:absolute; top:0; left:0; height:3.6vh; aspect-ratio:5/7; border-radius:.4vh;
    box-shadow:0 .2vh .5vh rgba(0,0,0,.5);}
.bj-seat-total{position:absolute; left:0; top:2.7vh; transform:translate(-50%,-50%); z-index:3;
    background:rgba(0,0,0,.72); color:#fff; font-size:0.91vh; font-weight:800; border-radius:1vh; padding:0 .7vh;
    font-variant-numeric:tabular-nums;}
.bj-seat-total.bust{background:#d64545;}
.bj-seat-total.win{background:#e3b23c; color:#241a10;}

/* ----- hands below the table (#game-info): my hand large, everyone else small ----- */
.bj-hands{display:flex; flex-direction:column; align-items:center; gap:14px; width:100%;}
/* Turn buttons sit between my hand and the other players' hands. */
.bj-turn-actions{display:flex; gap:12px; justify-content:center; flex-wrap:wrap; margin:-2px 0;}
.bj-others{display:flex; flex-wrap:wrap; gap:8px 18px; justify-content:center;}
.bj-hand-block{display:flex; flex-direction:column; align-items:center; padding:8px 10px; border-radius:12px;
    border:1px solid rgba(255,255,255,.14); background:rgba(255,255,255,.05);}
.bj-hand-block.empty{opacity:.5;}
.bj-hand-name{display:flex; align-items:center; justify-content:center; gap:7px; margin-bottom:5px;
    font-size:13px; font-weight:700; color:var(--text);}
.bj-hand-block.big .bj-hand-name{font-size:15px;}
.bj-hand-total{font-weight:800; background:rgba(0,0,0,.35); border-radius:999px; padding:0 8px;
    font-variant-numeric:tabular-nums;}
.bj-hand-total.bust{color:#ff8a8a;}
.bj-hand-total.win{color:#ffd24a;}
.bj-hand-cards{display:flex; gap:6px; align-items:flex-end; justify-content:center; min-height:6.2vh;}
.bj-card.hand{height:12vh;}
.bj-card.handmini{height:6.2vh;}

.bj-badge{margin-top:6px; font-size:12px; font-weight:800; letter-spacing:.02em; padding:2px 11px; border-radius:999px;}
.bj-badge.win{background:var(--accent); color:#06210f;}
.bj-badge.lose{background:#d64545; color:#fff;}
.bj-badge.push{background:#6b7280; color:#fff;}

/* During a 21 payout the other players' hand panels (below) AND their seat avatars (on the
   table) become click-to-give targets — a steady gold "pick a player" glow reusing Drunk
   Pirate's name-pick pulse. The active player's own white turn-glow steps aside meanwhile. */
.bj-givable{cursor:pointer; border-color:#ffd54a; transition:transform .1s ease;
    animation:piratePick 1.15s ease-in-out infinite;}
.bj-givable:hover{transform:translateY(-3px);}
.seat.occupied.bj-give-target{cursor:pointer;}
.seat.occupied.bj-give-target .seat-inner{border-color:#ffd54a; animation:piratePick 1.15s ease-in-out infinite;}

@media (max-width:640px){
    .bj-card.hand{height:78px;}
    .bj-card.handmini{height:42px;}
    .bj-others{gap:6px 12px;}
}

/* ============================ Account system ============================ */
/* Header buttons: both green — plain Login for guests, gear + label once logged in. */
.login-btn,.account-btn{border-color:rgba(63,174,90,.55); color:#0c2413; font-weight:800;
    background:linear-gradient(180deg,var(--accent-hover),var(--accent));}
.login-btn:hover,.account-btn:hover{filter:brightness(1.06); border-color:rgba(63,174,90,.85);}
.account-btn{display:inline-flex; align-items:center; gap:6px;}
.account-btn .account-gear{opacity:.9;}
.account-btn-label{font-weight:700;}

/* Pill-button variants inside the account modals (.acct-green = default .btn-start green). */
.acct-white{color:#1b130d; background:linear-gradient(180deg,#ffffff,#e9e6df);
    box-shadow:0 10px 24px rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.6);}

/* Blank row that breaks the login modal into groups. The card's own flex gap sits on
   both sides of it, so a few pixels here read as a clear separation. */
.acct-gap{height:4px;}

/* "Remember me" row, first item after a spacer -> no negative pull-up. */
.acct-remember{display:flex; align-items:center; justify-content:center; gap:8px; margin:0;
    cursor:pointer; user-select:none; font-size:17px; font-weight:700; color:var(--muted);}
.acct-remember input{width:19.5px; height:19.5px; accent-color:var(--accent); cursor:pointer;}

/* "Forgot password?" under the login password field: a light-red underlined text link. */
.acct-forgot-link{appearance:none; border:none; background:none; padding:0; margin:-6px 0 0;
    align-self:center; cursor:pointer; font-family:inherit; font-size:13px;
    color:#ffb0a3; text-decoration:underline;}
.acct-forgot-link:hover{color:#ffd6d0;}

.acct-switch-note{margin:0 0 -4px; color:var(--muted); font-size:13px;}
.acct-hint{margin:-4px 0 0; color:var(--muted); font-size:12px; text-align:left;}
.acct-or{text-align:center;}
.acct-q-text{margin:0; font-weight:700; text-align:left; color:var(--text);}
.acct-select{cursor:pointer;}
/* The open dropdown list is drawn by the OS, which would otherwise default to white.
   Only the resting rows can be styled - the hovered/selected one keeps the system
   highlight colour. */
.acct-select option{background-color:#2b2520; color:var(--text);}

/* Account Settings is a right-anchored dropdown (reuses .settings-panel positioning),
   not a centered modal. It's taller than the cog panel, so it scrolls within the viewport. */
.account-panel{max-height:calc(100vh - 74px); overflow:auto;}
.acct-panel-title{margin:0 24px 2px 0; font-size:16px;}
#account-customizer{margin:0;}
#account-customizer .avatar-stage{width:92px; height:92px;}

/* Two-section layout: "Settings" then a divider then "Account". */
.acct-section-heading{margin:6px 0 2px; font-size:15px; align-self:flex-start; color:var(--text);}
.acct-divider{width:100%; border:none; border-top:1px solid var(--panel-border); margin:8px 0 2px;}

/* Account section: one full-width button per row (View Stats / Change Password / Logout). */
.acct-row-btn{width:100%; appearance:none; border:1px solid var(--panel-border); background:rgba(255,255,255,.05);
    color:var(--text); border-radius:10px; padding:11px 14px; font-size:15px; font-weight:700; cursor:pointer;
    transition:transform .12s ease, filter .2s ease, background .2s ease;}
.acct-row-btn:hover:not(:disabled){transform:translateY(-1px); background:rgba(255,255,255,.1);}
.acct-row-btn:disabled{opacity:.45; cursor:default;}
.acct-row-btn--danger{border-color:rgba(214,69,69,.5); color:#ffd6d0; background:rgba(214,69,69,.16);}
.acct-row-btn--danger:hover:not(:disabled){background:rgba(214,69,69,.28);}
/* Save button in the account panel: shrink to the row buttons' height (Save is a big .btn-start). */
#acct-save{padding:11px 14px; font-size:15px;}
/* Statistics row: tint slightly brighter than the other rows. */
#acct-view-stats{background:rgba(255,255,255,.11);}
#acct-view-stats:hover:not(:disabled){background:rgba(255,255,255,.16);}

/* The change-password "success" reuses the error line, recoloured green. */
.field-error.acct-ok{color:#8fe6a4;}

/* Statistics modal: an Overall section + one per game, laid out in responsive columns. */
.stats-card{max-width:640px;}
.stats-body{display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:16px 22px;}
.stats-section{min-width:0;}
.stats-head{margin:0 0 6px; font-size:14px; letter-spacing:.02em; color:var(--accent);
    border-bottom:1px solid var(--panel-border); padding-bottom:4px;}
.stat-row{display:flex; justify-content:space-between; align-items:baseline; gap:10px; font-size:13px; padding:3px 0;}
.stat-label{color:var(--muted); min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.stat-value{color:var(--text); font-weight:700; font-variant-numeric:tabular-nums; flex:0 0 auto;}
.stats-empty{color:var(--muted); font-style:italic; text-align:center; margin:8px 0;}

/* ---------- Administration modal ---------- */
/* A wider card whose header carries the search box on the right (padded clear of the X), over
   three lists that scroll independently — so a busy section never pushes the others off-screen. */
.admin-card{max-width:820px;}
.admin-head{display:flex; align-items:center; gap:14px; padding-right:26px; margin-bottom:14px;}
.admin-head h2{margin:0; flex:0 0 auto;}
.admin-search{flex:1 1 auto; min-width:0; max-width:280px; margin-left:auto; padding:9px 13px; font-size:14px;}
/* "Invisible Spectator" — the admin's own setting, so it sits in the header next to the title
   rather than in any account row. Same pill as the per-account admin toggle, minus the
   uppercase shout, since this one is read as a sentence rather than scanned as a badge. */
.admin-invis{flex:0 0 auto; display:inline-flex; align-items:center; gap:6px; cursor:pointer;
    padding:4px 10px; border-radius:999px; font-size:12px; font-weight:700; color:var(--muted);
    user-select:none; border:1px solid var(--panel-border); background:rgba(255,255,255,.06);
    transition:background .2s ease, border-color .2s ease, color .2s ease;}
.admin-invis input{width:14px; height:14px; margin:0; cursor:pointer; accent-color:var(--accent);}
.admin-invis.is-on{border-color:rgba(214,171,69,.5); background:rgba(214,171,69,.16); color:#f2dda6;}
.admin-invis.is-error{border-color:rgba(214,69,69,.6); background:rgba(214,69,69,.2); color:#ffd6d0;}
.admin-invis input:disabled{cursor:default;}   /* locked until the server says what it is */
.admin-sections{display:flex; flex-direction:column; gap:14px;}
.admin-section{min-width:0;}
.admin-section-head{display:flex; align-items:baseline; justify-content:space-between; gap:10px;
    margin:0 0 6px; padding-bottom:4px; font-size:14px; letter-spacing:.02em; color:var(--accent);
    border-bottom:1px solid var(--panel-border);}
.admin-count{color:var(--muted); font-size:12px; font-weight:700; font-variant-numeric:tabular-nums;}
.admin-list{display:flex; flex-direction:column; gap:4px; padding-right:4px;
    max-height:clamp(120px,24vh,260px); overflow-y:auto;}

/* One row = an identity line (name + tags + a right-aligned headline number) over a muted
   line of "label value" pairs. Rows are built by account.js from /admin.php. */
.adm-row{padding:6px 9px; border-radius:9px; border:1px solid transparent; background:rgba(255,255,255,.04);}
.adm-row:hover{background:rgba(255,255,255,.08); border-color:var(--panel-border);}
.adm-row-top{display:flex; align-items:center; flex-wrap:wrap; gap:7px 7px; min-width:0;}
.adm-name{min-width:0; font-size:14px; font-weight:700; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.adm-code{letter-spacing:.16em;}
.adm-badge{margin-left:auto; flex:0 0 auto; font-size:12px; font-weight:700; color:var(--text); font-variant-numeric:tabular-nums;}
.adm-meta{margin-top:2px; font-size:12px; line-height:1.45; color:var(--muted); word-break:break-word;}
.adm-tag{flex:0 0 auto; padding:2px 6px; border-radius:6px; font-size:10px; font-weight:800;
    letter-spacing:.06em; text-transform:uppercase; color:var(--muted);
    border:1px solid var(--panel-border); background:rgba(255,255,255,.06);}
.adm-tag--online,.adm-tag--run{border-color:rgba(63,174,90,.5); background:rgba(63,174,90,.16); color:#cdeed6;}
.adm-tag--host{border-color:rgba(94,150,214,.5); background:rgba(94,150,214,.16); color:#d3e4f7;}
.adm-empty{margin:8px 0; color:var(--muted); font-style:italic; text-align:center;}

/* Admin checkbox: a .adm-tag-shaped control that saves the moment it's clicked. Its states are
   set as classes by account.js (is-on / is-locked / is-error), never inferred from the input. */
.adm-admin-toggle{flex:0 0 auto; display:inline-flex; align-items:center; gap:5px; cursor:pointer;
    padding:2px 7px 2px 5px; border-radius:6px; font-size:10px; font-weight:800;
    letter-spacing:.06em; text-transform:uppercase; color:var(--muted); user-select:none;
    border:1px solid var(--panel-border); background:rgba(255,255,255,.06);
    transition:background .2s ease, border-color .2s ease, color .2s ease;}
.adm-admin-toggle input{width:13px; height:13px; margin:0; cursor:pointer; accent-color:var(--accent);}
.adm-admin-toggle.is-on{border-color:rgba(214,171,69,.5); background:rgba(214,171,69,.16); color:#f2dda6;}
.adm-admin-toggle.is-locked,.adm-admin-toggle.is-locked input{cursor:default;}
.adm-admin-toggle.is-error{border-color:rgba(214,69,69,.6); background:rgba(214,69,69,.2); color:#ffd6d0;}

/* Account AND lobby rows expand on click, to reveal what would otherwise sit one accidental click
   away from the line everyone scans first: an account's admin checkbox and Reset/Delete, a lobby's
   roster and its Join button (which navigates off this page entirely). */
.adm-row-toggle{cursor:pointer;}
.adm-row-toggle::after{content:"\203A"; margin-left:auto; flex:0 0 auto; padding-left:4px;
    color:var(--muted); font-size:16px; line-height:1; transition:transform .15s ease;}
.adm-row--account.is-expanded .adm-row-toggle::after{transform:rotate(90deg);}
.adm-row-body{margin-top:8px; padding-top:8px; border-top:1px solid var(--panel-border);
    display:flex; flex-direction:column; gap:8px;}
.adm-row-body[hidden]{display:none;}   /* the [hidden] attribute alone loses to display:flex above */
.adm-actions-row{display:flex; align-items:center; gap:10px; flex-wrap:wrap;}
.adm-actions-right{display:flex; align-items:center; gap:8px; margin-left:auto;}

/* The player list inside an expanded lobby row. Indented and rule-marked down its left edge so it
   reads as belonging to the lobby above it rather than as more rows in the list. */
.adm-sub-title{font-size:11px; font-weight:800; letter-spacing:.04em; text-transform:uppercase;
    color:var(--muted);}
.adm-sub-list{display:flex; flex-direction:column; gap:4px;
    padding-left:10px; border-left:2px solid var(--panel-border);}
.adm-sub-row{display:flex; align-items:center; gap:6px; flex-wrap:wrap; font-size:12px;}
.adm-sub-name{font-weight:700; color:var(--text);}
.adm-sub-meta{color:var(--muted); font-size:11px;}
.adm-action-btn{appearance:none; cursor:pointer; padding:6px 12px; border-radius:8px;
    font-size:12px; font-weight:700; color:var(--text);
    border:1px solid var(--panel-border); background:rgba(255,255,255,.06);
    transition:background .2s ease, transform .12s ease;}
.adm-action-btn:hover:not(:disabled){background:rgba(255,255,255,.12); transform:translateY(-1px);}
.adm-action-btn:disabled{opacity:.5; cursor:default; transform:none;}
.adm-action-btn--danger{border-color:rgba(214,69,69,.5); background:rgba(214,69,69,.16); color:#ffd6d0;}
.adm-action-btn--danger:hover:not(:disabled){background:rgba(214,69,69,.28);}

@media (max-width:640px){
    .admin-head{flex-wrap:wrap; gap:8px;}
    .admin-search{width:100%; max-width:none; margin-left:0;}
    .admin-list{max-height:200px;}
}

/* ============================ Ring of Fire ============================ */
/* The interactive ring: 52 cards on ring.png, living inside #game-layer (which is absolutely
   positioned over the table felt). Everything below is sized in PERCENTAGES of that layer, the
   same way Uno's piles and Blackjack's board are — so the ring tracks the table through
   scrolling and zooming with no JS measurement at all. JS only toggles .mode-active. */
.ring-stage{position:absolute; left:50%; top:50%; width:50%; aspect-ratio:1/1;
    transform:translate(-50%,-50%); z-index:6; pointer-events:auto;   /* .game-layer is pointer-events:none */
    transition:top .45s ease, width .45s ease, transform .45s ease;}
.ring-stage[hidden]{display:none;}
/* The active player, while they still have to choose: bigger, and hung below the felt's bottom
   edge so the ring they're about to click isn't sitting on the table's own art. */
.ring-stage.mode-active{width:66%; top:100%; transform:translate(-50%,1%);}
/* --ring-radius (distance from the wheel's centre out to each card) lives HERE rather than on
   .ring-card so the king-progress dial can size against it too. Percentages resolve at the use
   site, not where the variable is declared, so the cards read it exactly as before. */
.ring-wheel{--ring-radius:20%;
    position:relative; width:100%; height:100%;}
/* ring.png sits on its OWN layer rather than on .ring-wheel's background, so a King can spin it
   without dragging the cards (which are the wheel's children) around with it. It still shares the
   wheel's box exactly, so the image and the card circle stay concentric. */
.ring-bg{position:absolute; inset:0; pointer-events:none;
    background-size:contain; background-position:center; background-repeat:no-repeat;}
/* Nudge in "active" mode only (see above): applied to the WHEEL, so the ring image and the card
   circle move together and stay concentric by construction. */
.ring-stage.mode-active .ring-wheel{transform:translateY(-25px);}
/* Each card hangs off a full-size spoke rotated about the wheel's centre (rotation set inline by
   ring.js). Because the spoke is the full wheel, the card's own top/width percentages resolve
   against the WHEEL — that's what makes the radius a pure ratio with no pixel maths. */
.ring-spoke{position:absolute; inset:0; pointer-events:none;}
.ring-card{position:absolute; left:50%; top:calc(50% - var(--ring-radius)); width:11%; aspect-ratio:2/3;
    transform:translate(-50%,-50%); pointer-events:auto;
    padding:0; border:none; background:none; cursor:default;
    transition:filter .12s ease;}
.ring-card[hidden]{display:none;}
/* King-draw progress, filling the ring's hollow centre: radius 45% of the card ring's, i.e. a
   diameter of 0.9x --ring-radius. Inline SVG rather than a conic-gradient so the "n/4" label
   scales with the viewBox — a CSS font-size can't be a % of the wheel (percentages there resolve
   against the parent's font-size), which would have left the text fixed while the dial scaled. */
.ring-kings{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
    width:calc(var(--ring-radius) * .9); aspect-ratio:1/1; z-index:3; pointer-events:none;
    overflow:visible; filter:drop-shadow(0 2px 5px rgba(0,0,0,.55));}
.ring-kings[hidden]{display:none;}
.ring-kings .kp-pie{transition:stroke-dasharray .45s ease, stroke .45s ease;}
.ring-kings .kp-label{fill:#fff; font-weight:900; font-family:inherit;
    paint-order:stroke; stroke:rgba(0,0,0,.75); stroke-width:4px; stroke-linejoin:round;}
/* Flat colour back with a white rim. The rim is a border (not padding + an inner box) so it
   scales with the card and never lets two overlapping cards' colour fields touch — that white
   gap is the only thing separating one card from the next in a tight ring. */
.ring-card-back{--ink:#1b1b1b;
    width:100%; height:100%; box-sizing:border-box; border-radius:4px;
    background:var(--ink); border:1px solid rgba(255,255,255,.95);
    /* Three edges, all softened with blur rather than spread — spread is what re-introduces a
       hard rasterized outline on a rotated box:
         1. inset white, feathering the rim inward so it doesn't meet the ink as a crisp line,
         2. dark edge just outside the rim,
         3. the card's own drop shadow. */
    box-shadow:inset 0 0 2px 0 rgba(255,255,255,.8),
               0 0 2.5px .5px rgba(0,0,0,.45),
               0 2px 5px rgba(0,0,0,.5);
    /* Each card is rotated by its spoke, and a hard 1px border on a rotated box rasterizes into
       visible stair-steps. A sub-pixel blur smooths those edges; the back is a flat colour, so
       there's no interior detail for it to cost us. */
    filter:blur(.5px);
    transition:transform .12s ease;}
.ring-card-back.is-red{--ink:#b5342a;}
/* Play-direction arrows, the same indicator Drunk Pirate and Uno use: two copies of
   direction.png, 180deg apart, reading together as one loop. Every King flips the direction, so
   this is the readout for that.
   These sit OUTSIDE .ring-stage (which moves between the table and below it) and stay pinned to
   the table centre. .ring-center is an invisible anchor box carrying the exact dimensions of
   Drunk Pirate's .pirate-center, and the arrow values below match .pirate-dir exactly, so both
   games place their arrows at an identical distance from the table centre. Keep them in sync. */
.ring-center{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
    width:clamp(118.75px,25%,234.375px); aspect-ratio:2/1; pointer-events:none;
    transition:transform .4s ease;}   /* animates the per-turn 180deg spin (see renderDirection) */
.ring-center[hidden]{display:none;}
.ring-dir{--dir-w:11vh; --dir-gap:calc(1vh + var(--dir-w) * .3);
    position:absolute; top:50%; width:var(--dir-w); max-width:22vw; pointer-events:none;
    opacity:.55; transition:transform .4s ease;}
/* Turned 90deg, an arrow's VISUAL width is the image's height — direction.png is 2:1, so half of
   --dir-w — but its layout box stays the full unrotated width. The negative margin cancels that
   difference (--dir-w/4); --dir-gap is then the clear space left beyond the anchor box. */
.ring-dir-right{left:100%;  margin-left:calc(var(--dir-w) / -4 + var(--dir-gap));}
.ring-dir-left{right:100%; margin-right:calc(var(--dir-w) / -4 + var(--dir-gap));}
/* Only on your own turn, and only while nothing is flipped, do the cards invite a click. */
.ring-stage.is-pickable .ring-card:not(:disabled){cursor:pointer;}
.ring-stage.is-pickable .ring-card:not(:disabled):hover{filter:brightness(1.25) drop-shadow(0 0 6px rgba(255,213,74,.9)); z-index:2;}
.ring-stage.is-pickable .ring-card:not(:disabled):hover .ring-card-back{transform:scale(1.16);}

/* The flipped card, big, in the middle below the table (#game-info). */
.ring-current{display:flex; justify-content:center;}
.ring-current-img{height:min(24vh,208px); width:auto; display:block; border-radius:10px;
    box-shadow:0 10px 24px rgba(0,0,0,.5); animation:ringFlipIn .3s ease both;}
@keyframes ringFlipIn{from{opacity:0; transform:rotateY(90deg) scale(.8);} to{opacity:1; transform:none;}}

/* Jack: the rule everybody has to follow, typed before Done unlocks. */
/* An 8 picks its drink buddy by clicking an avatar rather than typing — the selected seat swaps
   the gold "selectable" glow for a red "chosen" one. Only ever one seat at a time (ring.js). */
.seats-pick-buddy .seat.ring-buddy-picked .seat-inner{border-color:#ff4a3f;
    animation:ringBuddyPick 1.15s ease-in-out infinite;}
@keyframes ringBuddyPick{
    0%,100%{box-shadow:0 0 0 3px rgba(255,74,63,.9), 0 0 16px rgba(224,54,54,.55);}
    50%{box-shadow:0 0 0 4px rgba(255,74,63,1), 0 0 26px rgba(224,54,54,.85);}}
/* Done stays visibly dead until a buddy/rule is in — grey, not just a dimmed green. */
.gbtn.ring-done:disabled{background:linear-gradient(180deg,#6e6e6e,#585858); color:rgba(255,255,255,.7);}
.ring-rule{position:relative; width:min(90vw,360px);}
.ring-rule textarea{width:100%; min-height:54px; resize:none; box-sizing:border-box;
    padding:10px 44px 10px 12px; border-radius:10px; border:1px solid var(--panel-border);
    background:rgba(0,0,0,.3); color:var(--text); font:inherit;}
.ring-rule-count{position:absolute; right:10px; bottom:8px; font-size:11px; color:var(--muted); pointer-events:none;}

/* Every card drawn so far, bottom-left (Drunk Pirate's Active-tray spot). */
/* Drawn cards, split by rank into the two bottom corners — the same 7vw/92px spots Drunk Pirate
   parks its Active (left) and Permanent (right) trays in. */
.ring-panel{position:fixed; bottom:92px; z-index:36; width:min(46vw,316px);
    background:var(--panel); border:1px solid var(--panel-border); border-radius:14px; padding:12px;
    box-shadow:var(--shadow); -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);}
.ring-panel-low{left:7vw;}     /* 2-7 */
.ring-panel-high{right:7vw;}   /* 8-Ace */
.ring-panel[hidden]{display:none;}
/* Drink-buddy pairings, left-centre — the spot Drunk Pirate keeps its "ran out" tray in. Unlike
   that tray these persist: a pairing an 8 created lasts the rest of the game.
   --lift is set from JS (see liftTray in ring.js) to push this clear of the drawn-cards tray
   below as that one grows a row per rank. */
aside.ring-panel-buddies{left:7vw; top:50%; bottom:auto;
    transform:translateY(calc(-50% - var(--lift, 0px))); transition:transform .2s ease;
    width:min(34vw,232px);}
.rb-rows{display:flex; flex-direction:column; gap:4px;}
.rb-row{display:flex; align-items:center; gap:6px; justify-content:center;
    font-size:12px; font-weight:700; line-height:1.3;}
.rb-owner{color:var(--text);}
.rb-arrow{color:var(--muted); flex:0 0 auto;}
.rb-partner{color:#ffd54a;}
.rb-owner, .rb-partner{overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0;}
/* Rules a Jack wrote, right-centre — the spot Drunk Pirate keeps its Use tray in. These WRAP
   rather than ellipsis like the buddy rows: a rule runs up to 64 characters and is only useful
   if it can be read in full. */
aside.ring-panel-rules{right:7vw; top:50%; bottom:auto;
    transform:translateY(calc(-50% - var(--lift, 0px))); transition:transform .2s ease;
    width:min(34vw,232px);}
.rr-rows{display:flex; flex-direction:column; gap:6px;}
.rr-row{font-size:12px; line-height:1.35; text-align:center;}
.rr-owner{color:var(--muted); font-weight:800; margin-right:4px;}
.rr-text{color:#ffd54a; font-weight:700; overflow-wrap:anywhere;}
.rp-title{font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted);
    font-weight:800; margin-bottom:8px; text-align:center;}
/* One RANK per row, ascending downwards — so a row holds at most that rank's 4 suits and the
   tray reads as a rank ladder. Rows never wrap; the tray simply grows taller. */
.rp-cards{display:flex; flex-direction:column; gap:4px;}
.rp-row{display:flex; gap:6px; justify-content:center;}
.ring-mini{position:relative; width:46px; cursor:help; transition:transform .12s ease;}
.ring-mini:hover{transform:scale(1.12); z-index:40;}
.ring-mini-img{width:100%; display:block; border-radius:4px; box-shadow:0 2px 4px rgba(0,0,0,.45);}
/* Hover pop-up: the card big, plus what it did. Anchored above the mini it belongs to. */
.ring-pop{position:absolute; bottom:calc(100% + 14px); left:50%; transform:translateX(-50%) scale(.94);
    width:230px; background:rgba(20,14,10,.95); border:1px solid var(--panel-border); border-radius:12px;
    padding:12px; box-shadow:var(--shadow); opacity:0; visibility:hidden;
    transition:opacity .12s ease, transform .12s ease; z-index:9999; pointer-events:none;
    display:flex; flex-direction:column; align-items:center; gap:7px;}
.ring-mini:hover .ring-pop{opacity:1; visibility:visible; transform:translateX(-50%) scale(1);}
.ring-pop-img{height:120px; width:auto; display:block; border-radius:6px;}
.ring-pop-text{font-size:12px; font-weight:700; line-height:1.25; text-align:center; color:var(--text);}
.ring-pop-rule{font-size:12px; font-style:italic; color:var(--text); text-align:center;}
.ring-pop-owner{font-size:12px; font-weight:700; color:var(--muted);}

@media (max-width:640px){
    .ring-panel{bottom:80px; width:34vw; padding:7px;}
    .ring-mini{width:34px;}
}


/* ================================ Hanabi ================================ */
/* --hb-ink is the suit's swatch colour, used by every outline, clue band and chip.
   --hb-burst is the colour its firework is painted in on the card face (see .hb-tint) — the same
   hue, a touch brighter, since a colour tuned for a solid chip goes muddy as line art on navy.
   The fifth suit is SILVER (its key is still 'w'): it reads on the dark felt without the light
   rim the old black swatch needed, and a silver firework is what the artwork already looks like. */
.hbc-r{--hb-ink:#e2231a; --hb-burst:#ff4b3e;} .hbc-g{--hb-ink:#3aa856; --hb-burst:#48d472;}
.hbc-y{--hb-ink:#f5c400; --hb-burst:#ffd633;} .hbc-b{--hb-ink:#1b7ced; --hb-burst:#4aa3ff;}
.hbc-w{--hb-ink:#c3cad6; --hb-burst:#eef2f8;}
/* The sixth firework, only on the table with the host's "Use 6 colours" on. Dark purple: it has to
   sit clear of both the blue and the silver at swatch size, which is why the ink is this deep. */
.hbc-p{--hb-ink:#7b3fb5; --hb-burst:#a95cf0;}

/* ---- the table itself ---- */
/* Hanabi needs hands laid out on all four sides of the felt, so this is the one game whose table
   is centred on the SCREEN rather than on the content column. --hb-table is shared with the side
   hands below, which anchor off the felt's edges. */
/* --hb-cs scales every hand card at once. Each hand declares the size it wants as --hb-base and
   this multiplies it, so the eight breakpoints below only ever set the base and the card size is
   tuned from this one number. Inherited by <body>, which is what lets the drag ghost resolve it. */
body[data-game="hanabi"]{--hb-table:min(96vw, 930px, 63vh); --hb-cs:1.25;}
body[data-game="hanabi"] .table-wrap{width:var(--hb-table);}
/* The seat avatars are a % of the felt, so they already track it — this only re-sizes the clamp
   the other games set. Hanabi's is 30% smaller than theirs: this is the one table with a hand of
   cards laid along all four edges, so the avatars have to give the cards the room. Every term is
   scaled, not just the cap, so they stay 30% down at any window size. */
body[data-game="hanabi"] .seat{width:clamp(63px, 9.1%, 105px);}
/* Zeroing the padding drops the offset that normally keeps the content clear of the (fixed) top
   bar, which is exactly what's wanted here: the felt then centres on the true middle of the
   window. Nothing is left in the flow to pull it off-centre — see .game-panel below. */
body[data-game="hanabi"] .game-main{justify-content:center; padding:0;}
/* The local hand and the action buttons come OUT of the flow and pin to the bottom. In the flow
   they'd be centred together with the table, which pushes the felt well above the middle of the
   window — the one thing this layout can't have. */
/* bottom: how far the local hand sits above the status bar. That bar is ~54px tall (18px text plus
   its padding) starting 18px off the window's floor, so anything under ~76px here would run into
   it. 80px is deliberately close to that limit — the hand hangs as low as it can. */
body[data-game="hanabi"] .game-panel{position:fixed; left:50%; bottom:80px; z-index:33;
    transform:translateX(-50%); margin-top:0; padding-bottom:0;}

/* ---- the felt: the draw pile + five fireworks ---- */
/* Inside #game-layer, so everything is a % of the table and tracks it through scroll and zoom. */
/* z-index puts the board (and so the pick buttons hanging off it) over .hb-drop, which is a LATER
   sibling in #game-layer and would otherwise paint on top of them while a card is being dragged. */
/* --hb-sw / --hb-sgap size the boxes on the felt (draw pile + one per firework) and are shared
   with the misfire bar below them, so that bar lines up with the row exactly instead of being
   eyeballed — change a box's width here and the bar follows.
   --hb-nb is how many boxes there are: 6 normally, 7 with the host's sixth colour. hanabi.js sets
   it on this element from the colour list it just drew, so the bar can never be a box out of step
   with the row above it. The value here is only the pre-game default. */
.hb-board{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); z-index:7;
    --hb-sw:9%; --hb-sgap:1.6%; --hb-nb:6;
    display:flex; flex-direction:column; align-items:center; width:100%;}
.hb-stacks{display:flex; justify-content:center; align-items:flex-end; gap:var(--hb-sgap); width:100%;}
/* Empty, a stack is just the outline its colour is waiting in; once something has been played it
   holds that card, so the whole board reads as "what can I play next". */
/* No --hb-rim here, unlike the swatches and clue bands: it was there so the old black fifth suit
   could be seen at all, and on a box this size it read as a second, solid outline sitting inside
   the dashed one. The suit is silver now and needs no help. */
.hb-stack{position:relative; width:var(--hb-sw); aspect-ratio:2/3; border-radius:8px; box-sizing:border-box;
    border:2px dashed var(--hb-ink); background:rgba(0,0,0,.22);}
.hb-stack.is-done{border-style:solid; box-shadow:0 0 10px 2px var(--hb-ink);}
/* The played card fills its outline exactly: sized off the inset rather than the 2/3 ratio the
   loose hand cards use, so it can never sit a pixel proud of the box it's snapped into. */
.hb-stack .hb-face{position:absolute; inset:0; width:auto; height:auto; aspect-ratio:auto; border-radius:6px;}
/* The draw pile leads the row as a sixth box: the card back, with how much game is left counted
   over it — turned on its side, the way the edge of a real deck reads. The count is positioned so
   it paints above the (absolutely positioned) face rather than under it. */
.hb-drawpile{--hb-ink:#b9a887; border-style:solid; background:rgba(0,0,0,.42);
    display:flex; align-items:center; justify-content:center;}
.hb-draw-n{position:relative; z-index:1;
    transform:rotate(-90deg); font-weight:900; line-height:1; color:var(--text);
    font-size:clamp(13px, 2.6vh, 30px); letter-spacing:.02em;
    text-shadow:0 2px 6px rgba(0,0,0,.95), 0 0 12px rgba(0,0,0,.8);}
/* Run dry, the pile keeps its back but fades right back, so the row still reads as "nothing left
   to draw" at a glance — the red count and rim do the rest. */
.hb-drawpile.is-empty{--hb-ink:#8a4a44;}
.hb-drawpile.is-empty .hb-face{opacity:.3;}
.hb-drawpile.is-empty .hb-draw-n{color:#ff7a6e;}

/* ---- the misfire bar (fuses) ---- */
/* Sits directly under the row of stacks, running from the left edge of the draw pile to the right
   edge of the LAST firework — every box plus the gaps between them, NOT the full board width (the
   row is centred and only fills its middle). --hb-nb is the box count, so this follows the row
   into a six-colour game on its own. Untouched it is only an outline over a faint dark fill with
   "0/3" in the middle — present enough to be noticed and asked about, quiet enough not to nag
   while the table is doing fine. */
.hb-fuses{position:relative; height:clamp(16px, 2.2vh, 26px); margin-top:2.2%;
    width:calc(var(--hb-nb) * var(--hb-sw) + (var(--hb-nb) - 1) * var(--hb-sgap));
    border:1px solid rgba(255,255,255,.28); border-radius:999px;
    background:rgba(0,0,0,.34); overflow:hidden; box-sizing:border-box;}
/* The fill grows from the draw pile's end, a third per wrongly played card. Colour is set by the
   step class rather than by the width, so 1/3 is unmistakably yellow rather than "a bit of red". */
.hb-fuse-fill{position:absolute; left:0; top:0; bottom:0; width:0;
    background:var(--hb-fuse, transparent);
    transition:width .35s ease, background .35s ease;}
.hb-fuse-1{--hb-fuse:#e8c33a;}                    /* one gone: yellow */
.hb-fuse-2{--hb-fuse:#ef9134;}                    /* two gone: orange */
/* Three: red, and the round is already over server-side — so it also lights its own rim. */
.hb-fuse-3{--hb-fuse:#e0483c; border-color:rgba(224,72,60,.85); box-shadow:0 0 14px rgba(224,72,60,.5);}
.hb-fuse-n{position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    font-size:clamp(10px, 1.35vh, 14px); font-weight:900; letter-spacing:.06em; line-height:1;
    color:#fff; text-shadow:0 1px 3px rgba(0,0,0,.9);}
/* Nothing burnt yet: the count is grey like the outline, not white like a warning. */
.hb-fuse-0 .hb-fuse-n{color:var(--muted); text-shadow:none;}

/* ---- where a dragged card is dropped to be played ---- */
/* Most of the felt, not just the six boxes: you are aiming at "the table", and since you cannot
   see your own card you could not aim at one firework anyway — the server sorts it. Invisible
   until a card is actually in the air, and pointer-events:none throughout (hanabi.js hit-tests
   the rect, so this never has to intercept anything). */
.hb-drop{position:absolute; inset:6% 4%; border-radius:20px; pointer-events:none; opacity:0;
    border:2px dashed rgba(255,255,255,.45); background:rgba(255,255,255,.04);
    display:flex; align-items:flex-end; justify-content:center; padding-bottom:10px;
    transition:opacity .15s ease, background .15s ease, border-color .15s ease, box-shadow .15s ease;}
body.hb-dragging .hb-drop{opacity:1;}
body.hb-dragging .hb-drop.is-over{border-color:#48c667; background:rgba(72,198,103,.16);
    box-shadow:inset 0 0 30px rgba(72,198,103,.35);}
.hb-drop-label{font-size:13px; font-weight:800; letter-spacing:.06em; text-transform:uppercase;
    color:rgba(255,255,255,.8); text-shadow:0 1px 3px rgba(0,0,0,.8); pointer-events:none;}

/* ---- hands ---- */
/* Four fixed places, picked by seat count in hanabi.js; the viewer is ALWAYS the bottom one.
   The three opponents' hands hang off the felt's own edges (they live inside #game-layer) but are
   sized in vh rather than in % of the table, so they track it without shrinking with it. The gaps
   below clear the seat avatars, which straddle the felt's rim.
   On a narrow screen the side hands reach into the player list's column; that list collapses on
   demand for exactly this reason (the same trade-off Drunk Pirate's side trays make). */
.hb-hand{--hb-base:10vh; --hb-h:calc(var(--hb-base) * var(--hb-cs, 1));
    position:relative;   /* for the backdrop below; the three placed hands override it just after */
    display:flex; flex-direction:column; align-items:center; gap:5px; z-index:6;}
.hb-hand[hidden]{display:none;}
/* A soft black panel behind each hand, name included, so five cards over a busy felt still read as
   one player's hand. --hb-bd-pad is how far it spills past the content on each side: it wants to be
   a little bigger than it needs to be, or it reads as a border rather than a backdrop.
   Drawn as a pseudo-element rather than a background on .hb-hand itself so it can overflow the
   layout box without padding disturbing where the four hands sit. z-index is 0 against the two
   children's 1 rather than -1, which would drop it behind an ancestor's background for the bottom
   hand (the one hand that isn't positioned in its own right). */
.hb-hand::before{content:""; position:absolute; z-index:0;
    --hb-bd-pad:11px;
    inset:calc(var(--hb-bd-pad) * -1);
    background:rgba(0,0,0,.25); border-radius:14px; pointer-events:none;}
.hb-hand > *{position:relative; z-index:1;}
.hb-hand.hb-pos-left, .hb-hand.hb-pos-top, .hb-hand.hb-pos-right{position:absolute; pointer-events:auto;}
/* --hb-side-gap is measured from the felt's bounding box, but what the side hands actually have to
   clear is the seat avatar, which straddles the rim and so hangs OUTSIDE that box by roughly half
   its own width. That overhang is what eats the gap, which is why this is much larger than it
   looks like it needs to be. One number, shrunk at the breakpoints below. */
.hb-hand.hb-pos-left, .hb-hand.hb-pos-right{--hb-side-gap:92px;}
.hb-hand.hb-pos-left{right:100%; top:50%; margin-right:var(--hb-side-gap); transform:translateY(-50%);}
.hb-hand.hb-pos-right{left:100%; top:50%; margin-left:var(--hb-side-gap); transform:translateY(-50%);}
/* The hand across the table hangs under the LOBBY CODE instead of off the felt: with the table
   centred on the screen the whole upper half is empty, and up there the top player's cards cannot
   crowd their own avatar on the felt's rim. Fixed rather than absolute, so it is anchored to the
   window and not to the table — nothing between it and the viewport has a transform, so it
   escapes #game-layer cleanly (while still inheriting that layer's hidden-in-lobby behaviour).
   z-index stays under the lobby tag's 35, so the tag wins if they ever meet. */
.hb-hand.hb-pos-top{position:fixed; left:50%; top:78px; transform:translateX(-50%); z-index:33;}
/* The bottom hand is the local player's, so it gets the biggest cards. */
.hb-hand.hb-pos-bottom{--hb-base:11.5vh;}
.hb-hand-name{display:flex; align-items:center; justify-content:center; gap:6px; max-width:100%;
    font-size:12px; font-weight:800; letter-spacing:.04em; color:var(--muted);}
.hb-hand-who{overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.hb-hand.is-turn .hb-hand-name{color:#ffd54a;}
/* Which way this hand runs, 1st card to 5th. Drawn with borders rather than an arrow glyph so it
   is identical on every machine, and rotated per place: pointing right for your own hand, then
   turned with the seat, so it always lies along the row (or column) of cards beneath it. */
.hb-arrow{flex:0 0 auto; width:0; height:0; border-style:solid;
    border-width:5px 0 5px 8px; border-color:transparent transparent transparent #48c667;
    filter:drop-shadow(0 0 3px rgba(72,198,103,.5));}
.hb-pos-left  .hb-arrow{transform:rotate(90deg);}    /* down  */
.hb-pos-top   .hb-arrow{transform:rotate(180deg);}   /* left  */
.hb-pos-right .hb-arrow{transform:rotate(-90deg);}   /* up    */
.hb-cards{display:flex; gap:6px; justify-content:center;}

/* --hb-rot / --hb-lift feed one transform, so a card can be rotated for its seat AND lifted on
   hover without the two fighting over the property. The lift is written after the rotation, so it
   runs along the CARD's own axis — which means "up" is always "towards the middle of the table",
   whichever side of it the hand is sitting on. */
.hb-card{--hb-rot:0deg; --hb-lift:0px;
    position:relative; display:flex; flex-direction:column; align-items:stretch;
    width:calc(var(--hb-h) * 2 / 3); padding:0; border:none; background:none;
    cursor:default; transform:rotate(var(--hb-rot)) translateY(var(--hb-lift));
    transition:transform .12s ease;}
.hb-card:not(:disabled){cursor:pointer; touch-action:none;}   /* touch-action: so a drag is not a scroll */
.hb-card:not(:disabled):hover{--hb-lift:-6px;}
.hb-card.is-selected{--hb-lift:-10px;}
.hb-card.is-selected .hb-face{box-shadow:0 0 0 3px #ffd54a, 0 8px 18px rgba(0,0,0,.55);}
/* Previewing a clue: every card the clue would touch lights up, so you can see its spread before
   you spend a token on it. */
.hb-card.is-hinted .hb-face{box-shadow:0 0 0 3px #fff, 0 0 16px rgba(255,255,255,.6);}
/* The hand across the table is upside down from here, so its clue bands hang UNDER the cards and
   its lift goes downwards — both still "towards the middle of the table". */
.hb-pos-top .hb-card{flex-direction:column-reverse;}
.hb-pos-top .hb-card:not(:disabled):hover{--hb-lift:6px;}
.hb-pos-top .hb-card.is-selected{--hb-lift:10px;}

/* The two side hands are turned a quarter turn (clockwise on the left, anti-clockwise on the
   right) and stacked into a column, so they read as hands held by someone sitting there.
   The card's LAYOUT box stays portrait while its VISUAL box is landscape, so the column would
   reserve the wrong space in both axes; the margins below swap the two over. With the card's
   w:h being (2/3)h : 1.1h, that swap is +/- ((1.1 - 0.6667) / 2) * h = 0.21667h. */
/* Every hand is laid out as ITS OWNER would hold it: 1st card on their left, 5th on their right,
   turned with their seat. So the top hand reads right-to-left from here and the right-hand one
   bottom-to-top, both of which are just "left to right" seen from that chair. Without this the
   same card would be "the 3rd one" to one player and "the 3rd from the other end" to another,
   which is exactly the confusion a clue cannot afford. */
.hb-pos-left .hb-cards, .hb-pos-right .hb-cards{align-items:center;}
.hb-pos-left .hb-cards{flex-direction:column;}
.hb-pos-right .hb-cards{flex-direction:column-reverse;}
.hb-pos-top .hb-cards{flex-direction:row-reverse;}
.hb-pos-left .hb-card, .hb-pos-right .hb-card{
    margin:calc(var(--hb-h) * -0.21667) calc(var(--hb-h) * 0.21667);}
.hb-pos-left .hb-card{--hb-rot:90deg;}
.hb-pos-right .hb-card{--hb-rot:-90deg;}

/* The clue band: the card grows 10% on the side facing the table. Coloured once a colour clue has
   landed, grey once only a value has, and carrying the number when a value is known. The space is
   held even before a clue arrives so the cards in a hand stay in line as clues come in. */
/* It also runs half its height again UNDER the card. The card's face is rounded, so a band that
   stopped at its edge left two crumbs of felt showing in the top corners; the tuck fills them.
   Three parts, all off --hb-clue-tuck so one number moves them together:
     height        - the band is 1.5x as tall as it shows,
     margin-bottom - negative by the extra, so the card's layout height is exactly as before,
     padding-bottom- so align-items:center still centres the digit in the VISIBLE part.
   The tucked half is hidden because .hb-face is positioned and this band is not, so the face
   paints over it. */
.hb-clue{--hb-clue-h:calc(var(--hb-h) * .1); --hb-clue-tuck:calc(var(--hb-clue-h) * .5);
    height:calc(var(--hb-clue-h) + var(--hb-clue-tuck));
    margin-bottom:calc(var(--hb-clue-tuck) * -1);
    padding-bottom:var(--hb-clue-tuck);
    border-radius:4px 4px 0 0; visibility:hidden;
    background:var(--hb-ink, #7d7d7d); box-sizing:border-box;
    border:1px solid var(--hb-rim, transparent); border-bottom:none;
    display:flex; align-items:center; justify-content:center;
    font-size:calc(var(--hb-h) * .09); font-weight:900; line-height:1; color:#fff;
    text-shadow:0 1px 2px rgba(0,0,0,.85);}
.hb-clue.has-clue{visibility:visible;}
.hb-clue.hb-clue-grey{--hb-ink:#7d7d7d;}
/* Yellow and silver are both too light to read white digits on. */
.hb-clue.hbc-y{color:#2a2200; text-shadow:none;}
.hb-clue.hbc-w{color:#20242c; text-shadow:none;}
/* The hand across the table wears its band UNDER the card (the side facing the felt), so every
   part of the tuck flips with it — it has to reach upward, behind the card above it. */
.hb-pos-top .hb-clue{border-radius:0 0 4px 4px; border-bottom:1px solid var(--hb-rim, transparent); border-top:none;
    margin-bottom:0; margin-top:calc(var(--hb-clue-tuck) * -1);
    padding-bottom:0; padding-top:var(--hb-clue-tuck);}

/* container-type gives the number below something to size against: the face has no font-size of
   its own to scale from, and it appears at three very different sizes (hand card, played card on
   the felt, drag ghost). Its own size is set by width + aspect-ratio, so size containment costs
   nothing here. */
.hb-face{position:relative; width:100%; aspect-ratio:2/3; border-radius:9px; overflow:hidden;
    container-type:size;
    box-shadow:0 5px 13px rgba(0,0,0,.5); transition:box-shadow .12s ease;}
.hb-face > div{position:absolute; inset:0; background-position:center; background-repeat:no-repeat;
    background-size:cover;}

/* The card art: white fireworks on navy, one photo per suit (see ART in hanabi.js). */
.hb-art{background-color:#0d1c3a;}   /* the photo's own navy, in case it is still loading */

/* The suit colour, painted onto the bursts and nowhere else. The trick is the MASK: the same photo
   is used as a luminance mask, so this flat colour is drawn at the brightness of the pixel beneath
   it — full over a white burst, nothing over the navy, and part-way over the faint trails, which
   is what keeps the glow. A plain mix-blend-mode:multiply tint would also colour the white, but it
   would crush the navy background to near-black at the same time.
   mask-mode is the load-bearing line: without it an opaque JPEG masks nothing away. */
.hb-tint{background:var(--hb-burst, #fff);
    -webkit-mask-position:center; -webkit-mask-repeat:no-repeat; -webkit-mask-size:cover;
            mask-position:center;         mask-repeat:no-repeat;         mask-size:cover;
    -webkit-mask-source-type:luminance;   /* older WebKit's spelling of the line below */
            mask-mode:luminance;}
/* No luminance masking (very old WebKit): fall back to multiplying the colour over the whole card.
   The bursts still take the suit colour; the navy just goes darker than it should. */
@supports not ((mask-mode: luminance) or (-webkit-mask-source-type: luminance)) {
    .hb-tint{-webkit-mask-image:none !important; mask-image:none !important;
        mix-blend-mode:multiply;}
}

/* The back, for the draw pile and for your own hand — the one hand you may not look at. Its
   bursts run through all five suits at once, which is what marks it as "not a card yet". */
.hb-face.is-back .hb-tint{
    background:linear-gradient(135deg, #ff4b3e 0%, #ffd633 27%, #48d472 50%, #4aa3ff 73%, #e8edf5 100%);}

/* The value, straight over the art. Centred rather than cornered: at hand size a corner index
   would be a handful of pixels, and the middle of a firework is the one part of the photo that is
   reliably bright, so a white number with a hard shadow reads against every suit. */
.hb-num{position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    font-size:52cqw; font-weight:900; line-height:1; color:#fff; letter-spacing:-.02em;
    text-shadow:0 2px 6px rgba(0,0,0,.9), 0 0 14px rgba(0,0,0,.75);
    pointer-events:none; user-select:none;}

/* ---- dragging ---- */
/* The card that follows the pointer. Lives on <body> (hanabi.js sets left/top in px), so it needs
   --hb-h carried across from the hand it came out of — which hanabi.js does. Declared after
   .hb-card so its transform wins over the rotate/lift one. */
.hb-ghost{position:fixed; z-index:60; margin:0; pointer-events:none;
    transform:translate(-50%,-50%) rotate(-4deg) scale(1.06); transition:none;
    filter:drop-shadow(0 14px 24px rgba(0,0,0,.65));}
.hb-card.is-dragging{opacity:.22;}

/* ---- the two bottom corner panels ---- */
/* Same 7vw/92px corners Drunk Pirate and Ring of Fire park their trays in, and their headings
   reuse Ring's .rp-title so every tray on the site is captioned identically. */
.hb-panel{position:fixed; bottom:92px; z-index:36;
    background:var(--panel); border:1px solid var(--panel-border); border-radius:14px; padding:12px;
    box-shadow:var(--shadow); -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);
    transition:outline-color .15s ease, background .15s ease;}
.hb-panel[hidden]{display:none;}
.hb-discards{left:7vw;}
.hb-clues{right:7vw;}
/* The discard tray doubles as the bin you drag a card into, so it lights up alongside the table. */
body.hb-dragging .hb-discards{outline:2px dashed rgba(255,255,255,.45); outline-offset:4px;}
body.hb-dragging .hb-discards.is-over{outline-color:#e05a5a; background:rgba(224,90,90,.18);}
.hb-bin-label{display:none;}
body.hb-dragging .hb-bin-label{display:block; margin-top:8px; text-align:center;}

/* Discard grid: a row per colour holding one slot per PHYSICAL card in the deck — three 1s, two
   each of 2/3/4 and a single 5, divided by value. A slot fills in with its own number as that copy
   is lost, so the row reads as "what is still out there"; the last copy of a value going is what
   kills that colour's firework, and is called out in red. */
.hb-dgrid{display:flex; flex-direction:column; gap:3px;}
.hb-drow{display:flex; align-items:center; gap:2px;}
.hb-dswatch{width:10px; height:16px; flex:0 0 auto; border-radius:3px; margin-right:4px;
    background:var(--hb-ink); box-shadow:inset 0 0 0 1px var(--hb-rim, transparent);}
.hb-dsep{width:1px; height:14px; flex:0 0 auto; margin:0 2px; background:var(--panel-border);}
.hb-dcell{width:17px; height:16px; line-height:16px; flex:0 0 auto; border-radius:3px;
    text-align:center; font-size:11px; font-weight:800; color:#fff;
    text-shadow:0 1px 2px rgba(0,0,0,.8); background:rgba(255,255,255,.06);}
.hb-dcell.is-gone{background:var(--hb-ink); box-shadow:inset 0 0 0 1px var(--hb-rim, transparent);}
.hbc-y .hb-dcell.is-gone{color:#2a2200; text-shadow:none;}
.hbc-w .hb-dcell.is-gone{color:#20242c; text-shadow:none;}
/* An Extravagance is asking for a card out of here: every dead slot becomes a button. The whole
   tray pulses so the player looks DOWN HERE — this is the one moment the game asks for something
   from the bottom-left corner rather than from a hand. */
.hb-discards.is-picking{outline:2px solid rgba(255,213,74,.75); outline-offset:4px;
    animation:hb-bin-call 1.4s ease-in-out infinite;}
@keyframes hb-bin-call{
    0%,100%{box-shadow:var(--shadow);}
    50%{box-shadow:var(--shadow), 0 0 18px rgba(255,213,74,.55);}}
/* The "Revive Cards" option OFFERING a card rather than an extravagance demanding one: the same
   clickable slots, but a quiet outline and no pulse — it is a possibility, not an obligation. */
.hb-discards.is-buying{outline:2px dashed rgba(255,213,74,.45); outline-offset:4px;}
/* "Pass" on the 'revive' reward, above the tray it belongs to. Full width so it reads as the tray's
   own control rather than as a stray button parked next to it. */
.hb-bin-pass{display:block; width:100%; margin-bottom:8px; padding:6px 10px; font-size:13px;}
.hb-bin-pass[hidden]{display:none;}
.hb-dcell.is-pickable{cursor:pointer; transition:transform .1s ease, box-shadow .1s ease;}
.hb-dcell.is-pickable:hover{transform:scale(1.35); box-shadow:0 0 0 2px #ffd54a, 0 3px 8px rgba(0,0,0,.6);
    position:relative; z-index:1;}
/* The copy that took the last one of its value with it. */
.hb-dcell.is-last{outline:1.5px solid #ff5a4e; outline-offset:1px;}

/* Clue tokens: two rows of four. clue.png is drawn as a MASK rather than an <img> so the same
   file can be painted green (available) or red (spent) without a second asset. */
.hb-cluegrid{display:grid; grid-template-columns:repeat(4, 1fr); gap:6px;}
.hb-token{width:22px; height:22px; background:#48c667;
    -webkit-mask-size:contain; mask-size:contain;
    -webkit-mask-position:center; mask-position:center;
    -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
    filter:drop-shadow(0 0 4px rgba(72,198,103,.55)); transition:background .2s ease, filter .2s ease;}
.hb-token.is-spent{background:#d64545; filter:none; opacity:.75;}

/* ---- small grey notes (under the action buttons) ---- */
.hb-log{font-size:12px; font-weight:700; line-height:1.2; color:var(--muted); text-align:center;}

/* ---- the move that just happened, on the felt above the fireworks ---- */
/* "Ann threw away a green 2" is the running commentary the whole table plays off, so it goes where
   everyone is already looking — over the middle of the table rather than down by one player's hand.
   Absolute, hanging off the top of .hb-board, so the row of stacks stays centred on the felt no
   matter whether there is a line to show or how many lines it wraps to. pointer-events stays at the
   layer's inherited none: this sits inside the play drop zone and must never intercept a card. */
.hb-lastmove{position:absolute; bottom:calc(100% + 14px); left:50%; transform:translateX(-50%);
    width:96%; text-align:center;
    font-size:17px; font-weight:700; line-height:1.3; letter-spacing:.01em; color:#fff;
    text-shadow:0 2px 6px rgba(0,0,0,.9), 0 0 14px rgba(0,0,0,.7);}
@media (max-width:900px){ .hb-lastmove{font-size:15px;} }
@media (max-width:640px){ .hb-lastmove{font-size:13px;} }
/* The Extravagance a completed colour just earned, on its own line under the commentary and gold
   so it reads as a reward rather than as part of the move. Shown for exactly one turn — the server
   clears it with the next move — which is what makes the "extra clue token" roll visible at all,
   since nothing else on the table changes for it. */
.hb-xnote{margin-top:4px; font-size:.82em; font-weight:800; color:#ffd54a;
    text-shadow:0 2px 6px rgba(0,0,0,.9);}

/* ---- what to do with the card you picked up ---- */
/* Shares .hb-lastmove's slot: the commentary steps aside while these are up (hanabi.js decides
   which of the two gets the space), so the decision sits right above the fireworks it's about
   instead of below the table. Anchored by its BOTTOM edge, so a row that wraps to two lines grows
   upwards into the empty half of the felt and never pushes the stacks off centre.
   pointer-events has to be re-enabled: .game-layer turns it off for everything it holds. */
.hb-pick{position:absolute; bottom:calc(100% + 12px); left:50%; transform:translateX(-50%);
    display:flex; flex-wrap:wrap; gap:9px; justify-content:center; align-items:flex-end;
    width:max-content; max-width:min(92vw, 720px); pointer-events:auto; z-index:9;}
.hb-pick[hidden]{display:none;}
/* Quieter than a normal game button: this row sits over the felt and every clue button carries a
   whole sentence, so it is set unbolded, a size down, and padded tight — the label is what should
   be read here, not the button around it. */
.hb-pick .gbtn{padding:4.5px 7.5px; font-size:13px; font-weight:500;}
@media (max-width:640px){
    .hb-pick{gap:6px;}
    .hb-pick .gbtn{padding:4px 5.5px; font-size:12px;}
}

/* ---- clue buttons ---- */
/* Each one is labelled with the exact sentence the target will read, with a swatch of the colour
   (or the number) it names in front of it. */
.hb-cluebtn{display:inline-flex; align-items:center; gap:4.5px; background:linear-gradient(180deg,#6b7280,#565d68);}
.hb-cluebtn-chip{width:20px; height:20px; flex:0 0 auto; border-radius:5px;
    background:var(--hb-ink, #7d7d7d); box-shadow:inset 0 0 0 1px var(--hb-rim, rgba(0,0,0,.35));
    display:flex; align-items:center; justify-content:center;
    font-size:13px; font-weight:900; color:#fff; text-shadow:0 1px 2px rgba(0,0,0,.8);}
.hb-cluebtn.hbc-y .hb-cluebtn-chip{color:#2a2200; text-shadow:none;}
.hb-cluebtn.hbc-w .hb-cluebtn-chip{color:#20242c; text-shadow:none;}

/* The side hands are the first thing to run out of room as the window narrows — the table keeps
   its share of the height and the columns beside it do not. Shrink those two before anything else. */
@media (max-width:1200px){
    .hb-hand.hb-pos-left, .hb-hand.hb-pos-right{--hb-base:8vh; --hb-side-gap:64px;}
}
@media (max-width:900px){
    .hb-hand.hb-pos-left, .hb-hand.hb-pos-right{--hb-base:6.5vh; --hb-side-gap:46px;}
    .hb-hand.hb-pos-top{--hb-base:8vh;}
    .hb-hand.hb-pos-bottom{--hb-base:10vh;}
}
@media (max-width:640px){
    .hb-panel{bottom:80px; padding:8px;}
    .hb-discards{left:2vw;}
    .hb-clues{right:2vw;}
    .hb-token{width:16px; height:16px;}
    .hb-dcell{width:14px;}
    .hb-hand{--hb-base:6vh;}
    .hb-hand.hb-pos-bottom{--hb-base:9vh;}
    .hb-cards{gap:4px;}
}
/* Everything on this screen is sized in vh except the status bar and the gaps around the buttons,
   so a short window squeezes the hands towards the seat avatars rather than the other way round.
   Give it back by shrinking the hands first... */
@media (max-height:820px){
    .hb-hand{--hb-base:8.5vh;}
    .hb-hand.hb-pos-bottom{--hb-base:10vh;}
}
@media (max-height:700px){
    .hb-hand{--hb-base:7vh;}
    .hb-hand.hb-pos-bottom{--hb-base:8.5vh;}
    body[data-game="hanabi"]{--hb-table:min(96vw, 930px, 54vh);}
}
/* ...and below the point where a centred table and clear hands stop being possible together, hand
   the page back to a normal top-to-bottom flow that can scroll. The top hand stays pinned under
   the lobby code either way; the top padding here is what keeps the table out from under it. */
@media (max-height:600px){
    body[data-game="hanabi"] .game-main{justify-content:flex-start; padding:calc(88px + 12vh) 18px 40px;}
    body[data-game="hanabi"] .game-panel{position:static; transform:none;
        margin-top:20px; padding-bottom:80px;}
}

/* ============================== Busfahrer ============================== */
/* Shared SVG deck in /assets/images/shared/cards/ (rank+suit, e.g. AS.svg; 1B.svg is the back),
   the same one Blackjack and Ring of Fire use. Everything on the felt is sized in vh so the whole
   board shrinks with the table on shorter screens.
   The felt holds the pyramid (stage 2, dealt face down from the very start) and, at the end, the
   driver's row of five. Hands, the card showing and the claims played on it live below the table
   in #game-info. */
.bf-stage{position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    pointer-events:none;}

.bf-card{display:block; width:auto; aspect-ratio:5/7; border-radius:.6vh; background:#fff;
    box-shadow:0 .4vh 1vh rgba(0,0,0,.45);}

/* ----- the pyramid on the felt ----- */
/* Rows are drawn apex-first (see renderPyramid), so this is a plain column: 5 rows, the widest
   at the bottom. A flipped card is lifted and brightened; the one currently in play gets the
   gold ring, since every claim below the table is aimed at it.
   --bf-pyr-overlap is how far each row climbs back over the one above it, as a share of a card:
   at .3 the five rows stand 3.8 cards tall instead of 5, which is what keeps the pyramid clear of
   the seat avatars at the top and bottom of the felt. Rows later in the DOM are the wider, lower
   ones and paint over the rows above them (tree order), so it stacks the way a real pyramid of
   cards is laid out — and the overlap eats the BOTTOM of each card, leaving the corner pips that
   name it readable. */
.bf-pyramid{--bf-pyr-card:5.4vh; --bf-pyr-overlap:.3;
    display:flex; flex-direction:column; align-items:center;}
.bf-pyramid[hidden]{display:none;}   /* [hidden] alone loses to the display above */
.bf-row{display:flex; gap:.6vh; justify-content:center;}
.bf-row + .bf-row{margin-top:calc(var(--bf-pyr-card) * var(--bf-pyr-overlap) * -1);}
/* Solid, both face down and face up: a card overlapping the row above it has to read as one card
   on top of another, and any transparency at all makes the one underneath show through the
   overlap. The .is-up class is still set on flipped cards, it just carries no styling of its own —
   the face showing is what tells them apart. */
.bf-pyr-card{height:var(--bf-pyr-card); transition:transform .2s ease;}
.bf-pyr-card.is-current{transform:scale(1.12); box-shadow:0 0 0 .3vh #ffd54a, 0 .5vh 1.4vh rgba(0,0,0,.6);
    position:relative; z-index:2;}

/* ----- the driver's row (stage 3) ----- */
/* Five cards across the middle of the felt. The next one to be turned over pulses, so the driver
   always knows where they are in the row after a restart. */
.bf-bus{display:flex; gap:1.2vh; align-items:center; justify-content:center;}
.bf-bus[hidden]{display:none;}
.bf-bus-card{height:11vh; opacity:.9;}
.bf-bus-card.is-up{opacity:1;}
.bf-bus-card.is-next{animation:bfNext 1.2s ease-in-out infinite;}
@keyframes bfNext{
    0%,100%{box-shadow:0 0 0 .2vh rgba(255,213,74,.35), 0 .4vh 1vh rgba(0,0,0,.45);}
    50%    {box-shadow:0 0 0 .4vh rgba(255,213,74,.95), 0 .4vh 1.4vh rgba(0,0,0,.55);}
}

/* ----- below the table (#game-info): the card showing + what has been played on it ----- */
/* Captions, card, then the cards played on it — left to right, wrapping only if it runs out of
   room. This block sits in the middle column of .bf-hands, so it is also what decides how much
   width is left for the opponents' hands either side. */
.bf-current{display:flex; flex-direction:row; align-items:center; justify-content:center;
    gap:18px; flex-wrap:wrap; margin-bottom:10px;}
.bf-current-card{display:flex; align-items:center; justify-content:center;}
.bf-card.bf-big{height:13vh;}
/* No caption and no "Worth X" pill: the card is the whole of it. The pill also used to sit
   between the card and the flip button, which now lives directly under it. */

.bf-claims{display:flex; flex-direction:column; gap:5px; align-items:flex-start;}
.bf-claims-title{font-size:11px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
    color:rgba(255,255,255,.6);}
/* Runs off to the right of the card, one claim after another, as it always did. Uncapped: it can
   widen the middle column of .bf-hands when a card gets busy, which is accepted — the row is
   centred, so it eats the slack either side rather than moving my hand. */
.bf-claims-row{display:flex; gap:10px; flex-wrap:wrap;}
.bf-claim{display:flex; flex-direction:column; align-items:center; gap:4px;}
.bf-claim-card{height:7vh;}
.bf-claim.is-shown .bf-claim-card{opacity:.9;}
.bf-claim-name{font-size:11px; font-weight:700; color:var(--text); max-width:9ch;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.gbtn.bf-call{padding:2px 10px; font-size:11px; min-width:0;}
/* The verdict on a claim somebody called: red for a bluff, green for an honest card. */
.bf-tag{font-size:10px; font-weight:800; letter-spacing:.03em; border-radius:999px; padding:1px 8px;}
.bf-tag.is-liar{background:#d64545; color:#fff;}
.bf-tag.is-honest{background:var(--accent); color:#06210f;}

/* ----- below the table (#game-info): hands, mine large and everyone else's small ----- */
/* Three groups in a row: opponents' hands, my own, opponents' hands — my hand centred, with a
   wide fixed gap holding the others off it.
   Deliberately dumb, after percentages and grid fractions each failed in their own way. The two
   things that matter are bought outright here:
     - MY HAND IS CENTRED because both side columns are the same fixed width. Nothing about their
       contents can move it; unequal columns are what pulled it off centre every previous time.
     - NOTHING OVERLAPS because these are ordinary flex items that do not shrink (flex:0 0 auto)
       and the gap between them is a real gap, not leftover space that can run out.
   The cost is that the gap does not adapt to how wide my hand happens to be — turn the clamps up
   or down and what you see is what you get. */
.bf-hands{display:flex; flex-direction:row; align-items:flex-start; justify-content:center;
    gap:clamp(28px, 7vw, 130px); width:100%;}
/* The width is derived from the CARDS, not picked in px, because a mini card is sized in vh
   (6.2vh tall, so 6.2 * 5/7 = 4.43vh wide) and therefore grows with the screen HEIGHT: a fixed
   240px column is roomy at 1080p and too narrow at 1440p, where the cards simply spilled out of
   their frame. Four of them, three 6px gaps, the block's padding and its border, plus --bf-hand-pad
   of clear space at each end. Both columns get the same number, which is what keeps my hand centred. */
.bf-side{--bf-hand-pad:16px;
    flex:0 0 auto; min-width:0;
    width:max(190px, calc(4.43vh * 4 + 18px + 28px + 2px + var(--bf-hand-pad) * 2));
    display:flex; flex-direction:column; gap:8px; align-items:center;}
/* Everything that grows during stage 2 — the card showing, the claims on it, the flip button —
   grows inside here, below the line the side columns start on (align-items:start above). */
.bf-center{display:flex; flex-direction:column; align-items:center; gap:12px;}
.bf-hand-block{display:flex; flex-direction:column; align-items:center; padding:8px 14px;
    border-radius:12px; border:1px solid rgba(255,255,255,.14); background:rgba(255,255,255,.05);
    max-width:100%; box-sizing:border-box;}
/* Clipped, so a long player name can't push a side column past its fixed width and back into
   my hand — the one thing that could still break the layout above. */
.bf-hand-name{margin-bottom:5px; font-size:13px; font-weight:700; color:var(--text);
    max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.bf-hand-block.big .bf-hand-name{font-size:15px;}
.bf-hand-cards{display:flex; gap:6px; align-items:flex-end; justify-content:center; min-height:6.2vh;}
.bf-card.hand{height:12vh;}
.bf-card.handmini{height:6.2vh;}
/* An empty question still to be answered: a dashed slot the size of the card that will land in it,
   so a hand reads as "two to go" rather than as a short one. */
.bf-slot{border:1px dashed rgba(255,255,255,.28); border-radius:.6vh; aspect-ratio:5/7; width:auto;}
.bf-slot.hand{height:12vh;}
.bf-slot.handmini{height:6.2vh;}

/* My own cards are the claim buttons during the pyramid. */
.bf-card.bf-playable{cursor:pointer; transition:transform .1s ease, box-shadow .1s ease;}
.bf-card.bf-playable:hover{transform:translateY(-6px); box-shadow:0 .8vh 1.6vh rgba(0,0,0,.55);}
.bf-card.bf-playable.is-sent{opacity:.5; pointer-events:none;}

/* The four question buttons. Black text on all of them: they used to tint red/black with the
   suit, which made the label colour change from question to question for no reason the player
   could act on (and the suit is already in the pip and the word). The .bf-red / .bf-black classes
   stay on the elements — they cost nothing and say which is which if that is ever wanted back. */
.bf-answers{display:flex; gap:10px; justify-content:center; flex-wrap:wrap;}
.gbtn.bf-answer, .gbtn.bf-answer.bf-red, .gbtn.bf-answer.bf-black{color:#000;}
/* "Equal" is the long-odds call on Q2 (triple if it lands), so it is set apart from the two
   safe answers rather than reading as a third of the same kind. */
.gbtn.bf-answer.bf-equal{background:linear-gradient(180deg,#e2a33c,#cf8b28);}

/* The sip frames, in the two bottom corners Drunk Pirate keeps its card trays in (same geometry,
   same z-index band: above the felt at 9-13, below the top bar at 40 and the player dock at 45).
   Mine bottom-left, everyone else's bottom-right. Both are column stacks anchored at the bottom,
   so a new stage's frame appears underneath and pushes the older ones upwards on its own. */
.bf-panel{position:fixed; bottom:92px; z-index:36; display:flex; flex-direction:column; gap:8px;
    max-height:calc(100vh - 180px); overflow:auto;}
.bf-panel[hidden]{display:none;}   /* [hidden] alone loses to the display above */
.bf-panel-me{left:7vw;}
.bf-panel-players{right:7vw; align-items:flex-end;}
/* One player = one row; its stage frames sit side by side and stay centred as a pair. */
.bf-tally-row{display:flex; gap:8px; justify-content:center; align-items:stretch;}
.bf-tally{min-width:150px; padding:8px 10px; text-align:center;
    background:var(--panel); border:1px solid var(--panel-border); border-radius:12px;
    box-shadow:var(--shadow); -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);}
/* The quiz verdict: floats up off the card that just landed and fades, 3s and gone. Fixed to the
   viewport and positioned from the card's measured centre (the hand is rebuilt every poll, so
   parenting it to the card would tear the animation down mid-flight); translate(-50%,-50%) is
   what makes that measured point the CENTRE of the text rather than its corner. The heavy shadow
   plus the dark halo is there so it stays readable over a face card, which is mostly white. */
.bf-verdict{position:fixed; z-index:39; pointer-events:none; white-space:nowrap;
    font-size:19px; font-weight:800; letter-spacing:.01em;
    /* Two jobs, so two kinds of shadow: the four tight 1px offsets draw a hard black outline that
       separates the letters from a white card face, and the wide blurred ones sit behind it as a
       dark halo for the felt. -webkit-text-stroke thickens the outline again where it is
       supported; it is additive, so nothing is lost where it is not. */
    -webkit-text-stroke:.6px rgba(0,0,0,.9);
    text-shadow:
        -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000,
        0 2px 5px rgba(0,0,0,1), 0 0 8px rgba(0,0,0,.95), 0 0 18px rgba(0,0,0,.85),
        0 0 34px rgba(0,0,0,.6);
    animation:bfVerdict 3s ease-out forwards;}
.bf-verdict.is-right{color:#78ef97;}
.bf-verdict.is-wrong{color:#ff8f86;}
/* Most of the climb happens early and then eases off, so it reads as thrown upward rather than
   dragged. It ends roughly five line-heights above the card, well clear of the hand. */
@keyframes bfVerdict{
    0%{opacity:0; transform:translate(-50%,-50%) scale(.85);}
    10%{opacity:1; transform:translate(-50%,-90%) scale(1.06);}
    22%{transform:translate(-50%,-190%) scale(1);}
    70%{opacity:.95; transform:translate(-50%,-420%) scale(1);}
    100%{opacity:0; transform:translate(-50%,-560%) scale(1);}}
@media (prefers-reduced-motion:reduce){
    /* No flight, just a fade in and out on the spot. */
    .bf-verdict{animation:bfVerdictStill 3s ease-out forwards;}
    @keyframes bfVerdictStill{
        0%{opacity:0; transform:translate(-50%,-50%);}
        12%,70%{opacity:1; transform:translate(-50%,-50%);}
        100%{opacity:0; transform:translate(-50%,-50%);}}
}
@media (max-width:640px){
    .bf-verdict{font-size:15px;}
}

/* Sips still to hand out: the frame breathes until the last one is dealt. GREEN when it's mine to
   deal (something to do), ORANGE when it's another player's (something to wait for) — the same
   two readings the buttons and the turn markers use everywhere else. Only the stage being played
   can owe (an older one couldn't have ended while it did), so exactly one cell per player lights
   up. Border AND glow animate together: on a bright background a glow alone washes out. */
.bf-tally.is-owing, .bf-tally.is-owing-me{animation:bfOwing 1.4s ease-in-out infinite;}
.bf-tally.is-owing{--bf-owe:255,152,0; --bf-owe-lit:255,167,38;}
.bf-tally.is-owing-me{--bf-owe:63,174,90; --bf-owe-lit:88,205,116;}
@keyframes bfOwing{
    0%,100%{border-color:rgba(var(--bf-owe),.55); box-shadow:var(--shadow), 0 0 6px rgba(var(--bf-owe),.28);}
    50%{border-color:rgba(var(--bf-owe-lit),1); box-shadow:var(--shadow), 0 0 18px rgba(var(--bf-owe),.65);}}
/* Respect a reduced-motion preference: keep the colour, drop the breathing. */
@media (prefers-reduced-motion:reduce){
    .bf-tally.is-owing, .bf-tally.is-owing-me{animation:none; border-color:rgba(var(--bf-owe-lit),.9);}
}
.bf-tally-title{font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted);
    font-weight:800; margin-bottom:6px;}
.bf-tally-line{font-size:13px; color:var(--text); line-height:1.45;}
.bf-tally-line b{font-weight:800;}
/* The split under "Drink" and the per-giver breakdown are both hints: same grey, smaller. */
.bf-tally-hint{font-size:11px; color:var(--muted); line-height:1.45;}
.bf-tally-hint b{font-weight:800;}
/* Gap under the "(x by cards | y received)" breakdown, so Drink and its split read as one group
   and Deal as another. Same trick as .bf-tally-from below: margin, not an empty element. */
.bf-tally-split{margin-bottom:8px;}
/* The spacer before the breakdown is this rule's margin, not an empty element. */
.bf-tally-from{margin-top:8px; font-size:11px; color:var(--muted); font-weight:700;}

@media (max-width:900px){
    .bf-panel{bottom:80px;}
    .bf-panel-me{left:2vw;}
    .bf-panel-players{right:2vw;}
    .bf-tally{min-width:0; padding:6px 8px;}
    .bf-tally-line{font-size:12px;}
    .bf-tally-title, .bf-tally-hint, .bf-tally-from{font-size:10px;}
}

/* While you have sips to hand out, every other seat avatar becomes a click-to-give target —
   the same steady gold "pick a player" pulse Blackjack's 21 payout uses. */
.seat.occupied.bf-give-target{cursor:pointer;}
.seat.occupied.bf-give-target .seat-inner{border-color:#ffd54a;
    animation:piratePick 1.15s ease-in-out infinite;}

@media (max-width:640px){
    .bf-card.hand{height:78px;}
    .bf-card.handmini{height:42px;}
    .bf-slot.hand{height:78px;}
    .bf-slot.handmini{height:42px;}
    .bf-card.bf-big{height:90px;}
    .bf-hands{gap:6px;}
    .bf-side{gap:5px;}
    .bf-claim-card{height:52px;}
}
